Bootstrap

TCP 三次握手和四次挥手

一、三次握手 (Three-Way Handshake)

概念
三次握手用于在 TCP 连接建立时,确保客户端和服务器之间的通信信道可靠,并同步双方的初始序列号。

三次握手过程
  1. 第一次握手:客户端向服务器发送 SYN (synchronize) 报文,表示请求建立连接,并携带一个初始序列号 Seq = x

    • 客户端状态:SYN_SENT
  2. 第二次握手:服务器收到 SYN 报文后,回复一个 SYN + ACK 报文,表示同意建立连接,并携带自己的初始序列号 Seq = y 和确认号 Ack = x+1

    • 服务器状态:SYN_RCVD
  3. 第三次握手:客户端收到 SYN + ACK 报文后,回复一个 ACK 报文,确认序列号为 Ack = y+1,并正式建立连接。

    • 客户端状态:ESTABLISHED
    • 服务器状态:ESTABLISHED
举例

假设客户端与服务器建立一个连接:

  1. 客户端服务器:
    客户端发送 SYN (Seq=100),请求建立连接。

  2. 服务器客户端:
    服务器回复 SYN+ACK (Seq=300, Ack=101),同意连接。

  3. 客户端服务器:
    客户端回复 ACK (Seq=101, Ack=301),连接建立成功。

 

二、四次挥手 (Four-Way Handshake)

概念
四次挥手用于在 TCP 连接断开时,确保双方都能完全关闭通信,释放连接资源。

四次挥手过程
  1. 第一次挥手:客户端发送 FIN 报文,表示不再发送数据,但可以接收数据。

    • 客户端状态:FIN_WAIT_1
  2. 第二次挥手:服务器收到 FIN 报文后,回复一个 ACK 报文,表示已收到。

    • 服务器状态:CLOSE_WAIT
    • 客户端状态:FIN_WAIT_2
  3. 第三次挥手:服务器发送 FIN 报文,表示不再发送数据。

    • 服务器状态:LAST_ACK
  4. 第四次挥手:客户端收到 FIN 报文后,回复 ACK 报文,表示已确认,进入 TIME_WAIT 状态,等待一段时间后完全关闭连接。

    • 客户端状态:TIME_WAITCLOSED
    • 服务器状态:CLOSED

举例

假设客户端与服务器断开连接:

  1. 客户端服务器:
    客户端发送 FIN (Seq=200),请求关闭发送方向的连接。

  2. 服务器客户端:
    服务器回复 ACK (Seq=300, Ack=201),确认收到关闭请求。

  3. 服务器客户端:
    服务器发送 FIN (Seq=300),请求关闭发送方向的连接。

  4. 客户端服务器:
    客户端回复 ACK (Seq=201, Ack=301),确认收到关闭请求,进入 TIME_WAIT 状态,稍后关闭。


三、三次握手和四次挥手的关键点

比较点三次握手四次挥手
阶段连接建立连接断开
报文数量3 次4 次
参与方客户端和服务器客户端和服务器
目的确认通信双方的可靠性和同步序列号确保双方都完全关闭连接
状态客户端从 SYN_SENTESTABLISHED客户端进入 TIME_WAIT,服务器直接关闭

四、注意事项

  1. 为什么三次握手而不是两次?

    • 两次握手无法确保客户端和服务器的序列号都能正确同步,可能导致连接不可靠。
    • 三次握手能有效防止服务器处理已经过时的连接请求(如延迟的报文重发)。
  2. 为什么四次挥手而不是三次?

    • TCP 是全双工通信协议,连接的关闭需要双方各自发送 FIN,因此需要四次报文交互。
  3. 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;
}

关键点

  1. 三次握手

    • 客户端调用 connect() 时,完成了 SYNACK 报文的交互。
    • 服务器调用 accept() 时,确认连接建立。
  2. 四次挥手

    • 客户端调用 close() 发出 FIN,服务器回复 ACK
    • 服务器随后调用 close() 发出 FIN,客户端回复 ACK,关闭连接。

 

;