从例子开始

  1. static int count;
  2. static void sig_int(int signo) {
  3. printf("\nreceived %d datagrams\n", count);
  4. exit(0);
  5. }
  6. int main(int argc, char **argv) {
  7. int listenfd;
  8. listenfd = socket(AF_INET, SOCK_STREAM, 0);
  9. struct sockaddr_in server_addr;
  10. bzero(&server_addr, sizeof(server_addr));
  11. server_addr.sin_family = AF_INET;
  12. server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  13. server_addr.sin_port = htons(SERV_PORT);
  14. int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
  15. if (rt1 < 0) {
  16. error(1, errno, "bind failed ");
  17. }
  18. int rt2 = listen(listenfd, LISTENQ);
  19. if (rt2 < 0) {
  20. error(1, errno, "listen failed ");
  21. }
  22. signal(SIGPIPE, SIG_IGN);
  23. int connfd;
  24. struct sockaddr_in client_addr;
  25. socklen_t client_len = sizeof(client_addr);
  26. if ((connfd = accept(listenfd, (struct sockaddr *) &client_addr, &client_len)) < 0) {
  27. error(1, errno, "bind failed ");
  28. }
  29. char message[MAXLINE];
  30. count = 0;
  31. for (;;) {
  32. int n = read(connfd, message, MAXLINE);
  33. if (n < 0) {
  34. error(1, errno, "error read");
  35. } else if (n == 0) {
  36. error(1, 0, "client closed \n");
  37. }
  38. message[n] = 0;
  39. printf("received %d bytes: %s\n", n, message);
  40. count++;
  41. }
  42. }

启动服务端程序后, 使用 Telnet 客户端发送一些字符, 结束后服务端程序正常退出. 之后服务端程序可以很快重启:

 $./addressused 
 received 9 bytes: network
 received 6 bytes: good
 client closed
 $./addressused

改变一下连接的关闭顺序:

先启动服务器,再使用 Telnet 作为客户端登录到服务器,在屏幕上输入一些字符。注意接下来的不同,我不会在 Telnet 端关闭连接,而是直接使用 Ctrl+C 的方式在服务器端关闭连接。

$telneet 127.0.0.1 9527
network
bad
Connection closed by foreign host.

连接已经被关闭,Telnet 客户端也感知连接关闭并退出了。接下来,我们尝试重启服务器端程序。你会发现,这个时候服务端程序重启失败,报错信息为:bind failed: Address already in use。

 $./addressused 
 received 9 bytes: network
 received 6 bytes: good
 client closed
 $./addressused
 bind faied: Address already in use(98)

复习 TIME_WAIT

image.png

通过服务器端发起的关闭连接操作,引起了一个已有的 TCP 连接处于 TME_WAIT 状态,正是这个 TIME_WAIT 的连接,使得服务器重启时,继续绑定在 127.0.0.1 地址和 9527 端口上的操作,返回了 Address already in use 的错误。

重用套接字选项

即使在很小的概率下,客户端 Telnet 使用了相同的端口,从而造成了新连接和旧连接的四元组相同,在现代 Linux 操作系统下,也不会有什么大的问题.

现代 Linux 操作系统对此进行了一些优化:

  • 第一种优化是新连接 SYN 告知的初始序列号,一定比 TIME_WAIT 老连接的末序列号大,这样通过序列号就可以区别出新老连接。
  • 第二种优化是开启了 tcp_timestamps,使得新连接的时间戳比老连接的时间戳大,这样通过时间戳也可以区别出新老连接。

在这样的优化之下,一个 TIME_WAIT 的 TCP 连接可以忽略掉旧连接,重新被新的连接所使用。这就是重用套接字选项,通过给套接字配置可重用属性,告诉操作系统内核,这样的 TCP 连接完全可以复用 TIME_WAIT 状态的连接。

int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

SO_REUSEADDR 套接字选项,允许启动绑定在一个端口,即使之前存在一个和该端口一样的连接。前面的例子已经表明,在默认情况下,服务器端历经创建 socket、bind 和 listen 重启时,如果试图绑定到一个现有连接上的端口,bind 操作会失败,但是如果我们在创建 socket 和 bind 之间,使用上面的代码片段设置 SO_REUSEADDR 套接字选项,情况就会不同。

调用 setsockopt 方法:

  • 11~12行
int main(int argc, char **argv) {
    int listenfd;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(SERV_PORT);

    int on = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
    if (rt1 < 0) {
        error(1, errno, "bind failed ");
    }

    int rt2 = listen(listenfd, LISTENQ);
    if (rt2 < 0) {
        error(1, errno, "listen failed ");
    }

    signal(SIGPIPE, SIG_IGN);

    int connfd;
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);

    if ((connfd = accept(listenfd, (struct sockaddr *) &client_addr, &client_len)) < 0) {
        error(1, errno, "bind failed ");
    }

    char message[MAXLINE];
    count = 0;

    for (;;) {
        int n = read(connfd, message, MAXLINE);
        if (n < 0) {
            error(1, errno, "error read");
        } else if (n == 0) {
            error(1, 0, "client closed \n");
        }
        message[n] = 0;
        printf("received %d bytes: %s\n", n, message);
        count++;
    }
}

SO_REUSEADDR 套接字选项还有一个作用,那就是本机服务器如果有多个地址,可以在不同地址上使用相同的端口提供服务。

最佳实践

服务器端程序,都应该设置 SO_REUSEADDR 套接字选项,以便服务端程序可以在极短时间内复用同一个端口启动。

tcp_tw_reuse 的内核配置选项与 SO_REUSEADDR 没有关系.

  • tcp_tw_reuse 是内核选项,主要用在连接的发起方。TIME_WAIT 状态的连接创建时间超过 1 秒后,新的连接才可以被复用,注意,这里是连接的发起方;
  • SO_REUSEADDR 是用户态的选项,SO_REUSEADDR 选项用来告诉操作系统内核,如果端口已被占用,但是 TCP 连接状态位于 TIME_WAIT ,可以重用端口。如果端口忙,而 TCP 处于其他状态,重用端口时依旧得到“Address already in use”的错误信息。注意,这里一般都是连接的服务方。

精选留言

刘晓林

关于tcp_tw_reuse和SO_REUSEADDR的区别,可以概括为:tcp_tw_reuse是为了缩短time_wait的时间,避免出现大量的time_wait链接而占用系统资源,解决的是accept后的问题;SO_REUSEADDR是为了解决time_wait状态带来的端口占用问题,以及支持同一个port对应多个ip,解决的是bind时的问题。

作者回复: 总结得不错。