《Linux高性能服务器编程》阅读笔记:
Linux系统中,有控制文件描述符属性的通用Posix系统调用fcntl(),还有两个专门用来读取和设置socket文件描述符属性的方法:
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
(1) sockfd参数指定被操作的目标socket
(2) level参数该描述符的使用的协议,如IPPROTO_IP(IPv4)、IPPROTO_IPV6(IPv6)、IPPROTO_TCP(TCP选项)
(3) option_name参数指定要设置的属性的名字,如用于修改描述符的接收缓冲区大小的SO_RCVBUF
(4) optval参数和optlen分别指定该属性要使用的值和值的属性,如修改缓冲区大小时optval为缓冲区的大小
具体如下表格:
level | option_name | optval | 说明 |
---|---|---|---|
SOL_SOCKET (通用socket协议,与具体协议无关) | SO_DEBUG | int | 打开调试信息 |
SO_REUSEADDR | int | 重用本地地址(TIME_WAIT状态的地址) | |
SO_TYPE | int | 获取socket类型 | |
SO_ERROR | int | 获取并清除socket错误状态 | |
SO_DONTROUTE | int | 不查看路由表,直接将数据发送给本地局域网的主机。含义和send()的flags参数设置为MSG_DONTROUTE标志类似 | |
SO_RCVBUF | int | TCP接收缓冲区的大小 | |
SO_SNDBUF | int | TCP发送缓冲区的大小 | |
SO_KEEPALIVE | int | 发送周期性特定报文以维持连接 | |
SO_OOBINLINE | int | 将接收到的带外数据(如果有的话)存留在普通数据的输入队列中,此时程序员不能使用带MSG_OOB标志的读操作来读取带外数据(而应该向读取普通数据那样读取带外数据) | |
SO_LINGER | linger(结构体) | 关闭连接时,若发送缓冲区还有数据则延迟关闭 | |
SO_RCVLOWAT | int | TCP接收缓冲区低水位标记 | |
SO_SNDLOWAT | int | TCP发送缓冲区低水位标记 | |
SO_RCVTIMEO | timeval(结构体) | 接收数据超时时间 | |
SO_SNDTIMEO | timeval(结构体) | 发送数据超时时间 | |
IPPROTO_IP (IPv4协议) | IP_TOS | int | 服务类型 |
IP_TTL | int | 生存时间 | |
IPPROTO_IPV6 (IPv6选项) | IPV6_NEXTHOPF | sockaddr_in6 | 下一跳的IP地址 |
IPV6_RECVPKTINFO | int | 接收分组信息 | |
IPV6_DONTFRAG | int | 禁止分片 | |
IPV6_RECVTCLASS | int | 接收通信类型 | |
IPPROTO_TCP (TCP选项) | TCP_MAXSEG | int | TCP最大报文段大小 |
TCP_NODELAY | int | 禁止Nagle算法 |
函数执行成功返回0,否则返回-1并设置errno。
下面是几个重要的socket选项:
(1) SO_REUSEADDR
TIME_WAIT状态出现的时机如下图所示:
在TCP协议–TCP连接的状态转移中说道,当客户端主动发出FIN报文且得到服务端的确认、服务端的FIN报文后,在之后很长的时间内就一直处于TIME_WAIT状态,这时间为2MSL(2倍报文的最大生存时间)。若是服务端主动发起FIN报文,也会出现TIME_WAIT状态。处于TIME_WAIT状态的socket地址是不可被使用的,当socket被设置为SO_REUSEADDR选项后,可以强制使用尚处于TIME_TIME的socket地址,代码片段为:
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
assert(sockfd >= 0);
int ret = 1;
setsocketopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &ret, sizeof(ret));
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
inet_pton(AF_INET, ip, &addr.sin_addr);
addr.sin_port = htons(port);
bind(sockfd, (struct sockaddr* )&addr, sizoef(addr));
setsocketopt()设置套接字为SO_REUSEADDR后,即使sockfd处于TIME_WAIT状态,与之绑定的sock地址也能被立即重用。此外,内核参数
/proc/sys/net/ipv4/tcp_tw_recycle
也可以打开快速回收被关闭的sockfd(默认为0表示关闭),从而使得TCP连接根本不会进入TIME_WAIT状态。
(2) SO_RCVBUF和SO_SNDBUF
SO_RCVBUF和SO_SNDBUF分别用来设置或者获取TCP接收缓冲区和发送缓冲区的大小。注意,虽然程序员能够使用setsockopt()来设置缓冲区的大小,但是系统还是会将其值加倍且不得小于某个值。TCP的接收缓冲区的最小值为2240字节,发送缓冲区的最小值为4480字节(不同的系统可能不同),其目的是确保一个TCP连接拥有足够的空闲缓冲区来处理拥塞。另外直接修改内核参数
/proc/sys/net/ipv4/tcp_rmem
/proc/sys/net/ipv4/tcp_wmem
来强制接收/发送缓冲区大小没有限制。修改TCP接收发送缓冲区的代码片段为:
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
ERRP(socket_fd <= 0, return -1, "socket");
int rcv_buf_size = 50;
int len = sizeof(rcv_buf_size);
setsockopt(socket_fd, SOL_SOCKET, SO_RCVBUF, &rcv_buf_size, len);
getsockopt(socket_fd, SOL_SOCKET, SO_RCVBUF, &rcv_buf_size, (socklen_t* )&len);
printf("the tcp recv buffer size after setting is %d\n", rcv_buf_size);
//...
int ret = bind(socket_fd, (struct sockaddr* )&address, sizeof(address));
ret = listen(socket_fd, 5);
int connfd = accept(socket_fd, (struct sockaddr* )&client, &client_addrlen);
//...
这里将TCP接收缓冲区设置为50字节,由于它小于系统允许的TCP接收缓冲区的最小字节数(2240),所以系统会忽略此设置操作。缓冲区的大小会直接关系到数据在传输过程中接收通告的大小。
(3) SO_RCVLOWAT和SO_SNDLOWAT
SO_RCVLOWAT和SO_SNDLOWAT分别表示TCP接收缓冲区和发送缓冲区的低水位标记。当TCP接收缓冲区中可读数据的总大小大于其低水位标记时,应用程序可以从该socket上读取数据;当TCP发送缓冲区的空闲空间大于其低水位标记是,应用程序可以往对应的socket写入数据。应用程序一般是通过I/O复用技术来判断socket是否可读可写的:
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
readfds和writefds装载着待被判断可读和可写的文件描述符,通过判断函数返回值即可知道是否可读可写了,而select()的判断依据正是基于上述的水位标记。默认情况下,TCP接收/发送缓冲区的低水位标记均为1字节。
(4) SO_LINGER
默认情况下close()系统调用关闭一个socket连接时,close()立即返回,内核的TCP模块负责将该socket对应的TCP发送缓冲区中残留的数据发送个对端。然而,SO_LINGER选项可用于控制close()系统调用在关闭TCP连接时的行为。SO_LINGER选项的选项值是一个linger结构体,其定义为:
struct linger
{
int l_onoff; /* 开启(非0)/关闭(0)该选项 */
int l_linger; /* 滞留时间 */
};
(1) 当l_onoff为0时,SO_LINGER选项不起作用,close()还是以默认行为关闭socket
(2) 当l_onoff不为0,l_linger为0时,close()立即返回,TCP模块将丢弃被关闭的socket的发送缓冲区残留的数据,且返回对端一个复位(RST)报文段。这种情况给服务端提供了异常终止一个连接的方法。
(3) 当l_onoff不为0,l_linger大于0,且该发送缓冲区还存有待发送的数据:
a. 若该socket是阻塞的,close()将等待一段长为l_linger的时间,直到TCP模块发送完所有残留数据并得到对端确认,如果该时间段内TCP模块没有发送完成并得到对端确认,close()返回-1并设置errn为EWOULDBLOCK。
b. 若该socket为非阻塞的,close()立即返回,此时根据其返回值和errno判断残留数据是否发送完毕。
另外要注意:
对服务端而言,监听的socket和用来和客户端通信的socket并不是同一个,且socket选项一般程序员都是设置在后者,然而后者是accept()系统调用从监听队列中直接提取出来的socket,也就是说该socket已经完成了TCP建立连接的3次握手(至少也是完成前两个步骤,即socket连接进入SYN_RCVD状态),完成握手后的socket,某些属性选项将是无法设置,如TCP最大报文段选项是要在同步报文段中发送的,也就是要设置同步报文段发送前的socket。Linux提供的解决办法是: 对监听socket设置这些选项,那么accept()返回的连接用于和客户端通信的socket将自动继承这些选项。这些选项包括: SO_DEBUG、SO_DONTROUTE、SO_KEEPALIVE、SO_LINGER、SO_OOBINLINE、SO_RCVBUF、SO_RCVLOWAT、SO_SNDBUF、SO_SNDLOWAT、TCP_MAXSEG、TCP_NODELAY。
对客户端来说,这些socket则是应该在connect()函数之前设置,因为connect()成功返回后,TCP三次握手已经完成。