运行环境:Ubuntu 16.0.4

  • 无论是否开启 SO_REUSEADDR ,客户端主动断开连接后,无需等待 TIME_WAIT 即可重连,因为客户端每次 bind 的端口都不一样。由此可见, TIME_WAIT 是对端口而言

    不管是服务器还是客户端,只要是主动断开连接的,都会有 TIME_WAIT 。

  • 如不开启 SO_REUSEADDR,服务器端主动断开连接,则必须等待 TIME_WAIT 后才可重新 bind 该端口,原因见下文。

  • 注意,必须是要在 accept 或 connect 成功返回后(即连接成功后)断连或终止程序,才会有 TIME_WAIT ;仅仅 bind 但未连接成功,终止程序后是不会 TIME_WAIT 的

  • 注意,如果 bind 指定端口不成功,则会自动 bind 其他任意端口;

  • SO_REUSEADDR 生效的前提条件是开启时间戳,即令 /proc/sys/net/ipv4/tcp_timestamps 为 1(默认也为1)。

    实操发现即使关闭时间戳,服务器也能立刻重新绑定端口,原因尚不明确。

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
//server
int main()
{
int pid = getpid();
printf("server pid:%d\n",pid);
int sock_server = socket(AF_INET, SOCK_STREAM , IPPROTO_TCP);
int opt = 0; //SO_REUSEADDR默认也是0
socklen_t optlen = sizeof(opt);
setsockopt(sock_server, SOL_SOCKET, SO_REUSEADDR, &opt, optlen);
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(12345);
server_addr.sin_family = AF_INET;
if(-1 == bind(sock_server, (struct sockaddr*)&server_addr, sizeof(server_addr))){
printf("server failed to bind\n");
};
listen(sock_server, 20);
char buf[100] = {0};
int sock_clnt;
if(-1 != (sock_clnt = accept(sock_server,NULL,NULL))){
ssize_t size = read(sock_clnt, buf, sizeof(buf));
printf("server received %ld bytes\n",size);
size = write(sock_clnt, buf, sizeof(buf));
printf("server sent %ld bytes\n=====================",size);
}
//sleep(3); //stop 3s so that client could send FIN first
close(sock_server);
close(sock_clnt);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//client
int main()
{
int pid = getpid();
printf("client pid:%d\n",pid);
int sock_clnt = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(12345);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
connect(sock_clnt, (struct sockaddr*)&server_addr, sizeof(server_addr));
char in_buf[100];
char out_buf[100];
for(int i=0;i<99;i++)
out_buf[i]='f';
ssize_t size = write(sock_clnt, out_buf, sizeof(out_buf));
printf("client sent %ld bytes\n",size);
size = read(sock_clnt, in_buf, sizeof(in_buf));
printf("client received %ld bytes\n",size);
sleep(3); //stop 3s so that server could send FIN first
close(sock_clnt);
}

直接编译上述代码,先运行 server 端,再运行 client 端(注意,最好运行可执行文件,不要直接在编译器中运行),结果如下:
server
client
由于客户端 close 前调用了 sleep,所以可以判断一定是 server 主动断开连接,如下图:

localhost:12345 是本端地址信息,localhost:53188 是对端地址信息,后面的状态是描述本端状态的,因此可知 server 主动发送 FIN 并断开连接后,进入了 TIME_WAIT 状态。此时我们马上重启服务器,则输出以下内容:

在 TIME_WAIT 内重启服务器,则报错绑定失败。注意,绑定指定端口失败后,会随机绑定其他端口,如下:

第三行的 46937 便是服务器端随机绑定的端口。不过这已经失去意义,因为服务器是通过知名端口来被客户端认识的,客户端根本不认识这些随机端口,所以 server 会一直处于监听状态,不会有 client 来连接。因此,对于服务端,如果 bind 失败,应该直接终止程序或等待 TIME_WAIT 后重新 bind。

下面我们让 client 主动关闭连接——加上 server 的 sleep ,去掉 client 的 sleep 即可。结果如下:
server
client

本端为 localhost:53200 ,可见确实是 client 发起的 FIN 。因为 client 无需 bind 特定端口,即 client 每次运行绑定的端口都不同,所以不用担心会因为 TIME_WAIT 而连接失败。

接下来我们使用 SO_REUSEADDR 选项来修改 TIME_WAIT 的限制——将 server 代码第 7 行的 opt 赋值为 1 即可,这样就可以无视 TIME_WAIT 直接复用端口。下面是连续两次运行 server 和 client 的结果:

你看,即使服务器处于处于 TIME_WAIT 状态,还是可以直接绑定端口并进行通信。

注意,即使将 SO_REUSEADDR 置 1,TIME_WAIT 状态也依旧存在,只是可以无视该状态直接 bind 而已。

待补充

https://xiaolincoding.com/network/3_tcp/tcp_tw_reuse_close.html#为什么要设计-time-wait-状态

https://xiaolincoding.com/network/3_tcp/time_wait_recv_syn.html#先说结论