一、三次握手 (Three-Way Handshake)
概念:
三次握手用于在 TCP 连接建立时,确保客户端和服务器之间的通信信道可靠,并同步双方的初始序列号。
三次握手过程
-
第一次握手:客户端向服务器发送
SYN
(synchronize) 报文,表示请求建立连接,并携带一个初始序列号Seq = x
。- 客户端状态:
SYN_SENT
。
- 客户端状态:
-
第二次握手:服务器收到
SYN
报文后,回复一个SYN + ACK
报文,表示同意建立连接,并携带自己的初始序列号Seq = y
和确认号Ack = x+1
。- 服务器状态:
SYN_RCVD
。
- 服务器状态:
-
第三次握手:客户端收到
SYN + ACK
报文后,回复一个ACK
报文,确认序列号为Ack = y+1
,并正式建立连接。- 客户端状态:
ESTABLISHED
。 - 服务器状态:
ESTABLISHED
。
- 客户端状态:
举例
假设客户端与服务器建立一个连接:
-
客户端 → 服务器:
客户端发送SYN (Seq=100)
,请求建立连接。 -
服务器 → 客户端:
服务器回复SYN+ACK (Seq=300, Ack=101)
,同意连接。 -
客户端 → 服务器:
客户端回复ACK (Seq=101, Ack=301)
,连接建立成功。
二、四次挥手 (Four-Way Handshake)
概念:
四次挥手用于在 TCP 连接断开时,确保双方都能完全关闭通信,释放连接资源。
四次挥手过程
-
第一次挥手:客户端发送
FIN
报文,表示不再发送数据,但可以接收数据。- 客户端状态:
FIN_WAIT_1
。
- 客户端状态:
-
第二次挥手:服务器收到
FIN
报文后,回复一个ACK
报文,表示已收到。- 服务器状态:
CLOSE_WAIT
。 - 客户端状态:
FIN_WAIT_2
。
- 服务器状态:
-
第三次挥手:服务器发送
FIN
报文,表示不再发送数据。- 服务器状态:
LAST_ACK
。
- 服务器状态:
-
第四次挥手:客户端收到
FIN
报文后,回复ACK
报文,表示已确认,进入TIME_WAIT
状态,等待一段时间后完全关闭连接。- 客户端状态:
TIME_WAIT
→CLOSED
。 - 服务器状态:
CLOSED
。
- 客户端状态:
举例
假设客户端与服务器断开连接:
-
客户端 → 服务器:
客户端发送FIN (Seq=200)
,请求关闭发送方向的连接。 -
服务器 → 客户端:
服务器回复ACK (Seq=300, Ack=201)
,确认收到关闭请求。 -
服务器 → 客户端:
服务器发送FIN (Seq=300)
,请求关闭发送方向的连接。 -
客户端 → 服务器:
客户端回复ACK (Seq=201, Ack=301)
,确认收到关闭请求,进入TIME_WAIT
状态,稍后关闭。
三、三次握手和四次挥手的关键点
比较点 | 三次握手 | 四次挥手 |
---|---|---|
阶段 | 连接建立 | 连接断开 |
报文数量 | 3 次 | 4 次 |
参与方 | 客户端和服务器 | 客户端和服务器 |
目的 | 确认通信双方的可靠性和同步序列号 | 确保双方都完全关闭连接 |
状态 | 客户端从 SYN_SENT 到 ESTABLISHED | 客户端进入 TIME_WAIT ,服务器直接关闭 |
四、注意事项
-
为什么三次握手而不是两次?
- 两次握手无法确保客户端和服务器的序列号都能正确同步,可能导致连接不可靠。
- 三次握手能有效防止服务器处理已经过时的连接请求(如延迟的报文重发)。
-
为什么四次挥手而不是三次?
- TCP 是全双工通信协议,连接的关闭需要双方各自发送
FIN
,因此需要四次报文交互。
- TCP 是全双工通信协议,连接的关闭需要双方各自发送
-
TIME_WAIT 状态的作用:
- 确保延迟的 TCP 报文能被正确处理,避免影响后续连接。
五、TCP 三次握手和四次挥手代码示例
服务器端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8080 // 定义服务器监听的端口号
#define BUFFER_SIZE 1024 // 定义缓冲区大小
int main() {
int server_fd, new_socket; // 文件描述符:`server_fd`用于监听,`new_socket`用于与客户端通信
struct sockaddr_in address; // 服务器地址结构
int opt = 1; // 用于设置 socket 选项
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0}; // 缓冲区,用于存储客户端发来的数据
// 创建服务器套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("Socket failed"); // 如果创建失败,打印错误信息并退出
exit(EXIT_FAILURE);
}
// 设置套接字选项,允许端口复用
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
// 初始化服务器地址信息
address.sin_family = AF_INET; // 设置地址族为 IPv4
address.sin_addr.s_addr = INADDR_ANY; // 接受所有 IP 地址
address.sin_port = htons(PORT); // 设置端口号,使用网络字节序
// 绑定套接字到指定的 IP 和端口
if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0) {
perror("Bind failed"); // 如果绑定失败,打印错误信息并退出
exit(EXIT_FAILURE);
}
// 启动监听模式,允许最大连接数为 3
if (listen(server_fd, 3) < 0) {
perror("Listen");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 等待客户端连接(阻塞状态),返回一个新的套接字用于通信
if ((new_socket = accept(server_fd, (struct sockaddr*)&address, (socklen_t*)&addrlen)) < 0) {
perror("Accept");
exit(EXIT_FAILURE);
}
printf("Connection established with client.\n");
// 从客户端接收数据
int valread = read(new_socket, buffer, BUFFER_SIZE);
printf("Received from client: %s\n", buffer);
// 向客户端发送数据
char* hello = "Hello from server!";
send(new_socket, hello, strlen(hello), 0);
printf("Message sent to client.\n");
// 模拟四次挥手,关闭套接字连接
printf("Server: Closing connection.\n");
close(new_socket); // 关闭与客户端的通信套接字
close(server_fd); // 关闭监听套接字
printf("Server closed.\n");
return 0;
}
客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8080 // 定义服务器端口号
#define BUFFER_SIZE 1024 // 定义缓冲区大小
int main() {
int sock = 0; // 客户端套接字文件描述符
struct sockaddr_in serv_addr; // 服务器地址结构
char* hello = "Hello from client!"; // 要发送给服务器的消息
char buffer[BUFFER_SIZE] = {0}; // 缓冲区,用于接收服务器的消息
// 创建客户端套接字
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("Socket creation error\n"); // 如果创建失败,打印错误信息并退出
return -1;
}
// 初始化服务器地址信息
serv_addr.sin_family = AF_INET; // 设置地址族为 IPv4
serv_addr.sin_port = htons(PORT); // 设置端口号,使用网络字节序
// 将服务器的 IP 地址从文本转换为二进制形式
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
printf("Invalid address or Address not supported\n"); // 如果转换失败,打印错误信息并退出
return -1;
}
// 连接服务器(此时完成三次握手)
if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
printf("Connection failed\n"); // 如果连接失败,打印错误信息并退出
return -1;
}
printf("Connected to server.\n");
// 发送消息到服务器
send(sock, hello, strlen(hello), 0);
printf("Message sent to server: %s\n", hello);
// 接收来自服务器的回复
int valread = read(sock, buffer, BUFFER_SIZE);
printf("Received from server: %s\n", buffer);
// 模拟四次挥手,关闭套接字连接
printf("Client: Closing connection.\n");
close(sock); // 关闭客户端套接字
printf("Client closed.\n");
return 0;
}
关键点
-
三次握手:
- 客户端调用
connect()
时,完成了SYN
和ACK
报文的交互。 - 服务器调用
accept()
时,确认连接建立。
- 客户端调用
-
四次挥手:
- 客户端调用
close()
发出FIN
,服务器回复ACK
。 - 服务器随后调用
close()
发出FIN
,客户端回复ACK
,关闭连接。
- 客户端调用