常用socket选项
获取和设置socket选项
使用 getsockopt()
和 setsockopt
来获取和设置 socket 的各种选项:
1 | int getsockopt(int socket, |
- 以上两个函数,成功时均返回 0,出错则返回 -1 。
- 关于第二个和第三个参数,参见《UPN》第 151 页。
注意,细心的朋友可能已经注意到:上面的例子中,作用的套接字是 sock_listen,即监听套接字。那么,能不能作用于已连接套接字呢?好问题!是这样的,以下几个选项是由已连接套接字从监听套接字继承而来: SO_KEEPALIVE、SO_LINGER、SO_RECVBUF、SO_SNDBUF、SO_RCVLOWAT、SO_SNDLOWAT、TCP_MAXSEG、TCP_NODELAY 、SO_DEBUF、SO_DONTROUTE、SO_OBBINLINE (红色标记为重点选项)。所以,在这几个选项上设置监听套接字,将影响后面的所有已连接套接字。作为对比,getpeername() 函数就必须要作用于已连接套接字。
SO_RCVBUF 和 SO_SNDBUF
这是两个重要的选项。要弄清楚这两个选项,就必须先搞明白 Socket 的缓冲区机制。下面总结了 Socket 缓冲区的关键特性:
- 每个套接字都有独立的输入/输出缓冲区。
- 创建套接字时,自动生成缓冲区。
- 如果要写(write)的数据大于发送(输出)缓冲区的最大长度,那么将分批写入。
- 如果发送缓冲区全满(分批写入也不能进行),则进程被阻塞(假设套接字是阻塞模式)。
- 发送缓冲区的数据将一直保存(以便重发),直到接收到相应 ACK。
- 接收(输入)缓冲区的数据将一直保存,直到应用层读取(read)。
- 接收缓冲区若满,则直接抛弃新数据(不发送其他任何信息,等待对面重传)。
- TCP 接收窗口大小 <= 接收缓冲区大小。
以上几个特性的实验,参见本博客另一篇文章——网络数据读取的常见问题 ,建议浏览,加深理解。
SO_RCVBUF 直接限制本端 TCP 接收窗口的大小 。下面进行实验:
1 | //server |
分别运行运行 server 和 client 后,server 的接收缓冲区的大小为 6000:
1 | server recv buffer size: 6000 |
我们将 recv_size 设置的为 3000,而实际反馈为 6000,说明 recv_size 只是一个建议值,操作系统会根据一定条件进行修改(×2) 。在笔者操作系统上,接收缓冲区最小为 2304 。
然后我们使用 wireshark 抓包,结果如下:
看第二项,server 发送的 SYN 报文中,win 为 3000,和我们设置的 SO_RECVBUF 大小相同 ,但这并不意味着窗口大小就一定等于 SO_RECVBUF。接收窗口会随着网络状况而不断调整,但肯定不会超过缓冲区的大小 。
在本机上实验多次发现,接收窗口始终不会超过接收缓冲区的一半大小 。
还有以下几点需要注意:
- 上面说过,这两个选项是由监听套接字继承而来,所以对于 server,必须在 listen 前设置选项;对于 client,必须在 connect 前设置选项 。对已连接套接字(在 accept 返回之后)设置选项没有任何作用,因为 accept 只是从已连接队列中取出一个连接而已(参见深入理解socket基本函数)。
- 为了避免潜在的缓冲区空间浪费,接收缓冲区大小应该为 MSS 的整数倍,且至少为 MSS 的 4 倍,参见《UNP》P163。
- 为了最大化性能,缓冲区大小应该约等于带宽-延迟积 ,参见《UNP》P164 。
SO_RCVLOWAT 和 SO_SNDLOWAT
这两个选项作用于 select/poll/epoll,目的在于减少网络 I/O 的次数。SO_RCVLOWAT
指定接收缓冲区中的数据量必须达到多少时,才会唤醒 select/poll/epoll 去读取数据,其默认值为 1 。SO_SNDLOWAT
指定当发送缓冲区的空闲空间大于等于低水平位标记时,将唤醒 select/poll/epoll 写数据到socket ,其默认值为 2048(然而本机实测仍为 1) 。
SO_REUSEADDR
这是服务器最常用的选项之一。该选项有以下几个作用:
-
可以在 TIME_WAIT 期间重新绑定该端口,这对服务器崩溃重启并快速恢复有至关重要的作用,避免了几十秒甚至几分钟的等待。
需要注意的是,如果没有开启时间戳选项(默认开启),则 TIME_WAIT 期间重新绑定可能失败,参见TIME_WAIT探究。
-
允许在已连接的状态下重新绑定该端口 ,常见情景如下:
a)启动监听进程
b)连接请求到达,派生一个子进程处理该连接
c)监听进程崩溃,子进程仍在运行
d)重启监听进程,并绑定之前的端口 -
允许在同一个端口启动多个服务器,前提是每个服务器绑定的本地 IP 不同(IP 别名)
笔者在本机上实验,分别绑定环回地址
127.0.0.1
和本机 IP 地址,两者可以同时绑定同一个端口。如果未开启 SO_REUSEADDR,则无法同时绑定。
注意,如果某个服务器程序绑定的是通配地址(INADDR_ANY),那么后续在同一端口上启动的服务器程序则无法完成绑定。 -
其他作用不常见,详见《UNP》P166
另外注意,不论是否开启该选项,不同传输层协议是可以同时绑定到同一端口的,比如 TCP 和 UDP 程序就能够同时绑定到一个端口。
对于所有 TCP 服务器,都应该在 bind 前开启 SO_REUSEADDR !
另外,还有一个
SO_REUSEPORT
选项,不常用,参见《UNP》P165
SO_LINGER
SO_LINGER
用来控制 close 的行为:
注意缓冲区的各种情况! 其中第四种关闭方式就是我们常说的“优雅关闭”。
书中提供了一种有效办法,使发送端能够确认接收端的应用层已收到数据,详见《UPN》P161。
另外有个细节,上图中 SHUT_RD 后,接收到的任何数据都会被丢弃,这里的丢弃是先确认再丢弃,也就是说,这不会引发对方的重传。
TCP_NODELAY
此选项用来禁止 Nagle 算法。关于 Nagle 算法,参见本博客另一篇文章——TCP流量控制 。 有时候我们必须要关闭 Nagle 算法,特别是在一些对时延要求较高的交互式操作环境中,所有的小分组必须尽快发送出去。