缘由
最近在新的平台上测试程序, 以前一些没有注意到的问题都成为了性能瓶颈, 通过设置一些 TCP/IP 选项能够解决一部分问题, 当然根本的解决方法是重构代码, 重新设计服务器框架.
先列出几个 TCP/IP 选项:
**man 7 socket:**SO_REUSEADDRSO_RECVBUF/SO_SNDBUFSO_KEEPALIVESO_LINGER**man 7 tcp:**TCP_CORKTCP_NODELAYTCP_DEFER_ACCEPTTCP_KEEPCNT/TCP_KEEPIDLE/TCP_KEEPINTVLSO_REUSEADDR**man 命令的 领域 名称 说明 **1 用户命令, 可由任何人启动的. 2 系统调用, 即由内核提供的函数. 3 例程, 即库函数. 4 设备, 即 / dev 目录下的特殊文件. 5 文件格式描述, 例如 / etc/passwd.6 游戏, 不用解释啦! 7 杂项, 例如宏命令包, 惯例等. 8 系统管理员工具, 只能由 root 启动. 9 其他(Linux 特定的), 用来存放内核例行程序的文档.
SO_SNDBUF and SO_RECVBUF
The "SO_" prefix is for "socket option", so yes, these are per-socket settings for the per-socket buffers. There are usually system-wide defaults and maximum values.
SO_RCVBUF is simpler to understand: it's the size of the buffer the kernel allocates to hold the data arriving into the given socket during the time between it arrives over the network and when it is read by the program that owns this socket. With TCP, if data arrives and you aren't reading it, the buffer will fill up, and the sender will be told to slow down (using TCP window adjustment mechanism). For UDP, once the buffer is full, new packets will just be discarded.
SO_SNDBUF, I think, only matters for TCP (in UDP, whatever you send goes directly out to the network). For TCP, you could fill the buffer either if the remote side isn't reading (so that remote buffer becomes full, then TCP communicates this fact to your kernel, and your kernel stops sending data, instead accumulating it in the local buffer until it fills up). Or it could fill up if there is a network problem, and the kernel isn't getting acknowledgements for the data it sends. It will then slow down sending data on the network until, eventually, the outgoing buffer fills up. If so, future write() calls to this socket by the application will block (or return EAGAIN if you've set the O_NONBLOCK option).
参见 http://stackoverflow.com/questions/4257410/what-are-so-sndbuf-and-so-recvbuf
SO_REUSEADDR 选项:
在服务器程序中, SO_REUSEADDR socket 选项通常在调用 bind()之前被设置. SO_REUSEADDR 可以用在以下四种情况下: (摘自Unix 网络编程卷一, 即 UNPv1)
当有一个有相同本地地址和端口的 socket1 处于 TIME_WAIT 状态时, 而你启动的程序的 socket2 要占用该地址和端口, 你的程序就要用到该选项.
SO_REUSEADDR 允许同一 port 上启动同一服务器的多个实例(多个进程). 但每个实例绑定的 IP 地址是不能相同的. 在有多块网卡或用 IP Alias 技术的机器可以测试这种情况.
SO_REUSEADDR 允许单个进程绑定相同的端口到多个 socket 上, 但每个 socket 绑定的 ip 地址不同. 这和 2 很相似, 区别请看 UNPv1.
SO_REUSEADDR 允许完全相同的地址和端口的重复绑定. 但这只用于 UDP 的多播, 不用于 TCP.
TCP_NODELAY/TCP_CHORK 选项:
TCP_NODELAY/TCP_CHORKTCP_NODELAY 和 TCP_CORK 基本上控制了包的 "Nagle 化",Nagle 化在这里的含义是采用 Nagle 算法把较小的包组装为更大的帧. TCP_NODELAY 和 TCP_CORK 都禁掉了 Nagle 算法, 只不过他们的行为不同而已. TCP_NODELAY 不使用 Nagle 算法, 不会将小包进行拼接成大包再进行发送, 直接将小包发送出去, 会使得小包时候用户体验非常好.
当在传送大量数据的时候, 为了提高 TCP 发送效率, 可以设置 TCP_CORK,CORK 顾名思义, 就是 "塞子" 的意思, 它会尽量在每次发送最大的数据量. 当设置了 TCP_CORK 后, 会有阻塞 200ms, 当阻塞时间过后, 数据就会自动传送.
SO_LINGER 选项:
SO_LINGERlinger, 顾名思义是延迟延缓的意思, 这里是延缓面向连接的 socket 的 close 操作.
默认, close 立即返回, 但是当发送缓冲区中还有一部分数据的时候, 系统将会尝试将数据发送给对端. SO_LINGER 可以改变 close 的行为.
控制 SO_LINGER 通过下面一个结构: struct linger{ int l_onoff; /0=off, nonzero=on/int l_linger; /linger time, POSIX specifies units as seconds/}; 通过结构体中成员的不同赋值, 可以表现为下面几种情况:
l_onoff 设置为 0, 选项被关闭. l_linger 值被忽略, 就是上面的默认情形, close 立即返回.
l_onoff 设置为非 0,l_linger 被设置为 0, 则 close()不被阻塞立即执行, 丢弃 socket 发送缓冲区中的数据, 并向对端发送一个 RST 报文. 这种关闭方式称为 "强制" 或 "失效" 关闭.
l_onoff 设置为非 0,l_linger 被设置为非 0, 则 close()调用阻塞进程, 直到所剩数据发送完毕或超时. 这种关闭称为 "优雅的" 关闭.
注意: 这个选项需要谨慎使用, 尤其是强制式关闭, 会丢失服务器发给客户端的最后一部分数据. UNP 中: The TIME_WAIT state is our friend and is there to help us(i.e., to let the old duplicate segments expire in the network).
TCP_DEFER_ACCEPT 选项:
TCP_DEFER_ACCEPTdefer accept, 从字面上理解是推迟 accept, 实际上是当接收到第一个数据之后, 才会创建连接, 三次握手完成, 连接还没有建立.
对于像 HTTP 等非交互式的服务器, 这个很有意义, 可以用来防御空连接攻击(只是建立连接, 但是不发送任何数据). 使用方法如下:
val = 5; setsockopt(srv_socket->fd, SOL_TCP, TCP_DEFER_ACCEPT, &val, sizeof(val)) ;
里面 val 的单位是秒, 注意如果打开这个功能, kernel 在 val 秒之内还没有收到数据, 不会继续唤醒进程, 而是直接丢弃连接. 如果服务器设置 TCP_DEFER_ACCEPT 选项后, 服务器受到一个 CONNECT 请求后, 三次握手之后, 新的 socket 状态依然为 SYN_RECV, 而不是 ESTABLISHED, 操作系统不会 Accept.
由于设置 TCP_DEFER_ACCEPT 选项之后, 三次握手后状态没有达到 ESTABLISHED, 而是 SYN_RECV. 这个时候, 如果客户端一直没有发送 "数据" 报文, 服务器将重传 SYN/ACK 报文, 重传次数受 net.ipv4.tcp_synack_retries 参数控制, 达到重传次数之后, 才会再次进行 setsockopt 中设置的超时值, 因此会出现 SYN_RECV 生存时间比设置值大一些的情况.
SO_KEEPALIVE 选项:
SO_KEEPALIVE/TCP_KEEPCNT/TCP_KEEPIDLE/TCP_KEEPINTVL 如果一方已经关闭或异常终止连接, 而另一方却不知道, 我们将这样的 TCP 连接称为半打开的.
TCP 通过保活定时器 (KeepAlive) 来检测半打开连接. 在高并发的网络服务器中, 经常会出现漏掉 socket 的情况, 对应的结果有一种情况就是出现大量的 CLOSE_WAIT 状态的连接. 这个时候, 可以通过设置 KEEPALIVE 选项来解决这个问题, 当然还有其他的方法可以解决这个问题.
使用方法如下:
//Setting For KeepAliveint keepalive = 1;setsockopt(incomingsock,SOL_SOCKET,SO_KEEPALIVE,(void*)(&keepalive),(socklen_t)sizeof(keepalive));int keepalive_time = 30;setsockopt(incomingsock, IPPROTO_TCP, TCP_KEEPIDLE,(void*)(&keepalive_time),(socklen_t)sizeof(keepalive_time));int keepalive_intvl = 3;setsockopt(incomingsock, IPPROTO_TCP, TCP_KEEPINTVL,(void*)(&keepalive_intvl),(socklen_t)sizeof(keepalive_intvl));int keepalive_probes= 3;setsockopt(incomingsock, IPPROTO_TCP, TCP_KEEPCNT,(void*)(&keepalive_probes),(socklen_t)sizeof(keepalive_probes));
设置 SO_KEEPALIVE 选项来开启 KEEPALIVE, 然后通过 TCP_KEEPIDLE,TCP_KEEPINTVL 和 TCP_KEEPCNT 设置 keepalive 的开始时间, 间隔, 次数等参数.
当然, 也可以通过设置 / proc/sys/net/ipv4/tcp_keepalive_time,tcp_keepalive_intvl 和 tcp_keepalive_probes 等内核参数来达到目的, 但是这样的话, 会影响所有的 socket, 因此建议使用 setsockopt 设置.
参考:
- man 7 socket
- man 7 tcp
- Unix Network Programming Section 7.5 "Generic Socket Options"
- http://www.linuxsir.org/bbs/showthread.php?t=55738
- http://blog.chinaunix.net/u/12592/showart_1723934.html
- http://blog.csdn.net/fullsail/archive/2009/08/09/4429102.aspx
- http://www.kernelchina.org/?q=node/110
- http://blog.chinaunix.net/u/12592/showart.php?id=2059174
来源: https://juejin.im/entry/5b0bd6376fb9a009df63a5f9