backlog 全连接队列 + shutdown 半关闭 + recv_exact/send_all 深度详解

文章结构概览

本文深入解析 TCP Socket 编程中三个最核心的基础模块,它们共同构成了可靠长连接模型的基石:

TCP Socket 三大核心模块

backlog 全连接队列

shutdown 半关闭机制

recv_exact/send_all 原语

连接排队管理

accept() 数据源

解决连接积压问题

优雅断开连接

全双工独立控制

保障数据完整性

解决半包问题

可靠读写保证

应用层协议基础

实现高并发服务端

避免数据丢失

构建可靠消息传输

模块关系与执行顺序

  1. backlog 全连接队列 - 建立连接的基础,管理已完成三次握手的就绪连接
  2. shutdown 半关闭机制 - 断开连接的优雅方式,保障数据传输完整性
  3. recv_exact/send_all 原语 - 数据传输的可靠保证,解决TCP字节流边界问题

这三个模块层层递进,从连接建立、数据传输到连接关闭,构成了完整的TCP长连接生命周期管理。

整体定位

这三件事是 Socket底座完成后,TCP 长连接模型最关键的3个基础模块

  • backlog 全连接队列:解决 TCP 服务端监听连接排队问题,是 accept 的数据源
  • shutdown 半关闭:实现 TCP 全双工优雅断开,区别于粗暴 close,保障剩余数据不丢失
  • recv_exact / send_all:解决 TCP 字节流半包读写问题,提供可靠读写原语(内核层原语封装,≠应用层协议拆包)

一、backlog 全连接队列

1. 背景:TCP 三次握手两套队列

两套队列(Linux内核真实模型)
  1. 半连接队列(SYN队列 / 未完成队列)
    • 存放收到 SYN、回复 SYN+ACK,但还没收到客户端 ACK 的连接
    • 状态:SYN_RCVD,还没完成三次握手,不能被 accept 取用
    • 作用:等待客户端完成握手,防止恶意半连接攻击
  2. 全连接队列(ESTABLISHED 就绪队列)
    • 存放 三次握手完全完成、状态变为 ESTABLISHED 的连接
    • 这就是 backlog 参数控制的队列,accept() 只从这个队列取连接
    • backlog = 全连接队列最大容量(历史/内核版本细节略有差异,核心含义:就绪连接上限)
    • 队列满时:新完成握手的连接会被丢弃 / 延迟处理,导致客户端连接失败、卡顿
核心错误理解

❌ backlog = 总连接上限 / 半连接队列上限
✅ backlog = 全连接就绪队列的最大长度

3. 复刻实现细节
3.1 数据结构
// 加入监听Socket控制块
typedef struct SocketSk {
    // ...原有字段
    int backlog;
    struct SocketSk** accept_queue;  // 全连接就绪队列:存放已完成握手的新SocketSk指针
    int queue_size;
    int queue_capacity;
} SocketSk;
3.2 listen(backlog) 逻辑
int listen(int fd, int backlog) {
    SocketSk* sk = g_fd_table_get_socket(fd);
    if (!sk || sk->type != SOCK_STREAM || sk->state != SOCK_BOUND) {
        return -1; // 错误
    }
    sk->backlog = backlog;
    sk->accept_queue = malloc(sizeof(SocketSk*) * backlog);
    sk->queue_size = 0;
    sk->queue_capacity = backlog;
    sk->state = SOCK_LISTENING;
    return 0; // 成功
}
3.3 三次握手完成回调

当底层TCP完成三次握手,新连接进入 ESTABLISHED 状态时:

void on_tcp_handshake_complete(SocketSk* listen_sk, SocketSk* new_sk) {
    // 检查全连接队列是否已满
    if (listen_sk->queue_size < listen_sk->backlog) {
        listen_sk->accept_queue[listen_sk->queue_size++] = new_sk;
        wait_queue_wakeup(&listen_sk->wait_queue);  // 唤醒阻塞的 accept()
    } else {
        // 队列满:拒绝/延迟接入新连接
        // 实际实现中可能发送RST或等待
    }
}
3.4 accept() 逻辑修改
int accept(int fd) {
    SocketSk* sk = g_fd_table_get_socket(fd);
    if (!sk || sk->state != SOCK_LISTENING) {
        return -1;
    }
    
    // 阻塞:直到全连接队列非空
    while (sk->queue_size == 0) {
        wait_queue_block(&sk->wait_queue);
    }
    wait_queue_clear(&sk->wait_queue);

    // 取出队首就绪连接
    SocketSk* new_sk = sk->accept_queue[0];
    // 移动队列元素
    for (int i = 1; i < sk->queue_size; i++) {
        sk->accept_queue[i-1] = sk->accept_queue[i];
    }
    sk->queue_size--;
    
    int new_fd = g_fd_table_alloc_fd();
    g_fd_table_entries[new_fd] = new_sk;
    return new_fd;
}
3.5 关键问题
  • 队列耗尽 → 新连接无法进入就绪队列,客户端连接超时/失败
  • 真实内核:半连接队列另有参数配置,学习版只重点实现 accept_queue 全连接队列
  • 阻塞逻辑:等待队列只在 accept_queue 为空时休眠,有就绪连接直接取出

3.6 Linux 内核 backlog 实现细节

在实际的 Linux 内核中,backlog 参数的行为比我们简化的实现要复杂:

  1. 内核参数影响

    • /proc/sys/net/core/somaxconn:系统级全连接队列最大长度限制
    • /proc/sys/net/ipv4/tcp_max_syn_backlog:半连接队列(SYN队列)大小
    • 实际 backlog = min(用户传入的 backlog, somaxconn)
  2. 队列满时的行为

    • 当全连接队列满时,内核会忽略新到达的 ACK(第三次握手)
    • 客户端会重传 SYN+ACK,直到超时或队列有空位
    • 这可能导致客户端连接延迟或失败
  3. 性能调优建议

    // 生产环境推荐设置
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    int backlog = 128;  // 根据并发连接数调整
    
    // 获取系统 somaxconn 值
    int somaxconn;
    FILE* f = fopen("/proc/sys/net/core/somaxconn", "r");
    if (f) {
        fscanf(f, "%d", &somaxconn);
        fclose(f);
        backlog = (backlog > somaxconn) ? somaxconn : backlog;
    }
    
    listen(listen_fd, backlog);
    

二、shutdown 半关闭机制

1. TCP 全双工本质

TCP 是两条独立单向数据流:

  • 上行:客户端 → 服务端
  • 下行:服务端 → 客户端
    两条通道可以独立关闭,不必一起销毁整个连接 fd
  • close()全关闭,销毁 fd、释放Socket对象、可能直接发 RST 重置连接,丢弃未读/未发数据
  • shutdown()单向关闭(半关闭),修改数据流状态、发送 FIN 报文、走四次挥手,保留fd和剩余读通道
shutdown 三种模式(POSIX标准)
  1. SHUT_RD:关闭读通道,后续收到的数据直接丢弃,recv 返回 EOF
  2. SHUT_WR:关闭写通道,发送 FIN,不再发送新数据,但仍然可以继续 recv 读取剩余下行数据(核心半关闭用法)
  3. SHUT_RDWR:等价于全关闭,同时关闭读写通道
2. 四次挥手 & 半关闭状态流转
  • 客户端 shutdown(SHUT_WR)
    1. 发送 FIN 报文 → TCP 状态进入 FIN_WAIT_1
    2. 服务端回复 ACK → 客户端进入 FIN_WAIT_2,仍可读
    3. 服务端读完剩余数据后发送 FIN → 客户端回复 ACK,进入 TIME_WAIT
    4. 等待 2MSL 后彻底释放端口,防止延迟报文干扰新连接
    • 服务端此时状态:CLOSE_WAIT,读完剩余数据后完成关闭
  • 核心价值:保证缓冲区残留数据全部收发完毕,实现优雅断开,避免丢包
3. Socket控制块新增状态标记
// Socket控制块新增状态标记
typedef struct SocketSk {
    // ...原有字段
    int write_closed;  // SHUT_WR 半关闭标记 (0=false, 1=true)
    int read_closed;   // SHUT_RD 半关闭标记 (0=false, 1=true)
    // 其他字段...
} SocketSk;
4. shutdown API 实现
// 模式常量
#define SHUT_RD   0
#define SHUT_WR   1
#define SHUT_RDWR 2

int shutdown(int fd, int how) {
    SocketSk* sk = g_fd_table_get_socket(fd);
    if (!sk || sk->type != SOCK_STREAM) {
        return -1; // 错误
    }

    switch (how) {
        case SHUT_RD:
            sk->read_closed = 1;
            // 后续recv直接返回EOF,丢弃新入数据
            // 通知底层TCP丢弃接收缓冲区数据
            break;
        case SHUT_WR:
            sk->write_closed = 1;
            // 通知底层TCP发送FIN,停止发送新数据,启动四次挥手流程
            // 仍可正常recv读取剩余数据
            tcp_send_fin(sk);
            break;
        case SHUT_RDWR:
            sk->read_closed = 1;
            sk->write_closed = 1;
            // 同时关闭读写通道
            tcp_send_fin(sk);
            break;
        default:
            return -1; // 无效参数
    }
    return 0; // 成功
}
5. 配套 recv/send 逻辑修改
  • write_closed=True:send() 直接报错,禁止发送新数据
  • read_closed=True:recv() 立刻返回空/EOF
  • write_closed=True, read_closed=False:仍然正常 recv 读取剩余数据(半关闭核心语义)
  • close():最终回收fd前,确认已走完 TIME_WAIT 延迟、数据收发完毕
6. 典型半关闭使用场景

HTTP/1.0 短连接、文件传输:
客户端发送完文件数据后 shutdown(SHUT_WR),告知服务端数据发完,等待服务端读完并关闭连接,防止数据截断。


三、recv_exact / send_all:TCP 半包读写原语封装

1. 问题根源:TCP 是无边界字节流

原生 recv() / send() 行为:

  • recv(buf, n)不一定读取 n 字节,返回当前内核缓冲区可用字节(可能是1字节、几十字节、半包)
  • send(buf)不一定写完全部数据,内核发送缓冲区满时只会写入一部分字节,返回实际写入长度
  • 这就是 TCP半包问题:不是粘包(上层协议问题),而是 系统调用本身不保证完整读写
    • ✅ recv_exact / send_all:Socket层原语封装,解决半包读写
    • ✅ 上层应用再基于此原语做 长度头/分隔符 消息拆包(真正解决粘包)
    • ❌ 不要把消息协议硬编码进Socket核心结构

2. recv_exact 实现:读取精确字节数

ssize_t recv_exact(int fd, void* buf, size_t total_len) {
    SocketSk* sk = g_fd_table_get_socket(fd);
    if (!sk || sk->read_closed) {
        return 0; // 连接已关闭或读通道关闭
    }
    
    char* buffer = (char*)buf;
    size_t received = 0;
    
    while (received < total_len) {
        ssize_t n = recv(fd, buffer + received, total_len - received, 0);
        if (n <= 0) {
            // EOF/连接关闭或错误
            if (n == 0) {
                // 连接正常关闭
                return received; // 返回已接收的字节数
            }
            // 处理EINTR等错误
            if (errno == EINTR) {
                continue; // 被信号中断,重试
            }
            return -1; // 其他错误
        }
        received += n;
    }
    return received; // 成功读取指定字节数
}
  • 循环调用基础 recv,直到凑够指定字节
  • 处理 EINTR 信号中断重试、EOF 半关闭结束
  • 适合定长消息头读取(例如4字节长度头)

3. send_all 实现:发送全部数据

ssize_t send_all(int fd, const void* buf, size_t len) {
    SocketSk* sk = g_fd_table_get_socket(fd);
    if (!sk || sk->write_closed) {
        return 0; // 连接已关闭或写通道关闭
    }
    
    const char* data = (const char*)buf;
    size_t total_sent = 0;
    
    while (total_sent < len) {
        ssize_t n = send(fd, data + total_sent, len - total_sent, 0);
        if (n <= 0) {
            // 错误处理
            if (n == 0) {
                // 对端关闭连接
                return total_sent; // 返回已发送的字节数
            }
            // 处理EINTR、EAGAIN/EWOULDBLOCK等错误
            if (errno == EINTR) {
                continue; // 被信号中断,重试
            }
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                // 非阻塞模式下缓冲区满,需要等待可写
                // 实际实现中应使用select/poll/epoll等待
                continue;
            }
            return -1; // 其他错误
        }
        total_sent += n;
    }
    return total_sent; // 成功发送所有数据
}
  • 循环发送,直到全部数据写入内核发送缓冲区
  • 适配阻塞/非阻塞流控场景(非阻塞版需要处理 EAGAIN 重试)
  • 保证应用层完整消息下发,避免消息截断

4. 关键区分

  1. 半包(系统调用层面):单次 recv/send 无法读写完整消息 → 由 recv_exact/send_all 解决(Socket层原语)
  2. 粘包(应用协议层面):多条应用消息合并在TCP流中 → 由应用层协议(4字节长度头等)解决,不属于Socket内核层

四、整体测试验证方案

1. backlog 全连接队列测试

  • 设置 backlog=2,并发发起3个TCP连接
  • 验证前2个进入 accept_queue,第3个无法立刻被 accept,观察队列满行为
  • 逐步调用 accept 取出连接,验证队列消耗、等待队列唤醒逻辑

2. shutdown 半关闭测试

  1. 客户端发送消息后 shutdown(SHUT_WR)
  2. 继续 recv 读取服务端剩余回复
  3. 验证可以正常读完剩余数据,不会直接断开
  4. 对比直接 close(),观察是否丢失尾部数据

3. recv_exact 半包测试

  • 发送一条长消息(例如 1024 字节),分段写入Socket缓冲区
  • 使用 recv_exact 读取完整长度,验证最终数据完整性,确认半包被正确补齐

五、避坑指南

  1. backlog 不要设置过大:过大全连接队列会占用大量内存,易遭受连接洪水攻击
  2. 半关闭不等于直接关闭 fd:必须保留 fd 直到读完剩余数据,不要立刻调用 close
  3. recv_exact 不要无限阻塞:后续非阻塞版本要增加超时/EAGAIN判断,防止死锁
  4. TIME_WAIT 必须绑定定时器:防止端口被长期占用,无法快速重启服务端
  5. 同步底层TCP状态和SocketSk标记:保证 write_closed/read_closed 和真实TCP状态一致
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐