本文实践环境:
Operating System: macOS 12.6
Kernel: Darwin Kernel Version 21.6.0
Architecture: ARM64
其他环境可能有些许差异,比如 socket 相关的宏定义等。
在之前的《Socket 与 TCP 四次挥手(下)》一文中,笔者对 Socket 选项进行了初步的探讨和总结,并针对 setsockopt
函数做了较多的概述。但前文的重点还是站在四次挥手这个特例出发,故本文就从 Socket 选项的全局视角来做一个梳理。
再谈 Socket 可选项
Socket 特性很多,并且可以通过相关选项来改变。我们先来复习一下之前文章中的内容:
➜ vifile > man 2 setsockopt
...
...
NAME
getsockopt, setsockopt - get and set options on sockets
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#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);
DESCRIPTION
getsockopt() and setsockopt() manipulate options for the socket referred to by the file descriptor sockfd. Options may exist at multiple protocol levels; they are always present at the uppermost socket level.
When manipulating socket options, the level at which the option resides and the name of the option must be specified. To manipulate options at the sockets API level, level is specified as SOL_SOCKET. To manipulate options at any other level the protocol number of the appropriate protocol controlling the option is supplied. For example, to indicate that an option is to be interpreted by the TCP protocol, level should be set to the protocol number of TCP; see getprotoent(3).
The arguments optval and optlen are used to access option values for setsockopt(). For getsockopt() they identify a buffer in which the value for the requested option(s) are to be returned. For getsockopt(), optlen is a value-result argument, initially containing the size of the buffer pointed to by optval, and modified on return to indicate the actual size of the value returned. If no option value is to be supplied or returned, optval may be NULL.
Optname and any specified options are passed uninterpreted to the appropriate protocol module for interpretation. The include file <sys/socket.h> contains definitions for socket level options, described below. Options at other protocol levels vary in format and name; consult the appropriate entries in section 4 of the manual.
Most socket-level options utilize an int argument for optval. For setsockopt(), the argument should be nonzero to enable a boolean option, or zero if the option is to be disabled.
For a description of the available socket options see socket(7) and the appropriate protocol man pages.
RETURN VALUE
On success, zero is returned for the standard options. On error, -1 is returned, and errno is set appropriately.
Netfilter allows the programmer to define custom socket options with associated handlers; for such options, the return value on success is the value returned by the handler.
...
...
setsockopt()
函数:
int setsockopt(int socket, int level, int option_name,
const void *option_value, socklen_t option_len);
1)可以对某个 Socket 进行设置,其中 socket
参数表示某个 Socket 的文件描述符, option_value
和 option_len
代表要修改的值及其长度,level
和 option_name
则对应着在哪个级别的哪类选项上进行设置;
2)level
:如果是对 Socket 级别的属性进行设置,那么这个值为:SOL_SOCKET
,如果是其他级别,比如 TCP ,则设置为 TCP 的协议号:IPPROTO_TCP
;
// sys/socket.h
/*
* Level number for (get/set)sockopt() to apply to socket itself.
*/
#define SOL_SOCKET 0xffff /* options for socket level */
// ...
// netinet/in.h
/*
* Protocols (RFC 1700)
*/
#define IPPROTO_IP 0 /* dummy for IP */
// ...
#define IPPROTO_TCP 6 /* tcp */
// ...
3)option_name
:选项名称,比如保持长连接的 SO_KEEPALIVE
:
// sys/socket.h
/*
* Option flags per-socket.
*/
#define SO_DEBUG 0x0001 /* turn on debugging info recording */
#define SO_ACCEPTCONN 0x0002 /* socket has had listen() */
#define SO_REUSEADDR 0x0004 /* allow local address reuse */
#define SO_KEEPALIVE 0x0008 /* keep connections alive */
#define SO_DONTROUTE 0x0010 /* just use interface addresses */
#define SO_BROADCAST 0x0020 /* permit sending of broadcast msgs */
#if !defined(_POSIX_C_SOURCE) || defined(_DARWIN_C_SOURCE)
#define SO_USELOOPBACK 0x0040 /* bypass hardware when possible */
#define SO_LINGER 0x0080 /* linger on close if data present (in ticks) */
#else
#define SO_LINGER 0x1080 /* linger on close if data present (in seconds) */
#endif /* (!_POSIX_C_SOURCE || _DARWIN_C_SOURCE) */
#define SO_OOBINLINE 0x0100 /* leave received OOB data in line */
// ...
与 setsockopt
对应的是 getsockopt()
函数,由于二者的参数含义基本无差别,笔者就不赘述了。
int getsockopt(int sockfd, int level, int optname,
void *optval, socklen_t *optlen);
将相关选项整理成表格:
需要说明的是,这里只罗列了部分选项,更多的还需要去查看对应的头文件定义或相关文档。当然,我们也没有必要刻意去记这些,但我们脑中要有一个全局的轮廓。
下面来看一个 getsockopt
程序示例(来自《TCP/IP 网络编程》,笔者对其做了部分修改,完整代码已上传 GitHub):
// sock_type.c
#include <stdio.h>
#include <sys/socket.h>
#include "common/log.h"
void check_socket_type(int socket_type_expect, int socket_fd);
int main(int argc, char *argv[]) {
int tcp_socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_socket_fd < 0) {
error_handling("create tcp socket failed");
}
int udp_socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_socket_fd < 0) {
error_handling("create udp socket failed");
}
check_socket_type(SOCK_STREAM, tcp_socket_fd);
check_socket_type(SOCK_DGRAM, udp_socket_fd);
return 0;
}
void check_socket_type(int socket_type_expect, int socket_fd) {
int socket_type;
socklen_t socket_type_len = sizeof(socket_type);
int state = getsockopt(socket_fd, SOL_SOCKET, SO_TYPE, &socket_type, &socket_type_len);
if (state < 0) {
error_handling("get socket type failed");
}
if (socket_type != socket_type_expect) {
printf("get socket type incorrect: %d, expected: %d\n", socket_type, socket_type_expect);
}
}
程序运行正常,没有出现错误,表示 getsockopt
拿到了正确的结果。需要补充的是,通过前面的表格,我们知道 SO_TYPE
是不能被用在 setsockopt
之中的,这是什么意思?
Socket 类型只能在创建时决定,以后不能更改。
如果强行修改呢?笔者运行后,输出的错误信息是:Protocol not available
。
常见可选项
在之前的各种程序示例中,笔者基本上用的都是 Socket 默认选项值。但在某些场景下,我们是需要通过修改选项参数来满足对应需求的。下面将介绍几个常见的 Socket 可选项。
I/O 缓冲区:SO_SNDBUF/SO_RCVBUF
通过 Socket 发送或接收数据时,数据会先存入内核的发送或接收缓冲区,故缓冲区的大小决定了应用程序的延迟和性能。

下面的程序示例展示了对应选项的设置和查看:
// sock_buf.c
#include <stdio.h>
#include <sys/socket.h>
#include "common/log.h"
void get_socket_buf(const char *socket_type, int socket_fd);
void set_socket_buf(const char *socket_type, int socket_fd, int rcv_size, int send_size);
void test_tcp_socket_buf();
void test_udp_socket_buf();
int main(int argc, char *argv[]) {
test_tcp_socket_buf();
fprintf(stdout, "--------------------\n");
test_udp_socket_buf();
return 0;
}
void test_tcp_socket_buf() {
int tcp_socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_socket_fd < 0) {
error_handling("create tcp socket failed");
}
get_socket_buf("tcp", tcp_socket_fd);
set_socket_buf("tcp", tcp_socket_fd, 4096, 4096);
get_socket_buf("tcp", tcp_socket_fd);
set_socket_buf("tcp", tcp_socket_fd, 409600, 409600);
get_socket_buf("tcp", tcp_socket_fd);
set_socket_buf("tcp", tcp_socket_fd, 40960000, 40960000);
get_socket_buf("tcp", tcp_socket_fd);
}
void test_udp_socket_buf() {
int udp_socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_socket_fd < 0) {
error_handling("create udp socket failed");
}
get_socket_buf("udp", udp_socket_fd);
set_socket_buf("udp", udp_socket_fd, 4096, 4096);
get_socket_buf("udp", udp_socket_fd);
set_socket_buf("udp", udp_socket_fd, 409600, 409600);
get_socket_buf("udp", udp_socket_fd);
set_socket_buf("udp", udp_socket_fd, 40960000, 40960000);
get_socket_buf("udp", udp_socket_fd);
}
void get_socket_buf(const char *socket_type, int socket_fd) {
int buf;
socklen_t buf_len = sizeof(buf);
int state = getsockopt(socket_fd, SOL_SOCKET, SO_RCVBUF, &buf, &buf_len);
if (state < 0) {
error_handling("get socket receive buffer failed");
}
fprintf(stdout, "%s receive buf: %d\n", socket_type, buf);
state = getsockopt(socket_fd, SOL_SOCKET, SO_SNDBUF, &buf, &buf_len);
if (state < 0) {
error_handling("get socket send buffer failed");
}
fprintf(stdout, "%s send buf: %d\n", socket_type, buf);
fflush(stdout);
}
void set_socket_buf(const char *socket_type, int socket_fd, int rcv_size, int send_size) {
int state = setsockopt(socket_fd, SOL_SOCKET, SO_RCVBUF, &rcv_size, sizeof(rcv_size));
if (state < 0) {
fprintf(stderr, "set %s socket receive buffer: %d failed\n", socket_type, rcv_size);
}
state = setsockopt(socket_fd, SOL_SOCKET, SO_SNDBUF, &send_size, sizeof(send_size));
if (state < 0) {
fprintf(stderr, "set %s socket send buffer: %d failed\n", socket_type, send_size);
}
}
输出结果[1]:
tcp receive buf: 131072
tcp send buf: 131072
tcp receive buf: 4096
tcp send buf: 4096
tcp receive buf: 409600
tcp send buf: 409600
tcp receive buf: 409600
tcp send buf: 409600
--------------------
udp receive buf: 786896
udp send buf: 9216
udp receive buf: 4096
udp send buf: 4096
udp receive buf: 409600
udp send buf: 409600
udp receive buf: 409600
udp send buf: 409600
set tcp socket receive buffer: 40960000 failed
set tcp socket send buffer: 40960000 failed
set udp socket receive buffer: 40960000 failed
set udp socket send buffer: 40960000 failed
从输出结果可以看到,默认情况下本机 TCP 发送/接收缓冲区大小都是 128 KiB,我们可以修改这两个值,但也不能无限修改,它有上限。
Address already in use:SO_REUSEADDR
在讲 TCP 四次挥手时,我们称 TIME_WAIT 状态为魔鬼。该状态出现在主动关闭方,其存在的目的是为了保证被关闭方可以正常退出。
之前我们的很多程序示例都是客户端主动发起关闭,如果是服务端先发起关闭呢[2]?
很显然,TIME_WAIT 状态会出现在服务端,但这会带来什么问题呢?我们先看一个程序示例:
// sock_reuseaddr.c
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <mach/boolean.h>
#include "common/log.h"
int main(int argc, char **argv) {
if (argc < 3) {
error_handling("Usage: sock_reuseaddr <Port> <ReuseAddrFlag>");
}
struct sockaddr_in client_addr, server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[1]));
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) {
error_handling("create socket error");
}
// 设置 SO_REUSEADDR
int reuse = atoi(argv[2]);
if (reuse) {
int option = TRUE;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option));
}
if (bind(listen_fd, (struct sockaddr *) &server_addr, sizeof(server_addr)) == -1) {
error_handling("bind error");
}
if (listen(listen_fd, 1024) == -1) {
error_handling("listen error");
}
socklen_t client_len;
client_len = sizeof(client_addr);
int client_fd = accept(listen_fd, (struct sockaddr *) &client_addr, &client_len);
char msg[512];
ssize_t read_len;
for (;;) {
bzero(msg, sizeof(msg));
read_len = read(client_fd, msg, sizeof(msg));
if (read_len == 0) {
printf("client socket closed.\n");
break;
} else if (read_len < 0) {
error_handling("read client msg failed");
}
msg[read_len] = '\0';
printf("%s", msg);
}
close(client_fd);
close(listen_fd);
return 0;
}
我们要验证的问题是:关闭服务端后,能否立刻重新启动?
先看默认值的情况:
# 启动 sock_reuseaddr 服务端
➜ build git:(main) ✗ >./bin/sock_reuseaddr 12399 0
aaa
^C
➜ build git:(main) ✗ >./bin/sock_reuseaddr 12399 0
bind error: Address already in use
# 客户端通过 telnet 输入数据(192.168.31.50 是本机 IP)
➜ build git:(main) ✗ > telnet 192.168.31.50 12399
Trying 192.168.31.50...
Connected to demons-mbp.
Escape character is '^]'.
aaa
Connection closed by foreign host.
➜ build git:(main) ✗ >
可以看到,服务端按下 Ctrl-C
后,进程关闭,客户端断连:Connection closed by foreign host
。
然后立刻重启服务端程序,结果在调用 bind()
时出错:Address already in use
。之所以出现这个原因,是因为端口还未被释放,我们通过 netstat
命令可以查看当前的状态:
➜ build git:(main) ✗ > netstat -a -finet|grep 12399
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 demons-mbp.12399 demons-mbp.52226 TIME_WAIT
➜ build git:(main) ✗ >
服务端的 Socket 连接还处在 TIME_WAIT 状态没有释放对应的端口。
如何解决这类问题呢?服务不能停了之后,等待 2MSL 才能重新使用吧,这对客户端用户来说是不可接受的。
SO_REUSEADDR
选项就是用来解决这个问题的。通过设置该选项,当端口被 TIME_WAIT 状态的连接占用后,可以立刻重新启动对应的服务:
# 启动 sock_reuseaddr 服务端
➜ build git:(main) ✗ > ./bin/sock_reuseaddr 12399 1
aaa
^C
➜ build git:(main) ✗ > ./bin/sock_reuseaddr 12399 1
aaa
^C
➜ build git:(main) ✗ >
# 客户端通过 telnet 输入数据(192.168.31.50 是本机 IP)
➜ build git:(main) ✗ > telnet 192.168.31.50 12399
Trying 192.168.31.50...
Connected to demons-mbp.
Escape character is '^]'.
aaa
Connection closed by foreign host.
➜ build git:(main) ✗ >
➜ build git:(main) ✗ > telnet 192.168.31.50 12399
Trying 192.168.31.50...
Connected to demons-mbp.
Escape character is '^]'.
aaa
Connection closed by foreign host.
➜ build git:(main) ✗ >
可以看到,按下 Ctrl-C
停止服务后,立刻重启没有报错,当我们再次终止服务端程序后,查询连接状态:
➜ build git:(main) ✗ > netstat -a -finet|grep 12399
tcp4 0 0 demons-mbp.12399 demons-mbp.52631 TIME_WAIT
tcp4 0 0 demons-mbp.12399 demons-mbp.52636 TIME_WAIT
➜ build git:(main) ✗ >
服务端两个连接的状态还是 TIME_WAIT,这也说明 bind()
函数内部执行了新的逻辑,不对该状态进行限制。那么,我们平常开发服务端程序时,是否要设置 SO_REUSEADDR
选项呢?《网络编程实战》课中有这样一段话:
这里的最佳实践可以总结成一句话: 服务器端程序,都应该设置 SO_REUSEADDR 套接字选项,以便服务端程序可以在极短时间内复用同一个端口启动。
另外, SO_REUSEADDR
选项还有一个作用,如果本机服务器有多个网卡,即有多个 IP 地址,那么设置该选项后,可以在不同的地址上使用相同的端口提供服务。
比如,当前机器有 172.21.110.41,172.21.16.9 两个地址,那么它们都可以使用 12399 端口提供服务,另外再通过 INADDR_ANY + 12399
启动一个默认服务。目的地是 172.21.110.41 + 12399
的请求发往第一个服务,目的地是 172.21.16.9 + 12399
的请求发往第二个服务,其他发往 12399 端口的请求则会路由到默认服务。
笔者在上述代码的基础上,调整了一下函数入参,支持多 IP 启动,完整代码见 GitHub,下面是同时启动多个实例的截图:
Nagle 算法:TCP_NODELAY
什么是 Nagle 算法?它的作用是什么?带着这两个问题,我们一起来了解一下 Nagle 算法。
该算法诞生于 1984 年,使用在 TCP 层,其目的也比较简单:为防止连续发送过多数据包而导致网络拥堵,便限制了大批量的小数据连续发送,只有等到前一个数据包的 ACK 回来之后(即在任何时刻,未被确认的数据包不能超过一个),才继续发送后续的包。正是在等 ACK 的过程中,后面的数据可以累积在发送端的缓冲区,后续便可以一次性发送一个较大的数据包。
这里的小数据包指的是报文长度小于 MSS 的 TCP 分组。
下图来自《TCP/IP 网络编程》,笔者对其进行了重新绘制:
如果单纯从上面的图来看,我们可能会认为 Nagle 算法还是打开的好,因为可以充分利用缓冲区,提高带宽的利用率。但这也不是绝对的,想象一下网络稳定的情况下,传输大文件数据会是什么情况?
由于传输的数据比较大,所以不使用 Nagle 算法也能快速填满发送缓冲区,而且不用等前面包的 ACK 就能快速连续发送数据,因而传输效率要高效得多。
需要指出的是:除非我们有特别的把握,否则不要轻易修改默认的 TCP Nagle 算法。因为现代操作系统对 Nagle 算法和 ACK 延迟确认[3]做了很多优化,擅自修改默认值可能会起反作用。
如果有特别需求,想修改 Nagle 算法的默认值,可以使用 TCP_NODELAY
选项:
// sock_tcp_nodelay.c
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include "common/log.h"
void get_tcp_nodelay(int socket_fd);
int main(int argc, char *argv[]) {
int tcp_socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_socket_fd < 0) {
error_handling("create tcp socket failed");
}
get_tcp_nodelay(tcp_socket_fd);
int off = 1;
int state = setsockopt(tcp_socket_fd, IPPROTO_TCP, TCP_NODELAY, &off, sizeof(off));
if (state < 0) {
error_handling("set TCP_NODELAY off failed");
}
get_tcp_nodelay(tcp_socket_fd);
return 0;
}
void get_tcp_nodelay(int socket_fd) {
int nagle_flag;
socklen_t len = sizeof(nagle_flag);
int state = getsockopt(socket_fd, IPPROTO_TCP, TCP_NODELAY, &nagle_flag, &len);
if (state < 0) {
error_handling("get TCP_NODELAY flag failed");
}
printf("get TCP_NODELAY: %d\n", nagle_flag);
}
输出结果为:
get TCP_NODELAY: 0
get TCP_NODELAY: 4[4]
到这里,笔者介绍了几个常见的 Socket 选项,从前面的表格也能看到,这样的选项非常多。所以,只有到具体的业务场景中,我们才能针对某个特性做进一步的了解。比如当有紧急数据需要传输时,则需要对 SO_OOBINLINE
选项进行深入研究:
OOB
是带外数据(out-of-band data)的缩写,它指的是优先级更高的数据(TCP 中称为紧急数据),会优先安排传输,即使传输队列中已经有了普通数据。TCP 支持带外数据,UDP 不支持。
最后,笔者想说的是:虽说约定大于配置,但对于常用的配置,我们还是有必要做到心中有数。
题图: 公牛 GN-R322U(来源:jd.com)
这里也测试了 UDP 协议的缓冲区,有一种观点认为 UDP 没有发送缓冲区,因为发往缓冲区的数据,当其大小超过一定阈值(很小)后就会立刻发往对方。 ↩︎
互联网应用一般以服务端主动关闭连接的居多,因为服务端要保证连接数,节约资源,服务更多的用户。 ↩︎
网络通信中接收端的优化手段。接收端收到数据后不立刻回复 ACK,而是累积一段时间的 ACK 报文,再统一发送给对端(或接收端有数据要发给对端,顺便把 ACK 确认报文一起捎带过去)。ACK 延迟确认可以减少网络开销,但也不能一直延迟,否则发送端会误以为是丢包。 ↩︎
https://stackoverflow.com/questions/31834030/reading-tcp-nodelay-using-getsockopt-returning-a-weird-value ↩︎