目录
个人主页:东洛的克莱斯韦克-CSDN博客
状态汇总
过程 | 客户端状态 | 服务器状态 | 描述 |
---|---|---|---|
开始 | CLOSED | LISTEN | 客户端处于初始状态,服务器监听连接请求 |
SYN | SYN_SENT | LISTEN -> ... | 客户端发送SYN,进入SYN_SENT状态;服务器收到SYN,准备响应 |
SYN-ACK | SYN_SENT | SYN_RECEIVED (短暂) | 客户端保持SYN_SENT,等待ACK;服务器发送SYN-ACK,短暂进入SYN_RECEIVED |
ACK | ESTABLISHED | ESTABLISHED | 客户端收到SYN-ACK并发送ACK,进入ESTABLISHED;服务器收到ACK,也进入ESTABLISHED |
数据传输 | ESTABLISHED | ESTABLISHED | 数据传输阶段,双方都处于ESTABLISHED状态 |
FIN | FIN_WAIT_1 | CLOSE_WAIT | 客户端发送FIN,进入FIN_WAIT_1;服务器收到FIN,发送ACK并进入CLOSE_WAIT |
ACK (对FIN的响应) | FIN_WAIT_2 | CLOSE_WAIT | 客户端收到服务器的ACK,进入FIN_WAIT_2;服务器保持CLOSE_WAIT |
服务器FIN | FIN_WAIT_2 -> TIME_WAIT | LAST_ACK | 服务器发送FIN,进入LAST_ACK;客户端收到FIN,准备发送ACK |
ACK (对服务器FIN的响应) | TIME_WAIT | CLOSED | 客户端发送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 即可解 决问题