使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)来实现网络进程之间的通信。
socket是应用层与TCP/IP协议族通信的中间软件抽象层
Socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”。在许多操作系统中,套接字API最初是作为UNIX操作系统的一部分而开发的,所以套接字API与系统的其他I/O设备集成在一起。应用程序要为因特网通信而创建一个套接字(socket)时,操作系统就返回一个小整数作为描述符(descriptor)来标识这个套接字。然后应用程序以该描述符作为传递参数,通过调用相应函数(如read、write、close等)来完成某种操作(如从套接字中读取或写入数据)。
socket是“open-write/read-close”模式的一种实现
以TCP为例介绍几个基本的socket接口函数
int socket(int domain, int type, int protocol);
服务器端中socket()的创建
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
客户端中socket()的创建
con_fd = socket(AF_INET, SOCK_STREAM, 0);
调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数。通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,由系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。当然客户端也可以在调用connect()之前bind一个地址和端口,这样就能使用特定的IP和端口来连服务器了。
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
typedef unsigned short int sa_family_t;
struct sockaddr {
sa_family_t sa_family; /* 2 bytes address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
}
typedef unsigned short sa_family_t;
typedef uint16_t in_port_t;
struct in_addr {
uint32_t s_addr;
};
struct sockaddr_in {
sa_family_t sin_family; /* 2 bytes address family, AF_xxx such as AF_INET */
in_port_t sin_port; /* 2 bytes port*/
struct in_addr sin_addr; /* 4 bytes IPv4 address*/
/* Pad to size of `struct sockaddr'. */unsigned char sin_zero[8]; /* 8 bytes unused padding data, always set be zero */
};
typedef unsigned short sa_family_t;
typedef uint16_t in_port_t;
struct in6_addr
{
union
{
uint8_t __u6_addr8[16];
uint16_t __u6_addr16[8];
uint32_t __u6_addr32[4];
} __in6_u;
}
struct sockaddr_in6 {
sa_family_t sin6_family; /*2B*/
in_port_t sin6_port; /*2B*/
uint32_t sin6_flowinfo; /*4B*/
struct in6_addr sin6_addr; /*16B*/
uint32_t sin6_scope_id; /*4B*/
};
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family;
char sun_path[UNIX_PATH_MAX];
};
其实只需要知道每种不同协议格式地址(sockaddr_in、sockaddr_in6、sockaddr_un)前两个字节(sin_family、sin6_family、sun_family)对应到通用套接字结构体(struct sockaddr)中的sa_family值以及传进来的结构体变量的首地址就可以了,对于struct sockaddr中sa_data成员占多少个自己、或要不要都无所谓,因为强制类型转换我们只需要相应的协议地址格式的结构体(sockaddr_in、sockaddr_in6、sockaddr_un)以及首地址就行了。
以ipv4为例:
#define LISTEN_PORT 9999;
...
struct sockaddr_in serv_addr;
...
memset(&serv_addr, 0, sizeof(serv_addr))
serv_addr.sin_family = AF_INET; //ipv4
serv_addr.sin_port = htons(LISTEN_PORT); //监听端口号
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); //监听所有IP
if( bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0 )
{
printf("create socket failure: %s\n", strerror(errno));
return -2;
}
/*(struct sockaddr *)&serv_addr:serv_addr为struct sockaddr_in类型,
bind()中要求const struct sockaddr *addr,
故(struct sockaddr *)&serv_addr实现强制转换 */
serv_addr.sin_port = htons(LISTEN_PORT); //监听端口号
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); //监听所有IP
上述调用 **htons()**和 **htonl()**两个函数实现将端口号和IP地址由主机字节序转换成网络字节序
h表示host, n表示network, s表示short(2字节/16位), l表示long(4字节/32位)。因为端口号是16位的,所以用htons()把端口号从主机字节序转换成网络字节序, 而IP地址是32位的,所以htonl()函数把IP地址从主机字节序转换成网络字节序。INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。 一般来说,在各个系统中均定义成为0值。这里也就意味着监听所有的IP地址。
注意:socket有两种类型:
socket被创建以后默认为active主动模式,所以对于服务器端而言,写socket后一定要调用listen()函数,其可将socket由主动切换到被动监听模式,等待客户的连接请求。
int listen(int sockfd, int backlog);
backlog其实是一个连接队列,在Linux内核2.2之前,backlog大小包括半连接状态和全连接状态两种队列大小,当服务器接收到客户端的ACK报文后,该条目将从半连接队列搬到全连接队列尾部,即 accept queue (服务器端口状态为:ESTABLISHED)。在Linux内核2.2之后,分离为两个backlog来分别限制半连接(SYN_RCVD状态)队列大小和全连接(ESTABLISHED状态)队列大小。
SYN queue 队列长度由 /proc/sys/net/ipv4/tcp_max_syn_backlog 指定,默认为2048。
Accept queue 队列长度由 /proc/sys/net/core/somaxconn 和使用listen函数时传入的参数,二者取最小值。默认为128。
在Linux内核2.4.25之前,是写死在代码常量 SOMAXCONN ,在Linux内核2.4.25之后,在配置文件/proc/sys/net/core/somaxconn 中直接修改,或者在 /etc/sysctl.conf 中配置 net.core.somaxconn = 128 。
该函数属于阻塞函数,即一旦调用该函数,若不满足条件,该函数则不会返回(等待客户端连接,若客户端不连接则默认为阻塞模式)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept函数的返回值是由内核自动生成的一个全新的描述字(fd),代表与返回客户的TCP连接。如果想发送数据给该客户端,则我们可以调用write()等函数往该fd里写内容即可;而如果想从该客户端读内容则调用read()等函数从该fd里读数据即可。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个新的socket描述字,当服务器完成了对某个客户的服务,就应当把该客户端相应的的socket描述字关闭。
参考如下:
client_fd = accept(listen_fd, NULL, NULL);
int main()
{
struct sockaddr_in cli_addr;
socklen_t cliaddr_len;//局部变量,存放在栈中,不赋初始值时每次运行时给个随机值
cliaddr_len = sizeof(cli_addr);//若不赋值,有时会运行成功,有时会报错提示为无效参数
client_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, &cliaddr_len);
if(client_fd < 0)
{
printf("accept new socket failure: %s\n", strerror(errno));
return -2;
}
printf("Accept new client[%s:%d] with fd [%d]\n", inet_ntoa(cli_addr.sin_addr),
ntohs(cli_addr.sin_port), client_fd);
}
accept()系统调用将会把客户端的信息保存在cli_addr这个结构体变量中,其中cli_addr是struct sockaddr_in 这种IPv4地址类型,客户端的IP地址和端口号都保存在该结构体中。在该结构体中IP地址是以32位整形值的形式存放,端口号也是以网络字节序的形式存放。这时可以使用inet_ntoa() 函数将32位整形的IP地址转换成点分十进制字符串格式的IP地址“127.0.0.1”,我们也可以调用**ntohs()**函数将网络字节序的端口号转换成主机字节序的端口号。
强调:
connect() 函数也是一个阻塞函数。
TCP客户端程序调用socket()创建socket fd之后,就可以调用connect()函数来连接服务器。如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求并使accept()返回,accept()返回的新的文件描述符就是对应到该客户的TCP连接,通过这两个文件描述符(客户端connect的fd和服务器端accept返回的fd)就可以实现客户端和服务器端的相互通信。
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
在调用connect之前,需要先设置服务器端的IP地址和端口等信息到addr中去,参考示例:
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERVER_PORT);
inet_aton( SERVER_IP, &serv_addr.sin_addr );
if( connect(conn_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
{
printf("connect to server [%s:%d] failure: %s\n", SERVER_IP, SERVER_PORT, strerror(errno));
return 0;
}
SERVER_IP 可定义为“127.0.0.1”,这是一个回环测试IP地址,该地址不管在哪台机器上都表示本机,因为服务器和客户端都在同一台机器上运行,这时我们可以直接使用该地址。如果我们希望连接的服务器运行在另外一台主机上,则需将它改为服务器主机的IP地址即可,如“192.168.0.5”。
IP地址“127.0.0.1”这是点分十进制形式的字符串形式,而在结构体struct sockaddr_in 中IP地址是以32位(即4字节整形类型)数据保存的,这时我们可以调用 inet_aton() 函数将点分十进制字符串转换成 32位整形类型。同样,端口号也要使用 htons() 函数从主机字节序转成网络字节序。
答:socket三路握手是在客户端开始调用connect()函数时发起的,但三路握手的完成不是在accept()阶段完成的;三路握手的完成由Linux操作系统帮忙完成的;三路握手完成以后将发出的申请放到队列中,即listen()函数中提到的backlog,accept()函数从队列中拿取第一个并返回一个socket_fd;
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
read()函数调用的返回值就是实际读到的字节数,返回值小于0出错,等于0表示socket断开
write()函数调用的返回值就是实际写的字节数,返回值小于0出错,
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用close()来关闭一样。close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。
int close(int fd);
如果对socket fd调用close()则会触发该TCP连接断开的四路握手,有些时候需要数据发送出去并到达对方之后才能关闭socket套接字,则可以调用shutdown()函数来半关闭套接字:
int shutdown(int sockfd, int how);
如果how的值为 SHUT_RD 则该套接字不可再读入数据了; 如果how的值为 SHUT_WR 则该套接字不可再发送数据了; 如果how的值为 SHUT_RDWR 则该套接字既不可以读,也不可以写数据了。