IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> 【网络编程】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 状态可以让资源尽快释放。设置端口可重用也还有其他更大的用处。

完整代码

头文件

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>    // 用到了 pthread 库,编译要加选项 -lpthread

#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;
}
  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-09-13 09:37:35  更:2021-09-13 09:38:11 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年6日历 -2024/6/27 2:07:16-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码