【网络编程】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 状态可以让资源尽快释放。设置端口可重用也还有其他更大的用处。

完整代码

头文件

#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;
}
  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值