获取和设置socket选项

使用 getsockopt()setsockopt 来获取和设置 socket 的各种选项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int getsockopt(int socket, 
int level,
int option_name,
void *restrict option_value,
socklen_t *restrict option_len);
//example
int bufsize = 10000;
socklen_t optlen = sizeof(bufsize);
getsockopt(sock_listen, SOL_SOCKET, SO_RCVBUF, &bufsize, &optlen);
//============================================================================
int setsockopt (int socket,
int level,
int option_name,
const void *option_val,
socklen_t option_len);
//example
int bufsize = 10000;
setsockopt(sock_listen, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));
  • 以上两个函数,成功时均返回 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//server

int main() {
int sock_listen = socket(AF_INET, SOCK_STREAM, 0);
int recv_size = 3000;
socklen_t optlen = sizeof(recv_size);
setsockopt(sock_listen, SOL_SOCKET, SO_RCVBUF, (void*)&recv_size, optlen);
getsockopt(sock_listen, SOL_SOCKET, SO_RCVBUF,(void*)&recv_size,&optlen);
printf("server recv buffer size: %d\n",recv_size);
struct sockaddr_in addr_listen;
bzero(&addr_listen, sizeof(addr_listen));
addr_listen.sin_addr.s_addr = inet_addr("192.168.248.128");
addr_listen.sin_port = htons(12345);
addr_listen.sin_family = AF_INET;
Bind(sock_listen, (struct sockaddr *) &addr_listen, sizeof(addr_listen));
Listen(sock_listen, 20);
int sock_conn = Accept(sock_listen, NULL, NULL);
while (1); //不read,不close
}

//client
int main()
{
int sock_clnt;
struct sockaddr_in addr_serv;
sock_clnt = socket(AF_INET, SOCK_STREAM, 0);
memset(&addr_serv, 0, sizeof(addr_serv));
addr_serv.sin_family = AF_INET;
addr_serv.sin_port = htons(12345);
addr_serv.sin_addr.s_addr = inet_addr("192.168.248.128");
Connect(sock_clnt, (struct sockaddr*)&addr_serv, sizeof(addr_serv));
char buf[100000] = {0};
writen(sock_clnt, buf, sizeof(buf));
while(1);
}

分别运行运行 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。接收窗口会随着网络状况而不断调整,但肯定不会超过缓冲区的大小

在本机上实验多次发现,接收窗口始终不会超过接收缓冲区的一半大小

还有以下几点需要注意:

  1. 上面说过,这两个选项是由监听套接字继承而来,所以对于 server,必须在 listen 前设置选项;对于 client,必须在 connect 前设置选项 。对已连接套接字(在 accept 返回之后)设置选项没有任何作用,因为 accept 只是从已连接队列中取出一个连接而已(参见深入理解socket基本函数)。
  2. 为了避免潜在的缓冲区空间浪费,接收缓冲区大小应该为 MSS 的整数倍,且至少为 MSS 的 4 倍,参见《UNP》P163。
  3. 为了最大化性能,缓冲区大小应该约等于带宽-延迟积 ,参见《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

这是服务器最常用的选项之一。该选项有以下几个作用:

  1. 可以在 TIME_WAIT 期间重新绑定该端口,这对服务器崩溃重启并快速恢复有至关重要的作用,避免了几十秒甚至几分钟的等待。

    需要注意的是,如果没有开启时间戳选项(默认开启),则 TIME_WAIT 期间重新绑定可能失败,参见TIME_WAIT探究

  2. 允许在已连接的状态下重新绑定该端口 ,常见情景如下:
    a)启动监听进程
    b)连接请求到达,派生一个子进程处理该连接
    c)监听进程崩溃,子进程仍在运行
    d)重启监听进程,并绑定之前的端口

  3. 允许在同一个端口启动多个服务器,前提是每个服务器绑定的本地 IP 不同(IP 别名)

    笔者在本机上实验,分别绑定环回地址 127.0.0.1 和本机 IP 地址,两者可以同时绑定同一个端口。如果未开启 SO_REUSEADDR,则无法同时绑定。
    注意,如果某个服务器程序绑定的是通配地址(INADDR_ANY),那么后续在同一端口上启动的服务器程序则无法完成绑定。

  4. 其他作用不常见,详见《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 算法,特别是在一些对时延要求较高的交互式操作环境中,所有的小分组必须尽快发送出去。