一、Socket定义
Socket:在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯 一标识网络通讯中的一个进程,所以“IP地址+端口号”就称为socket。 在TCP协议中,建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。 TCP/IP协议最早在BSD UNIX上实现,为TCP/IP协议设计的应用层编程接口称为socket API。
二、TCP套接字编程模型
1、服务器端流程简:
(1)创建套接字(socket);
(2)将套接字绑定到一个本地地址和端口上(bind);
(3)将套接字设定为监听模式,准备接受客户端请求(listen);
(4)阻塞等待客户端请求到来。当请求到来后,接受连接请求,返回一个新的对应于此客户端连接的套接字sockClient(accept);
(5)用返回的套接字sockClient和客户端进行通信(send/recv);
(6)返回,等待另一个客户端请求(accept);
(7)关闭套接字(close);
2、客户端流程:
(1) 创建套接字(socket);
(2) 向服务器发出连接请求(connect);
(3) 和服务器进行通信(send/recv);
(4) 关闭套接字(close);
具体流程如下图所示:
三、Socket基本操作
1、创建套接字,socket函数
int socket(int domain, int type, int protocol)
//成功时返回文件句柄,失败时返回-1.
//domain 套接字中使用的协议族信息
//type 套接字数据传输类型信息
//protocol 计算机间通信使用的协议信息
(1)domain所选的协议族
名称 | 协议族 |
PF_INET | IPV4互联网协议族 |
PF_INET6 | IPV6互联网协议族 |
(2)type套接字类型
type类型 | 作用 |
SOCK_STREAM | 面向连接的套接字 |
SOCK_DGRAM | 面向消息的套接字 |
(3)protacol协议的最终选择
同一协议中存在多个数据类型传输方式相同的协议,就通过protocol区分最终协议,如果只有一个,默认为0。
2、分配IP和端口,bind函数
int bind(int sockfd, struct sockaddr * myaddr, socklen_t addrlen)
//成功返回0,失败返回-1
//sockfd 套接字文件描述符
//myaddr 结构体变量地址值,包括IP地址和端口号
//addrlen 结构体变量的长度
bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
(1)sockfd:即socket描述字,它是通过socket()函数创建的,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
(2)addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如IPV4对应的是:
struct sockaddr_in {
/*address family:AF_INET*/
sa_family_t sin_family;
/*port in network byte order */
in_port_t sin_port;
/* internet address */
struct in_addr sin_addr;
};
/* Internet address. */
struct in_addr {
/* address in network byte order */
uint32_t s_addr;
};
IPV6对应的是:
struct sockaddr_in6 {
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* port number */
uint32_t sin6_flowinfo;/* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /*Scope ID (new in 2.4) */
};
struct in6_addr {
unsigned char s6_addr[16]; /* IPv6 address */
};
(3)addrlen:对应的是地址的长度。
这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
3、listen()/connect()函数
如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。
4、accept()函数
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。
注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
5、read()/write()函数
万事具备只欠东风,至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!网络I/O操作有下面几组:
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
开发语言不同可能读写函数也就不同,只要把自己想要发送的消息,以字节流的方式写入Socket或者从Socket读出来即可实现网络的I/O操作。
6、close()函数
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。
#include <unistd.h>
int close(int fd);
close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。
注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
三、Socket编程实例
咋Linux上实现的一个简单的socket通信实例:
Server端:
#include<stdio.h>
//下面两个头文件是使用socket必须引入的
#include<sys/types.h>
#include<sys/socket.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
//启动服务器通信端口
int startup(int _port,const char* _ip)
{
//socket()函数打开一个网络通信窗口,成功则返回一个文件描述符,应用程序可以向读写文件一样用read/write在网络上转发数据。
//若调用出错则返回-1
//socket()函数的三个参数:协议类型, 套接字类型, 协议类型的常量或设置为0
//AF_INET(IPv4协议) SOCK_STREAM字节流套接字
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
perror("socket");
exit(1);
}
struct sockaddr_in local;//网络编程中常用的数据结构
local.sin_family=AF_INET;//IPVC4地址族
local.sin_port=htons(_port);//将端口地址转换为网络二进制数字
local.sin_addr.s_addr=inet_addr(_ip);//将网络地址转换为网络二进制数字
socklen_t len=sizeof(local);
//绑定套接字:成功返回0, 失败返回-1
//功能:将sock和local绑定在一起,使得sock这个用于网络通讯的问价描述符监听local所描述的地址和端口
if(bind(sock,(struct sockaddr*)&local,len)<0)
{
perror("bind");
exit(2);
}
//listen(int sockfd, int backlog)监听函数,sockfd为要监听的socket套接字,backlog为可以排队的最大连接数。
//socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
//监听成功返回0, 失败返回-1
if(listen(sock,5)<0)
{
perror("listen");
exit(3);
}
return sock;
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
printf("Usage: [local_ip] [local_port]",argv[0]);
return 3;
}
//启动服务器套接字listen_socket
int listen_socket=startup(atoi(argv[2]),argv[1]);
struct sockaddr_in remote;
socklen_t len=sizeof(struct sockaddr_in);
while(1)
{
//accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen)函数,
//sockfd为服务器的socket套接字,addr为客户端协议地址,addrlen为协议地址的长度,
//如果accept成功,则返回一个由内核自动生成的全新套接字,代表与返回客户的TCP连接
int socket=accept(listen_socket,(struct sockaddr*)&remote,&len);
if(socket<0)
{
perror("accept");
continue;
}
//inet_ntoa:将网络二进制数字转换为网络地址
//ntohs:将网络二进制数字转换为端口号
printf("client,ip:%s,port:%d\n",inet_ntoa(remote.sin_addr)\
,ntohs(remote.sin_port));
char buf[1024];
while(1)
{
//调用网络I/O进行读写
ssize_t _s=read(socket,buf,sizeof(buf)-1);
if(_s>0)
{
buf[_s]=0;
printf("client# %s\n",buf);
}
else
{
printf("client is quit!\n");
break;
}
}
//关闭套接字
close(socket);
}
return 0;
}
Client端:
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
static void usage(const char* proc)
{
printf("usage:%s [ip] [port]\n",proc);
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
usage(argv[0]);
return 3;
}
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
perror("socket");
exit(1);
}
struct sockaddr_in server;
server.sin_family=AF_INET;
server.sin_port=htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
//connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
//sockfd为要监听的socket套接字,addr参数为服务器的socket地址,addrlen为socket地址的长度。
//客户端通过调用connect函数来建立与TCP服务器的连接。
if(connect(sock,(struct sockaddr*)&server,(socklen_t)sizeof(server))<0)
{
perror("connect");
exit(2);
}
char buf[1024];
while(1)
{
printf("send#");
fflush(stdout);
ssize_t _s=read(0,buf,sizeof(buf)-1);
buf[_s-1]=0;
write(sock,buf,_s);
}
close(sock);
return 0;
}