当前位置:   article > 正文

【网络编程】TCP 连接的四种 WAIT 状态_tcp wait

tcp wait

总述

        文章以【网络编程】TCP socket 单机通信实验 为基础。对 TCP 连接中的四种 WAIT 状态进行分析实验。从服务端开始监听到建立连接、收发数据,到最后连接完全关闭,TCP 一共会经历 11 种状态,总体状态变化图如下(假设由 client 主动关闭连接,server 被动关闭),其中有四种 "WAIT" 状态 —— FIN_WAIT_1、FIN_WAIT_2、TIME_WAIT、CLOSE_WAIT —— 容易混淆,而且有很多 “诡异” 的问题也于此有关:

 图片来源:自己拍的《TCP/IP 详解 卷一:协议》 第 441 页

FIN_WAIT_1 和 FIN_WAIT_2

        client 应用层调用 close 接口,主动发送 FIN 包(第一次挥手),进入 FIN_WAIT_1 状态。待 FIN 包发送到 server,server 返回的 ACK 到达后(第二次挥手),client 进入 FIN_WAIT_2 状态。正常情况下,FIN_WAIT_1 是一个非常短暂的状态。FIN_WAIT_2 的时间长度则取决于 server 什么时候进行第三次挥手。

CLOSE_WAIT 和 LAST_ACK

        server 在收到 FIN(第一次挥手)后马上答复 ACK(第二次挥手),自身的状态也变化为 CLOSE_WAIT。等到 server 的应用层主动调用 close 接口,发送 FIN 包给 client(第三次挥手),server 结束 CLOSE_WAIT 状态,转换为 LAST_ACK 状态。待 server 发送的 FIN 包到达 client,client 返回 ACK,server 的状态变为 CLOSED,连接结束。和 FIN_WAIT_1 一样,LAST_ACK 的存在时间很短。CLOSE_WAIT 持续时间的长短则取决于 server 的应用层:

        CLOSE_WAIT 可能几乎不存在。【网络编程】TCP socket 单机通信实验 中因为 client 和 sever 几乎同时发完数据、同时调用 close 接口,第二次挥手和第三次挥手合并在一个数据包里发送(这里是 server 先调用 close):

         CLOSE_WAIT 也可能相对长时间地存在。如果完成前两次挥手后,server 迟迟不调用 close(可能是数据还真的没有发完,也可能是 server 应用层代码的 bug,关闭连接不够及时),就会一直保持在 CLOSE_WAIT 状态。修改 server 的代码,让 server 持续读取数据,直到 recv 返回 0,说明 client 已经调用了 close,此时让 server 线程休眠 30s:

        30s 内用 netstat 查看连接状态,server 会一直处于 CLOSE_WAIT 状态,client 则处于 FIN_WAIT_2 状态(因为没有收到 server 的 FIN 包): 

关于 close 和 shutdown

        在 CLOSE_WAIT 阶段,因为从 server 到 client 这一方向的连接还没有关闭,如果 server 还没有发送完数据,是可以给 client 继续发送数据的。但实际上,如果只是简单地在 client 调用 close 后增加 recv 的代码:

         同时,server 在 recv 返回 0 后增加发送数据的代码(SERVER_SEND_BYE 为 宏定义,值是 "Goodbye Client~"):

        client 的应用层不仅接收不到最后的 good bye,client 在收到数据包后还会给 server 返回 RST 包直接断掉连接:

        原因是 close 会把套接字标记为已关闭,关闭之后就不能再被应用层使用,也就是说不能再作为 recv 或 send 的第一个参数。为了能在 CLOSE_WAIT 阶段进行数据的收发,需要把 client 调用 close 改为调用 shutdown,在发送完数据后先关闭连接写的一半,接收完最后的数据再关闭连接读的一半:

TIME_WAIT 

        client 收到 server 的 FIN 后答复 ACK,并进入 TIME_WAIT 状态(这个 ACK 发到 server,server 就进入 CLOSED 状态)。TIME_WAIT 的时间长度是 2MSL。MSL(Maximum Segment Lifetime)是任何 IP 数据报能够在因特网中存活的最长时间,如果一个报文段发出后,经过 MSL 还没有来得及到达终点,就会被丢弃。关于为什么要设置 TIME_WAIT,从《UNIX 网络编程 卷1:套接字联网 API》里找到了两点:

  1. 可靠地实现 TCP 全双工连接的终止:假设第四次挥手的报文段(ACK)没有到达 server,那么 server 将重发 FIN(重新进行第三次挥手),收到重发的 FIN 后,主动关闭连接的 client 的也需要重发 ACK;如果没有 TIME_WAIT 的等待时间,重发的 FIN 达到后,client 将回以 RST,导致连接异常终止。
  2. 允许老的重复分节在网络中消逝:我们关闭一个连接,过一段时间后在相同的 IP 地址和端口之间建立另一个连接;后一个连接称为前一个连接的化身,因为它们的地址和端口号都相同;TCP 必须防止来自某个连接的老的重复分在在该连接已终止后再出现,从而被误解成属于同一连接的某个新的化身;为了做到这一点,TCP 将不给处于 TIME_WAIT 状态的连接发起新的化身;既然 TIME_WAIT 状态的持续时间是 MSL 的 2 倍,这就足以让某个方向上的分组最多存活 MSL 秒即被丢弃,另一个方向的应答最多存活 MSL 秒也被丢弃。通过实施这个规则,我们就能保证每成功建立一个 TCP 连接时,来自该连接先前化身的老的重复分组都已在网络中消逝了。

        在【网络编程】TCP socket 单机通信实验 里,如果在短时间内连续启动服务端,可能出现端口绑定失败的情况,印证了第 2 点的说法:

        MSL 在实现中的常用值是 30 秒、1 分钟或者 2 分钟, 也就是一旦进入 TIME_WAIT 状态,就会有至少 1 分钟的时间,端口无法使用。另一方面,在 TIME_WAIT 状态,连接句柄是不会被释放的,只有转到 CLOSED 状态后才会。文件描述符的数量有限(一般是 1024 个),如果短时间内,有大量的处于 TIME_WAIT 状态的连接,会导致文件描述符耗尽,涉及到文件描述符的操作都会受到影响。在这一点上,CLOSE_WAIT 也是一样的,大量 CLOSE_WAIT 状态的连接同样会导致文件描述符耗尽。所以,最好让这两种状态尽快结束或者干脆不要进入。规避 TIME_WAIT 带来的影响的方式主要有两种:

让客户端先关闭连接

        只有主动关闭连接的一方,才会进入 TIME_WAIT 状态。如果 client 先调用 close 接口,server 后调用,server 就不会进入 TIME_WAIT 状态。 按照上面 CLOSE_WAIT 和 LAST_ACK 部分对代码的修改 —— 直到 recv 返回 0 才调用 close 接口,就可以实现让客户端先(主动)关闭连接,避免 server 进入 TIME_WAIT 状态。 

设置端口可复用

        如果屏蔽掉 CLOSE_WAIT 和 LAST_ACK 部分对代码的修改,允许 server 主动关闭连接进入 TIME_WAIT 状态,则可以设置 socket 的 SO_REUSEADDR(端口重用) 属性,在 TIME_WAIT 时允许端口复用:

         设置选项后,先运行一次程序,然后用 netstat 查看,server 处于 TIME_WAIT 状态:

        紧接着再次运行程序,server 绑定端口成功。第二次运行结束后,用 netstat 查看,有两个服务端的连接处于 TIME_WAIT 状态(在 TIME_WAIT 状态结束前用同一个端口又完成了一次消息通信):

        针对 TIME_WAIT,让客户端先关闭连接是比设置端口重用更好的一种方式,避免 server 进入 TIME_WAIT 状态可以让资源尽快释放。设置端口可重用也还有其他更大的用处。

完整代码

头文件

  1. #include <stdio.h>
  2. #include <unistd.h>
  3. #include <errno.h>
  4. #include <string.h>
  5. #include <pthread.h>
  6. #include <sys/types.h>
  7. #include <sys/socket.h>
  8. #include <netinet/in.h>
  9. #include <arpa/inet.h>

宏定义

  1. #define LOCAL_IP_ADDR "127.0.0.1"
  2. #define SERVER_LISTEN_PORT 5197
  3. #define NET_MSG_BUF_LEN 128
  4. #define CLINET_SEND_MSG "Hello Server~"
  5. #define SERVER_SEND_MSG "Hello Client~"
  6. #define SERVER_SEND_BYE "Goodbye Client~"

服务端线程入口函数

  1. void* socketServer(void* param){
  2. int iRes = 0;
  3. int iLsnFd, iConnFd;
  4. int iReuse = 0;
  5. int iNetMsgLen = 0;
  6. socklen_t iSockAddrLen = 0;
  7. char szNetMsg[NET_MSG_BUF_LEN] = {0};
  8. struct sockaddr_in stLsnAddr;
  9. struct sockaddr_in stCliAddr;
  10. // 1 参指定协议族,AF_INET 对应 IPv4
  11. // 2 参指定套接字类型,SOCK_STREAM 对应 面向连接的流式套接字
  12. // 3 参指定协议类型,0 对应 TCP 协议
  13. iLsnFd = socket(AF_INET, SOCK_STREAM, 0);
  14. if (-1 == iLsnFd) {
  15. printf("Server failed to create socket, err[%s]\n",
  16. strerror(errno));
  17. return NULL;
  18. }
  19. // 设置端口复用要在 bind 端口之前进行,且所有使用同一端口的套接字都要设置可复用选项
  20. // 1 参传入 socket 句柄(监听句柄)
  21. // 2 参传入 socket 选项所在的协议层,SOL_SOCKET 表示在套接字级别上设置选项
  22. // 3 参传入选项名,设置端口可复用用选项名 SO_REUSEPORT
  23. // 4 参传入保存选项值的内存地址,设置端口可复用,选项值传 1
  24. // 5 参传入保存选项值的内存空间大小
  25. iReuse = 1;
  26. iRes = setsockopt(iLsnFd, SOL_SOCKET, SO_REUSEPORT, &iReuse, sizeof (iReuse));
  27. if (-1 == iRes) {
  28. printf("Server failed set reuse attr, err[%s]\n", strerror(errno));
  29. close(iLsnFd);
  30. return NULL;
  31. }
  32. // 填写监听地址,设置 s_addr = INADDR_ANY 表示监听所有网卡上对应的端口
  33. stLsnAddr.sin_family = AF_INET;
  34. stLsnAddr.sin_port = htons(SERVER_LISTEN_PORT);
  35. stLsnAddr.sin_addr.s_addr = INADDR_ANY;
  36. // 1 参传入 socket 句柄,2 参传入监听地址,3 参传入监听地址结构体的大小
  37. iRes = bind(iLsnFd, (struct sockaddr*)&stLsnAddr, sizeof(stLsnAddr));
  38. if (-1 == iRes) {
  39. printf("Server failed to bind port[%u], err[%s]\n",
  40. SERVER_LISTEN_PORT, strerror(errno));
  41. close(iLsnFd);
  42. return NULL;
  43. } else {
  44. printf("Server succeeded to bind port[%u], start listen.\n",
  45. SERVER_LISTEN_PORT);
  46. }
  47. // 1 参传入监听句柄,
  48. // 2 参设置已完成连接队列(已完成三次握手,未 accept 的连接)的长度
  49. iRes = listen(iLsnFd, 16);
  50. if (-1 == iRes) {
  51. printf("Server failed to listen port[%u], err[%s]\n",
  52. SERVER_LISTEN_PORT, strerror(errno));
  53. close(iLsnFd);
  54. return NULL;
  55. }
  56. iSockAddrLen = sizeof(stCliAddr);
  57. // 1 参传入监听句柄,2 传入地址结构体指针接收客户端地址,3 参传入地址结构体大小
  58. iConnFd = accept(iLsnFd, (struct sockaddr*)&stCliAddr, &iSockAddrLen);
  59. if (-1 == iConnFd) {
  60. printf("Server failed to accept connect request, err[%s]\n",
  61. strerror(errno));
  62. close(iLsnFd);
  63. return NULL;
  64. } else {
  65. printf("Server accept connect request from[%s:%u]\n",
  66. inet_ntoa(stCliAddr.sin_addr), ntohs(stCliAddr.sin_port));
  67. }
  68. // 1 参传已连接套接字描述符,2 参传缓冲区指针,3 参传缓冲区大小,
  69. // 4 参指定行为,默认为 0
  70. iNetMsgLen = recv(iConnFd, szNetMsg, sizeof(szNetMsg), 0);
  71. if (iNetMsgLen < 0) {
  72. printf("Server failed to read from network, err[%s]\n", strerror(errno));
  73. close(iConnFd);
  74. close(iLsnFd);
  75. return NULL;
  76. } else {
  77. printf("Server recv msg[%s]\n", szNetMsg);
  78. }
  79. // 1 参传已连接套接字的描述符,2 参传指向消息数据的指针
  80. // 3 参传消息长度,4 参指定行为,默认为 0
  81. iNetMsgLen = send(iConnFd, SERVER_SEND_MSG, strlen(SERVER_SEND_MSG), 0);
  82. if (iNetMsgLen < 0) {
  83. printf("Server failed to reply client, err[%s]\n", strerror(errno));
  84. close(iConnFd);
  85. close(iLsnFd);
  86. return NULL;
  87. }
  88. while (1) {
  89. iNetMsgLen = recv(iConnFd, szNetMsg, sizeof(szNetMsg), 0);
  90. if (iNetMsgLen < 0) {
  91. printf("Server failed to read from network, err[%s]\n", strerror(errno));
  92. break;
  93. } else if (iNetMsgLen == 0) {
  94. printf("Server recv return zero, client already closed connection\n");
  95. iNetMsgLen = send(iConnFd, SERVER_SEND_BYE, strlen(SERVER_SEND_BYE), 0);
  96. if (iNetMsgLen < 0) {
  97. printf("Server failed to say bye, err[%s]\n", strerror(errno));
  98. }
  99. break;
  100. }
  101. }
  102. close(iConnFd);
  103. close(iLsnFd);
  104. return NULL;
  105. }

客户端线程入口函数

  1. void* socketClient(void* param){
  2. int iRes = 0;
  3. int iConnFd;
  4. int iNetMsgLen = 0;
  5. char szNetMsg[NET_MSG_BUF_LEN] = {0};
  6. struct sockaddr_in stServAddr;
  7. iConnFd = socket(AF_INET, SOCK_STREAM, 0);
  8. if (-1 == iConnFd) {
  9. printf("Client failed to create socket, err[%s]\n", strerror(errno));
  10. return NULL;
  11. }
  12. // 填充目标地址结构体,指定协议族、目标端口、目标主机 IP 地址
  13. stServAddr.sin_family = AF_INET;
  14. stServAddr.sin_port = htons(SERVER_LISTEN_PORT);
  15. stServAddr.sin_addr.s_addr = inet_addr(LOCAL_IP_ADDR);
  16. // 1 参传套接字句柄,2 参传准备连接的目标地址结构体指针,3 参传地址结构体大小
  17. while (1) {
  18. iRes = connect(iConnFd, (struct sockaddr *)&stServAddr, sizeof(stServAddr));
  19. if (0 != iRes) {
  20. printf("Client failed to connect to[%s:%u], err[%s]\n",
  21. LOCAL_IP_ADDR, SERVER_LISTEN_PORT, strerror(errno));
  22. sleep(2);
  23. continue;
  24. } else {
  25. printf("Client succeeded to connect to[%s:%u]\n",
  26. LOCAL_IP_ADDR, SERVER_LISTEN_PORT);
  27. break;
  28. }
  29. }
  30. iNetMsgLen = send(iConnFd, CLINET_SEND_MSG, strlen(CLINET_SEND_MSG), 0);
  31. if (iNetMsgLen < 0) {
  32. printf("Client failed to send msg to server, err[%s]\n", strerror(errno));
  33. close(iConnFd);
  34. return NULL;
  35. }
  36. iNetMsgLen = recv(iConnFd, szNetMsg, sizeof(szNetMsg), 0);
  37. if (iNetMsgLen < 0) {
  38. printf("Client failed to read from network, err[%s]\n", strerror(errno));
  39. close(iConnFd);
  40. return NULL;
  41. } else {
  42. printf("Client recv reply[%s]\n", szNetMsg);
  43. }
  44. #if 0
  45. // close 后套接字无法再被使用
  46. close(iConnFd);
  47. #else
  48. // 1 参传入 socket 句柄,2 参传入 socket 连接的断开方式:
  49. // SHUT_WR 关闭连接的写这一半,SHUT_RD 关闭连接的读这一半,SHUT_RDWR 把读和写都关掉
  50. shutdown(iConnFd, SHUT_WR);
  51. #endif
  52. while (1)
  53. {
  54. iNetMsgLen = recv(iConnFd, szNetMsg, sizeof(szNetMsg), 0);
  55. if (iNetMsgLen < 0) {
  56. printf("Client failed to read from network after close, err[%s]\n",
  57. strerror(errno));
  58. break;
  59. } else if (iNetMsgLen == 0) {
  60. printf("Client recv return zero, server closed connection too\n");
  61. break;
  62. } else {
  63. printf("Client recv msg[%s] after close socket\n", szNetMsg);
  64. }
  65. }
  66. #if 1
  67. // 关闭连接读的一半
  68. shutdown(iConnFd, SHUT_RD);
  69. #endif
  70. return NULL;
  71. }

主函数

  1. int main(){
  2. // 线程 ID,实质是 unsigned long 类型整数
  3. pthread_t thdServer = 1;
  4. pthread_t thdClient = 2;
  5. // 1 参传线程 ID,2 参传线程属性,3 参指定线程入口函数,4 参指定传给入口函数的参数
  6. pthread_create(&thdServer, NULL, socketServer, NULL);
  7. pthread_create(&thdClient, NULL, socketClient, NULL);
  8. // 1 参传入线程 ID,2 参用于接收线程入口函数的返回值,不需要返回值则置 NULL
  9. pthread_join(thdServer, NULL);
  10. pthread_join(thdClient, NULL);
  11. return 0;
  12. }
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/天景科技苑/article/detail/754205
推荐阅读
相关标签
  

闽ICP备14008679号