【网络】详解TCP协议通信时客户/服务端的状态

目录

状态汇总

三次握手和四次挥手

服务端状态

客户端状态

TIME_WAIT状态

TIME_WAIT状态的问题

代码测试

理解 CLOSE_WAIT 状态


个人主页:东洛的克莱斯韦克-CSDN博客

状态汇总

过程客户端状态服务器状态描述
开始CLOSEDLISTEN客户端处于初始状态,服务器监听连接请求
SYNSYN_SENTLISTEN -> ...客户端发送SYN,进入SYN_SENT状态;服务器收到SYN,准备响应
SYN-ACKSYN_SENTSYN_RECEIVED (短暂)客户端保持SYN_SENT,等待ACK;服务器发送SYN-ACK,短暂进入SYN_RECEIVED
ACKESTABLISHEDESTABLISHED客户端收到SYN-ACK并发送ACK,进入ESTABLISHED;服务器收到ACK,也进入ESTABLISHED
数据传输ESTABLISHEDESTABLISHED数据传输阶段,双方都处于ESTABLISHED状态
FINFIN_WAIT_1CLOSE_WAIT客户端发送FIN,进入FIN_WAIT_1;服务器收到FIN,发送ACK并进入CLOSE_WAIT
ACK (对FIN的响应)FIN_WAIT_2CLOSE_WAIT客户端收到服务器的ACK,进入FIN_WAIT_2;服务器保持CLOSE_WAIT
服务器FINFIN_WAIT_2 -> TIME_WAITLAST_ACK服务器发送FIN,进入LAST_ACK;客户端收到FIN,准备发送ACK
ACK (对服务器FIN的响应)TIME_WAITCLOSED客户端发送ACK给服务器,进入TIME_WAIT;服务器收到ACK,关闭连接
结束TIME_WAIT -> CLOSED-客户端等待足够时间(TIME_WAIT),然后关闭连接;服务器已关闭

三次握手和四次挥手

服务端状态

[CLOSED -> LISTEN] 服务器端调用 listen 后进入 LISTEN 状态, 等待客户端连 接;

[LISTEN -> SYN_RCVD] 一旦监听到连接请求(同步报文段), 就将该连接放入 内核等待队列中, 并向客户端发送 SYN 确认报文.

[SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文, 就进入 ESTABLISHED 状态, 可以进行读写数据了.

[ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用 close), 服务 器会收到结束报文段, 服务器返回确认报文段并进入 CLOSE_WAIT; • [CLOSE_WAIT -> LAST_ACK] 进入CLOSE_WAIT 后说明服务器准备关闭连 接(需要处理完之前的数据);

当服务器真正调用 close 关闭连接时, 会向客户端发送 FIN, 此时服务器进入 LAST_ACK 状态, 等待最后一个 ACK 到来(这个 ACK 是客户 端确认收到了 FIN) • [LAST_ACK -> CLOSED] 服务器收到了对 FIN 的 ACK

客户端状态

[CLOSED -> SYN_SENT] 客户端调用 connect, 发送同步报文段;

[SYN_SENT -> ESTABLISHED] connect 调用成功, 则进入 ESTABLISHED 状 态, 开始读写数据;

[ESTABLISHED -> FIN_WAIT_1] 客户端主动调用 close 时, 向服务器发送结 束报文段, 同时进入 FIN_WAIT_1;

[FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认, 则进 入 FIN_WAIT_2, 开始等待服务器的结束报文段;

[FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报文段, 进入 TIME_WAIT, 并发出 LAST_ACK;

[TIME_WAIT -> CLOSED] 客户端要等待一个 2MSL(Max Segment Life, 报文 最大生存时间)的时间, 才会进入 CLOSED 状态.

TIME_WAIT状态

四次挥手过程:TCP连接的关闭需要四个步骤(或称为“挥手”),这主要是因为TCP是全双工协议,即数据可以在两个方向上同时流动。因此,每个方向都需要独立地关闭。

FIN_WAIT_2状态:当客户端发送FIN给服务器,并收到服务器对FIN的ACK时,客户端进入FIN_WAIT_2状态。在这个状态下,客户端等待服务器发送FIN来关闭其到客户端的数据流。

TIME_WAIT状态:一旦客户端收到服务器的FIN,它发送一个ACK给服务器,并进入TIME_WAIT状态。TIME_WAIT状态的主要目的是:

确保服务器接收到了来自客户端的对FIN的ACK。如果服务器没有收到这个ACK,它可能会重新发送FIN。在TIME_WAIT期间,客户端可以重发这个ACK(尽管这通常不是必需的,因为ACK通常不需要确认)。

防止旧的重复连接初始化报文段(即“迟到的”报文段)被错误地认为是新连接的SYN报文段。TIME_WAIT状态的持续时间(通常为2MSL,即两倍的最大报文段生存时间)足够长,可以确保来自上一个连接的所有报文段都已经从网络中消失。

MSL(Maximum Segment Lifetime):最大报文段生存时间,是指一个TCP报文段在网络中能够存在的最长时间。这个时间通常是基于网络的最大往返时间(RTT)来估计的,但通常会设置一个固定的值(如30秒、1分钟等),以确保报文的生存时间足够长,能够覆盖几乎所有可能的网络延迟和拥塞情况。

CLOSED状态:在TIME_WAIT状态持续一段时间后(通常是2MSL),客户端会关闭连接并进入CLOSED状态,此时连接完全关闭,所有资源都被释放。

TIME_WAIT状态的问题

可用如下

TCP 协议规定,主动关闭连接的一方要处于 TIME_ WAIT 状态,等待两个 MSL(maximum segment lifetime)的时间后才能回到 CLOSED 状态. 我们使用 Ctrl-C 终止了 server, 所以 server 是主动关闭连接的一方, 在 TIME_WAIT 期间仍然不能再次监听同样的 server 端口;

代码测试

服务端

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <unistd.h>  
#include <sys/socket.h>  
#include <netinet/in.h>  
  
#define PORT 12345 
#define BUFFER_SIZE 1024  
  
int main() {  
    int server_fd, new_socket;  
    struct sockaddr_in address;  
    int opt = 1;  
    int addrlen = sizeof(address);  
    char buffer[BUFFER_SIZE] = "Hello from server";  
    char *hello = "Hello from server";  
  
    // 创建套接字文件描述符  
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {  
        perror("socket failed");  
        exit(EXIT_FAILURE);  
    }  
  
    // 绑定套接字到端口  
    address.sin_family = AF_INET;  
    address.sin_addr.s_addr = INADDR_ANY;  
    address.sin_port = htons(PORT);  
  
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address))<0) {  
        perror("bind failed");  
        exit(EXIT_FAILURE);  
    }  
  
    // 监听连接  
    if (listen(server_fd, 3) < 0) {  
        perror("listen");  
        exit(EXIT_FAILURE);  
    }  
  
    // 接受连接  
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen))<0) {  
        perror("accept");  
        exit(EXIT_FAILURE);  
    }  
  
    // 发送数据  
    send(new_socket, hello, strlen(hello), 0);  
   
     while (1)
    {
        sleep(1);
    }
  
    // 关闭套接字  
    close(new_socket);  
    close(server_fd);  
    return 0;  
}

客户端

#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <string.h>  
#include <sys/socket.h>  
#include <netinet/in.h>  
#include <arpa/inet.h>  
  
#define PORT 12345  
#define BUFFER_SIZE 1024  
  
int main() {  
    struct sockaddr_in serv_addr;  
    int sock = 0;  
    char buffer[BUFFER_SIZE] = {0};  
  
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {  
        printf("\n Socket creation error \n");  
        return -1;  
    }  
  
    serv_addr.sin_family = AF_INET;  
    serv_addr.sin_port = htons(PORT);  
  
    // 将IPv4地址从文本转换为二进制形式  
    if(inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr)<=0) {  
        printf("\nInvalid address/ Address not supported \n");  
        return -1;  
    }  
  
    // 连接到服务器  
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {  
        printf("\nConnection Failed \n");  
        return -1;  
    }  
  
    // 接收数据  
    int valread = read(sock, buffer, BUFFER_SIZE);  
    printf("%s\n", buffer);  

    while (1)
    {
        sleep(1);
    }
  
    // 关闭套接字以触发四次挥手  
    close(sock);  
    return 0;  
}

上述两份代码都是死循环,如果客户端先 Ctrl-C掉,服务端也能马上起来,如果服务端先 Ctrl-C掉,就会有地址复用的问题。

服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短, 但是 每秒都有很大数量的客户端来请求).

这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃, 就需要被服务 器端主动清理掉), 就会产生大量 TIME_WAIT 连接.

 由于我们的请求量很大, 就可能导致 TIME_WAIT 的连接数很多, 每个连接都会 占用一个通信五元组(源 ip, 源端口, 目的 ip, 目的端口, 协议). 其中服务器的 ip 和端 口和协议是固定的. 如果新来的客户端连接的 ip 和端口号和 TIME_WAIT 占用的链 接重复了, 就会出现问题.

理解 CLOSE_WAIT 状态

对于服务器上出现大量的 CLOSE_WAIT 状态, 原因就是服务器没有正确的关闭 socket, 导致四次挥手没有正确完成. 这是一个 BUG. 只需要加上对应的 close 即可解 决问题


【网络】传输层TCP协议的报头和传输机制-CSDN博客

【网络】代理服务器-CSDN博客

【网络】私有IP和公网IP的转换——NAT技术-CSDN博客

  • 17
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值