赞
踩
文章以【网络编程】TCP socket 单机通信实验 为基础。对 TCP 连接中的四种 WAIT 状态进行分析实验。从服务端开始监听到建立连接、收发数据,到最后连接完全关闭,TCP 一共会经历 11 种状态,总体状态变化图如下(假设由 client 主动关闭连接,server 被动关闭),其中有四种 "WAIT" 状态 —— FIN_WAIT_1、FIN_WAIT_2、TIME_WAIT、CLOSE_WAIT —— 容易混淆,而且有很多 “诡异” 的问题也于此有关:
图片来源:自己拍的《TCP/IP 详解 卷一:协议》 第 441 页
client 应用层调用 close 接口,主动发送 FIN 包(第一次挥手),进入 FIN_WAIT_1 状态。待 FIN 包发送到 server,server 返回的 ACK 到达后(第二次挥手),client 进入 FIN_WAIT_2 状态。正常情况下,FIN_WAIT_1 是一个非常短暂的状态。FIN_WAIT_2 的时间长度则取决于 server 什么时候进行第三次挥手。
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_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,在发送完数据后先关闭连接写的一半,接收完最后的数据再关闭连接读的一半:
client 收到 server 的 FIN 后答复 ACK,并进入 TIME_WAIT 状态(这个 ACK 发到 server,server 就进入 CLOSED 状态)。TIME_WAIT 的时间长度是 2MSL。MSL(Maximum Segment Lifetime)是任何 IP 数据报能够在因特网中存活的最长时间,如果一个报文段发出后,经过 MSL 还没有来得及到达终点,就会被丢弃。关于为什么要设置 TIME_WAIT,从《UNIX 网络编程 卷1:套接字联网 API》里找到了两点:
在【网络编程】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 状态可以让资源尽快释放。设置端口可重用也还有其他更大的用处。
- #include <stdio.h>
- #include <unistd.h>
- #include <errno.h>
- #include <string.h>
- #include <pthread.h>
-
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #define LOCAL_IP_ADDR "127.0.0.1"
- #define SERVER_LISTEN_PORT 5197
- #define NET_MSG_BUF_LEN 128
- #define CLINET_SEND_MSG "Hello Server~"
- #define SERVER_SEND_MSG "Hello Client~"
- #define SERVER_SEND_BYE "Goodbye Client~"
- void* socketServer(void* param){
- int iRes = 0;
- int iLsnFd, iConnFd;
- int iReuse = 0;
- int iNetMsgLen = 0;
- socklen_t iSockAddrLen = 0;
- char szNetMsg[NET_MSG_BUF_LEN] = {0};
- struct sockaddr_in stLsnAddr;
- struct sockaddr_in stCliAddr;
-
- // 1 参指定协议族,AF_INET 对应 IPv4
- // 2 参指定套接字类型,SOCK_STREAM 对应 面向连接的流式套接字
- // 3 参指定协议类型,0 对应 TCP 协议
- iLsnFd = socket(AF_INET, SOCK_STREAM, 0);
- if (-1 == iLsnFd) {
- printf("Server failed to create socket, err[%s]\n",
- strerror(errno));
- return NULL;
- }
-
- // 设置端口复用要在 bind 端口之前进行,且所有使用同一端口的套接字都要设置可复用选项
- // 1 参传入 socket 句柄(监听句柄)
- // 2 参传入 socket 选项所在的协议层,SOL_SOCKET 表示在套接字级别上设置选项
- // 3 参传入选项名,设置端口可复用用选项名 SO_REUSEPORT
- // 4 参传入保存选项值的内存地址,设置端口可复用,选项值传 1
- // 5 参传入保存选项值的内存空间大小
- iReuse = 1;
- iRes = setsockopt(iLsnFd, SOL_SOCKET, SO_REUSEPORT, &iReuse, sizeof (iReuse));
- if (-1 == iRes) {
- printf("Server failed set reuse attr, err[%s]\n", strerror(errno));
- close(iLsnFd);
- return NULL;
- }
-
- // 填写监听地址,设置 s_addr = INADDR_ANY 表示监听所有网卡上对应的端口
- stLsnAddr.sin_family = AF_INET;
- stLsnAddr.sin_port = htons(SERVER_LISTEN_PORT);
- stLsnAddr.sin_addr.s_addr = INADDR_ANY;
- // 1 参传入 socket 句柄,2 参传入监听地址,3 参传入监听地址结构体的大小
- iRes = bind(iLsnFd, (struct sockaddr*)&stLsnAddr, sizeof(stLsnAddr));
- if (-1 == iRes) {
- printf("Server failed to bind port[%u], err[%s]\n",
- SERVER_LISTEN_PORT, strerror(errno));
- close(iLsnFd);
- return NULL;
- } else {
- printf("Server succeeded to bind port[%u], start listen.\n",
- SERVER_LISTEN_PORT);
- }
-
- // 1 参传入监听句柄,
- // 2 参设置已完成连接队列(已完成三次握手,未 accept 的连接)的长度
- iRes = listen(iLsnFd, 16);
- if (-1 == iRes) {
- printf("Server failed to listen port[%u], err[%s]\n",
- SERVER_LISTEN_PORT, strerror(errno));
- close(iLsnFd);
- return NULL;
- }
-
- iSockAddrLen = sizeof(stCliAddr);
- // 1 参传入监听句柄,2 传入地址结构体指针接收客户端地址,3 参传入地址结构体大小
- iConnFd = accept(iLsnFd, (struct sockaddr*)&stCliAddr, &iSockAddrLen);
- if (-1 == iConnFd) {
- printf("Server failed to accept connect request, err[%s]\n",
- strerror(errno));
- close(iLsnFd);
- return NULL;
- } else {
- printf("Server accept connect request from[%s:%u]\n",
- inet_ntoa(stCliAddr.sin_addr), ntohs(stCliAddr.sin_port));
- }
-
- // 1 参传已连接套接字描述符,2 参传缓冲区指针,3 参传缓冲区大小,
- // 4 参指定行为,默认为 0
- iNetMsgLen = recv(iConnFd, szNetMsg, sizeof(szNetMsg), 0);
- if (iNetMsgLen < 0) {
- printf("Server failed to read from network, err[%s]\n", strerror(errno));
- close(iConnFd);
- close(iLsnFd);
- return NULL;
- } else {
- printf("Server recv msg[%s]\n", szNetMsg);
- }
-
- // 1 参传已连接套接字的描述符,2 参传指向消息数据的指针
- // 3 参传消息长度,4 参指定行为,默认为 0
- iNetMsgLen = send(iConnFd, SERVER_SEND_MSG, strlen(SERVER_SEND_MSG), 0);
- if (iNetMsgLen < 0) {
- printf("Server failed to reply client, err[%s]\n", strerror(errno));
- close(iConnFd);
- close(iLsnFd);
- return NULL;
- }
-
- while (1) {
- iNetMsgLen = recv(iConnFd, szNetMsg, sizeof(szNetMsg), 0);
- if (iNetMsgLen < 0) {
- printf("Server failed to read from network, err[%s]\n", strerror(errno));
- break;
- } else if (iNetMsgLen == 0) {
- printf("Server recv return zero, client already closed connection\n");
- iNetMsgLen = send(iConnFd, SERVER_SEND_BYE, strlen(SERVER_SEND_BYE), 0);
- if (iNetMsgLen < 0) {
- printf("Server failed to say bye, err[%s]\n", strerror(errno));
- }
- break;
- }
- }
-
- close(iConnFd);
- close(iLsnFd);
- return NULL;
- }
- void* socketClient(void* param){
- int iRes = 0;
- int iConnFd;
- int iNetMsgLen = 0;
- char szNetMsg[NET_MSG_BUF_LEN] = {0};
- struct sockaddr_in stServAddr;
-
- iConnFd = socket(AF_INET, SOCK_STREAM, 0);
- if (-1 == iConnFd) {
- printf("Client failed to create socket, err[%s]\n", strerror(errno));
-
- return NULL;
- }
-
- // 填充目标地址结构体,指定协议族、目标端口、目标主机 IP 地址
- stServAddr.sin_family = AF_INET;
- stServAddr.sin_port = htons(SERVER_LISTEN_PORT);
- stServAddr.sin_addr.s_addr = inet_addr(LOCAL_IP_ADDR);
- // 1 参传套接字句柄,2 参传准备连接的目标地址结构体指针,3 参传地址结构体大小
- while (1) {
- iRes = connect(iConnFd, (struct sockaddr *)&stServAddr, sizeof(stServAddr));
- if (0 != iRes) {
- printf("Client failed to connect to[%s:%u], err[%s]\n",
- LOCAL_IP_ADDR, SERVER_LISTEN_PORT, strerror(errno));
- sleep(2);
- continue;
- } else {
- printf("Client succeeded to connect to[%s:%u]\n",
- LOCAL_IP_ADDR, SERVER_LISTEN_PORT);
- break;
- }
- }
-
- iNetMsgLen = send(iConnFd, CLINET_SEND_MSG, strlen(CLINET_SEND_MSG), 0);
- if (iNetMsgLen < 0) {
- printf("Client failed to send msg to server, err[%s]\n", strerror(errno));
- close(iConnFd);
- return NULL;
- }
-
- iNetMsgLen = recv(iConnFd, szNetMsg, sizeof(szNetMsg), 0);
- if (iNetMsgLen < 0) {
- printf("Client failed to read from network, err[%s]\n", strerror(errno));
- close(iConnFd);
- return NULL;
- } else {
- printf("Client recv reply[%s]\n", szNetMsg);
- }
-
- #if 0
- // close 后套接字无法再被使用
- close(iConnFd);
- #else
- // 1 参传入 socket 句柄,2 参传入 socket 连接的断开方式:
- // SHUT_WR 关闭连接的写这一半,SHUT_RD 关闭连接的读这一半,SHUT_RDWR 把读和写都关掉
- shutdown(iConnFd, SHUT_WR);
- #endif
- while (1)
- {
- iNetMsgLen = recv(iConnFd, szNetMsg, sizeof(szNetMsg), 0);
- if (iNetMsgLen < 0) {
- printf("Client failed to read from network after close, err[%s]\n",
- strerror(errno));
- break;
- } else if (iNetMsgLen == 0) {
- printf("Client recv return zero, server closed connection too\n");
- break;
- } else {
- printf("Client recv msg[%s] after close socket\n", szNetMsg);
- }
- }
- #if 1
- // 关闭连接读的一半
- shutdown(iConnFd, SHUT_RD);
- #endif
-
- return NULL;
- }
- int main(){
-
- // 线程 ID,实质是 unsigned long 类型整数
- pthread_t thdServer = 1;
- pthread_t thdClient = 2;
-
- // 1 参传线程 ID,2 参传线程属性,3 参指定线程入口函数,4 参指定传给入口函数的参数
- pthread_create(&thdServer, NULL, socketServer, NULL);
- pthread_create(&thdClient, NULL, socketClient, NULL);
-
- // 1 参传入线程 ID,2 参用于接收线程入口函数的返回值,不需要返回值则置 NULL
- pthread_join(thdServer, NULL);
- pthread_join(thdClient, NULL);
- return 0;
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。