【Linux之旅】Linux TCP Socket 编程实战:从单连接到线程池,构建高并发服务端

请君浏览

    • 前言
    • [一、TCP API 详解------服务端的五步套路](#一、TCP API 详解——服务端的五步套路)
    • [二、V1:TCP Echo 服务器------发现单连接瓶颈](#二、V1:TCP Echo 服务器——发现单连接瓶颈)
      • [2.1 基础框架](#2.1 基础框架)
      • [2.2 TCP 客户端](#2.2 TCP 客户端)
      • [2.3 V1 的致命缺陷:只能服务一个客户端](#2.3 V1 的致命缺陷:只能服务一个客户端)
    • [三、V2:多进程版本------用 fork 解耦 accept 和 Service](#三、V2:多进程版本——用 fork 解耦 accept 和 Service)
    • 四、V3:多线程版本------更轻量的并发模型
    • 五、V3-1:远程命令执行------给多线程加点业务
    • 六、V4:线程池版本------从"每连接一线程"到"任务队列"
    • 七、常见问题与避坑指南
      • [7.1 服务端重启后 bind: Address already in use](#7.1 服务端重启后 bind: Address already in use)
      • [7.2 子进程忘记关闭监听 fd](#7.2 子进程忘记关闭监听 fd)
      • [7.3 read 返回 0 不是错误](#7.3 read 返回 0 不是错误)
      • [7.4 多线程下 ThreadData 用栈变量](#7.4 多线程下 ThreadData 用栈变量)
      • [7.5 listen 的 backlog 设太小](#7.5 listen 的 backlog 设太小)
    • [八、多进程版本的补充------SIGCHLD 信号处理](#八、多进程版本的补充——SIGCHLD 信号处理)
    • [九、V1~V4 性能对比与选型建议](#九、V1~V4 性能对比与选型建议)
      • [9.1 性能维度对比](#9.1 性能维度对比)
      • [9.2 一个实际的计算](#9.2 一个实际的计算)
      • [9.3 选型决策树](#9.3 选型决策树)
    • [十、TCP 连接的生命周期与调试工具](#十、TCP 连接的生命周期与调试工具)
      • [10.1 一条 TCP 连接的三个阶段](#10.1 一条 TCP 连接的三个阶段)
      • [10.2 ss 和 netstat------查看 TCP 连接状态](#10.2 ss 和 netstat——查看 TCP 连接状态)
      • [10.3 tcpdump------抓包验证 TCP 握手](#10.3 tcpdump——抓包验证 TCP 握手)
      • [10.4 常见 TCP 状态异常排查](#10.4 常见 TCP 状态异常排查)
    • 十一、优雅关闭与生产化改造
      • [11.1 信号驱动的优雅退出](#11.1 信号驱动的优雅退出)
      • [11.2 有界队列------防止内存耗尽](#11.2 有界队列——防止内存耗尽)
    • 总结
    • 尾声

前言

回顾:Linux UDP Socket 编程实战:从 Echo 服务器到多线程聊天室

上一篇我们掌握了 UDP 编程------无连接、直发直收,简单但不可靠。本篇进入 TCP Socket 编程,TCP 的核心差异在于"连接管理":三次握手建立连接、四次挥手断开连接、确认重传保证可靠。多出来的复杂度换来了可靠的字节流传输,这是绝大多数互联网应用(HTTP、SSH、数据库协议)选择 TCP 的原因。

本文同样采用渐进式版本迭代 :V1 实现最简单的 TCP Echo 服务器(发现单连接问题)→ V2 引入多进程解决并发 → V3 升级为多线程 → V3-1 实现远程命令执行 → V4 引入线程池优雅管理资源。读完本文,你将彻底理解 TCP 服务器从单机玩具演进到高并发生产级架构的完整思维路径。

一、TCP API 详解------服务端的五步套路

TCP 相比 UDP 多了三个核心 API:listen()accept()connect()。我们先逐一拆解。

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>

// 创建套接字
int socket(int domain, int type, int protocol);
//   domain:   AF_INET(IPv4) / AF_INET6(IPv6)
//   type:     SOCK_STREAM(TCP/流式) / SOCK_DGRAM(UDP/数据报)
//   protocol: 传 0,由前两个参数自动决定
//   返回值:   成功返回文件描述符,失败返回 -1

// 绑定地址和端口
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//   sockfd:  socket() 返回的 fd
//   addr:    本地地址(IP+端口),强转为 sockaddr*
//   addrlen: sizeof(sockaddr_in)
//   返回值:  成功 0,失败 -1(EADDRINUSE 表示端口被占用)

socket() 和 bind() 与 UDP 完全一致,唯一的区别:type 传 SOCK_STREAM 而非 SOCK_DGRAM

以下三个是 TCP 独有的 API

cpp 复制代码
// 开始监听(将 socket 从主动模式切换为被动模式)
int listen(int sockfd, int backlog);
//   sockfd:  已 bind 的 socket fd
//   backlog: 全连接队列的最大长度(已三次握手、等待 accept 的连接数),通常设为 5~128
//   返回值:   成功 0,失败 -1

// 从全连接队列取出一个客户端连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//   sockfd:  处于 LISTEN 状态的 socket(监听 fd)
//   addr:    输出参数,保存客户端地址信息(可传 NULL)
//   addrlen: 输入输出参数,传入 addr 缓冲区大小,传出实际地址大小
//   返回值:   成功返回新的文件描述符(用于与该客户端通信),失败返回 -1
//   关键点:   如果全连接队列为空,accept() 会阻塞等待

// 客户端发起连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//   sockfd:  客户端 socket() 返回的 fd
//   addr:    目标服务器地址
//   addrlen: sizeof(sockaddr_in)
//   返回值:   成功 0,失败 -1(ECONNREFUSED=端口无人监听)
//   connect() 会触发三次握手,客户端首次调用时 OS 自动 bind 随机端口

核心理解:两个文件描述符

fd 类型 来源 生命周期 用途
监听 fd socket() 创建,bind() + listen() 激活 服务启动到关闭 只用于 accept(),不用于通信
通信 fd accept() 返回 每个客户端独立,用完 close() 用于 read()/write()

饭店拉客的类比: listen() 后的监听 fd 就像饭店门口的迎宾员------他只负责把客人引进来(accept),自己不和客人吃饭。accept() 返回的通信 fd 才是带客人上桌的服务员------一个服务员服务一桌客人,服务完就离开(close)。

二、V1:TCP Echo 服务器------发现单连接瓶颈

2.1 基础框架

cpp 复制代码
// TcpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdio>
#include "nocopy.hpp"
#include "Comm.hpp"

// 简单日志宏:直接打印到 stderr
#define LOG_FATAL(fmt, ...)   fprintf(stderr, "[FATAL] " fmt "\n", ##__VA_ARGS__)
#define LOG_ERROR(fmt, ...)   fprintf(stderr, "[ERROR] " fmt "\n", ##__VA_ARGS__)
#define LOG_WARNING(fmt, ...) fprintf(stderr, "[WARNING] " fmt "\n", ##__VA_ARGS__)
#define LOG_INFO(fmt, ...)    fprintf(stderr, "[INFO] " fmt "\n", ##__VA_ARGS__)
#define LOG_DEBUG(fmt, ...)   fprintf(stderr, "[DEBUG] " fmt "\n", ##__VA_ARGS__)

const static int default_backlog = 6;

class TcpServer : public nocopy
{
public:
    TcpServer(uint16_t port) : _port(port), _isrunning(false) {}

    void Init()
    {
        // ① 创建 socket
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            LOG_FATAL("create socket error, errno: %d, error: %s",
                      errno, strerror(errno));
            exit(Fatal);
        }
        // setsockopt: 允许端口复用,重启服务器时避免 TIME_WAIT 导致 bind 失败
        int opt = 1;
        setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
        LOG_DEBUG("create socket success, sockfd: %d", _listensock);

        // ② bind 绑定本地信息
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = htonl(INADDR_ANY);
        if (bind(_listensock, (struct sockaddr*)&local, sizeof(local)) != 0)
        {
            LOG_FATAL("bind error");
            exit(Bind_Err);
        }
        LOG_DEBUG("bind socket success, sockfd: %d", _listensock);

        // ③ listen:TCP 特有,将 socket 设为监听状态
        if (listen(_listensock, default_backlog) != 0)
        {
            LOG_FATAL("listen error");
            exit(Listen_Err);
        }
        LOG_DEBUG("listen socket success, sockfd: %d", _listensock);
    }

    // TCP 是全双工的------同一个 fd 既可读也可写
    void Service(int sockfd)
    {
        char buffer[1024];
        while (true)
        {
            ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
            if (n > 0)
            {
                buffer[n] = 0;
                std::cout << "client say# " << buffer << std::endl;
                std::string echo = "server echo# ";
                echo += buffer;
                write(sockfd, echo.c_str(), echo.size());
            }
            else if (n == 0)  // read 返回 0 = 对端关闭连接
            {
                LOG_INFO("client quit...");
                break;
            }
            else
            {
                LOG_ERROR("read error");
                break;
            }
        }
    }

    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // ④ accept:阻塞等待客户端连接,返回新 fd
            int sockfd = accept(_listensock, (struct sockaddr*)&peer, &len);
            if (sockfd < 0)
            {
                LOG_WARNING("accept error");
                continue;
            }
            LOG_DEBUG("accept success, new sockfd: %d", sockfd);
            // ⑤ 提供服务
            Service(sockfd);
            close(sockfd);  // 服务完关闭通信 fd
        }
    }

    ~TcpServer() {}
private:
    uint16_t _port;
    int _listensock;
    bool _isrunning;
};

Start() 主循环拆解:

步骤 代码 做了什么 关键点
④ accept accept(_listensock, &peer, &len) 从全连接队列取连接,返回新 fd 阻塞等待;peer 输出客户端地址
⑤ Service Service(sockfd) 在一个循环里反复 read/write 这里阻塞了回到 accept 的路径!
⑥ close close(sockfd) 释放通信 fd 只有 Service 退出才会执行

2.2 TCP 客户端

cpp 复制代码
![QQ20260528-130845](C:\Users\hou\Pictures\博客\Linux\20\QQ20260528-130845.png)// TcpClient.cc
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

void Usage(const std::string &process)
{
    std::cout << "Usage: " << process << " server_ip server_port" << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3) { Usage(argv[0]); return 1; }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // ① 创建 socket(SOCK_STREAM = TCP)
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) { std::cerr << "socket error" << std::endl; return 1; }

    // ② connect:发起三次握手,首次调用时 OS 自动 bind 随机端口
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);

    int n = connect(sockfd, (struct sockaddr*)&server, sizeof(server));
    if (n < 0) { std::cerr << "connect error" << std::endl; return 2; }

    // ③ 连接成功后用 write/read 收发数据
    while (true)
    {
        std::string inbuffer;
        std::cout << "Please Enter# ";
        std::getline(std::cin, inbuffer);

        ssize_t n = write(sockfd, inbuffer.c_str(), inbuffer.size());
        if (n > 0)
        {
            char buffer[1024];
            ssize_t m = read(sockfd, buffer, sizeof(buffer) - 1);
            if (m > 0) { buffer[m] = 0; std::cout << "get echo -> " << buffer << std::endl; }
            else break;
        }
        else break;
    }
    close(sockfd);
    return 0;
}

客户端流程拆解:

步骤 代码 说明
① socket socket(AF_INET, SOCK_STREAM, 0) 与服务端一样
② connect connect(sockfd, &server, ...) 触发三次握手;OS 自动 bind 随机端口
③ IO write()/read() TCP 已建立连接,无需每次指定目标地址

与 UDP 客户端的关键区别:UDP 每次 sendto 都要带目标地址,TCP 的 write 不需要------因为连接建立后,内核已经记住了四元组 {源IP, 源端口, 目的IP, 目的端口}。

2.3 V1 的致命缺陷:只能服务一个客户端

启动第二个客户端连接服务器时,发现第二个客户端无法正常通信。原因在 Start() 中一目了然:

cpp 复制代码
while (_isrunning) {
    int sockfd = accept(...);   // 接受连接 A
    Service(sockfd);            // 陷入死循环 read/write,直到 A 断开
    close(sockfd);
    // 只有 A 断开后,才能回到 accept 接受连接 B
}

Service() 是一个 while(true) 死循环 ------在读-写循环中出不来。"accept → 服务 → 等断开 → accept → 服务"的串行模式,注定只能一次处理一个连接。要解决这个问题,必须在 accept 新连接后,将 Service 交给独立的执行流去处理。

三、V2:多进程版本------用 fork 解耦 accept 和 Service

思路:accept 拿到新连接后,fork() 一个子进程,让子进程去 Service(),父进程立刻回到 accept 等待下一个连接。

cpp 复制代码
void ProcessConnection(int sockfd, struct sockaddr_in &peer)
{
    pid_t id = fork();
    if (id < 0)
    {
        close(sockfd);
        return;
    }
    else if (id == 0)
    {
        // child
        close(_listensock);         // 子进程用不到监听 fd,必须关闭
        if (fork() > 0) exit(0);    // 创建孙子进程,儿子立即退出
        // 孙子进程------孤儿进程,被 init 领养,无需父进程 wait
        InetAddr addr(peer);
        LOG_INFO("process connection: %s:%d",
                 addr.Ip().c_str(), addr.Port());
        Service(sockfd);
        close(sockfd);
        exit(0);
    }
    else
    {
        // parent
        close(sockfd);              // 父进程用不到通信 fd,必须关闭
        waitpid(id, nullptr, 0);    // 等待儿子退出(很快,因为儿子 fork 后立即退出)
    }
}

关键设计决策:

设计 原因
子进程关闭 _listensock 子进程只负责通信,不应持有监听 fd------减少资源占用,避免误操作
父进程关闭 sockfd 通信 fd 已交给子进程,父进程不持有------引用计数减 1,等子进程 close 后真正释放
孙子进程处理 Service 儿子 fork() 后立即 exit(0),孙子变孤儿被 init 领养。这样父进程只需 waitpid 儿子(极短等待),不会阻塞主循环

多进程版本的代价: 每个客户端 fork 一个新进程,进程创建和销毁开销大;客户端断开后留下僵尸进程需要处理;进程间不共享地址空间,数据交换需要 IPC。

四、V3:多线程版本------更轻量的并发模型

线程共享进程地址空间,创建开销远小于进程。思路:accept 拿到新连接后,pthread_create 一个新线程执行 Service

cpp 复制代码
class ThreadData  // 向线程传递参数
{
public:
    ThreadData(int sockfd, struct sockaddr_in addr) : _sockfd(sockfd), _addr(addr) {}
    int _sockfd;
    InetAddr _addr;
};

// Service 改为 static,从 ThreadData 中取参数
static void Service(ThreadData &td)
{
    char buffer[1024];
    while (true)
    {
        ssize_t n = read(td._sockfd, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "client say# " << buffer << std::endl;
            std::string echo = "server echo# ";
            echo += buffer;
            write(td._sockfd, echo.c_str(), echo.size());
        }
        else if (n == 0)
        {
            LOG_INFO("client[%s:%d] quit...",
                     td._addr.Ip().c_str(), td._addr.Port());
            break;
        }
        else { break; }
    }
}

static void *threadExcute(void *args)
{
    pthread_detach(pthread_self());  // 线程分离,无需主线程 join
    ThreadData *td = static_cast<ThreadData *>(args);
    TcpServer::Service(*td);
    close(td->_sockfd);
    delete td;
    return nullptr;
}

void ProcessConnection(int sockfd, struct sockaddr_in &peer)
{
    InetAddr addr(peer);
    pthread_t tid;
    ThreadData *td = new ThreadData(sockfd, peer);  // new 保证堆上分配,线程结束前不被销毁
    pthread_create(&tid, nullptr, threadExcute, (void*)td);
}

线程版本的改进点:

对比项 多进程 V2 多线程 V3
创建开销 大(复制整个地址空间) 小(共享地址空间)
内存占用 每个进程独立内存 线程共享,仅独立的栈
fd 数量限制 每个进程独立 fd 表 共享 fd 表
数据共享 需要 IPC 直接访问全局变量(需同步)

五、V3-1:远程命令执行------给多线程加点业务

在 V3 的多线程框架基础上,替换 Service() 的内容,实现一个远程命令执行服务器(限定在白名单内的安全命令):

cpp 复制代码
// Command.hpp
#pragma once
#include <iostream>
#include <string>
#include <set>
#include <unistd.h>

class Command
{
public:
    Command(int sockfd) : _sockfd(sockfd)
    {
        // 安全白名单:只允许执行这些命令
        _safe_command.insert("ls");
        _safe_command.insert("pwd");
        _safe_command.insert("ls -l");
        _safe_command.insert("ll");
        _safe_command.insert("touch");
        _safe_command.insert("who");
        _safe_command.insert("whoami");
    }

    bool IsSafe(const std::string &command)
    {
        return _safe_command.find(command) != _safe_command.end();
    }

    std::string Execute(const std::string &command)
    {
        if (!IsSafe(command)) return "unsafe command";
        FILE *fp = popen(command.c_str(), "r");  // popen: 执行命令并捕获输出
        if (fp == nullptr) return std::string();
        char buffer[1024];
        std::string result;
        while (fgets(buffer, sizeof(buffer), fp))
            result += buffer;
        pclose(fp);
        return result;
    }

    std::string RecvCommand()
    {
        char line[1024];
        ssize_t n = recv(_sockfd, line, sizeof(line) - 1, 0);
        if (n > 0) { line[n] = 0; return line; }
        return std::string();
    }

    void SendCommand(std::string result)
    {
        if (result.empty()) result = "done";
        send(_sockfd, result.c_str(), result.size(), 0);
    }

    ~Command() {}
private:
    std::set<std::string> _safe_command;
    int _sockfd;
};

Service 改写------三行驱动整个命令执行流程:

cpp 复制代码
static void Service(ThreadData &td)
{
    while (true)
    {
        Command command(td._sockfd);
        std::string commandstr = command.RecvCommand();    // ① 收命令
        if (commandstr.empty()) return;
        std::string result = command.Execute(commandstr);  // ② 执行
        command.SendCommand(result);                        // ③ 回结果
    }
}

设计亮点: Command 类封装了命令的接收、安全检查、执行和结果返回。Service() 不需要知道命令怎么执行的,也不需要知道白名单里有什么------它只是"收→处理→回"的管道。这和 V2 DictServer 用回调解耦的思路一脉相承。

六、V4:线程池版本------从"每连接一线程"到"任务队列"

V3 在连接数激增时会暴露一个问题:每个连接创建一个线程,线程数无上限。引入线程池,核心改动只有一处------ProcessConnection

cpp 复制代码
void ProcessConnection(int sockfd, struct sockaddr_in &peer)
{
    using func_t = std::function<void()>;
    InetAddr addr(peer);
    // 将 Service 包装成任务,投递到线程池
    func_t func = std::bind(&TcpServer::Service, this, sockfd, addr);
    ThreadPool<func_t>::GetInstance()->Push(func);
    // 主线程立即返回 accept 循环,不等待 Service 完成
}

四版对比:

版本 并发模型 主循环等待 适用场景
V1 串行 accept 阻塞 + Service 死循环 Service 完成后才回到 accept 调试、演示
V2 多进程 fork 子进程 per 连接 waitpid 短暂等待(孙子进程) 需要强隔离,进程互相独立
V3 多线程 pthread_create per 连接 立即回到 accept 中小规模并发(< 1000 连接)
V4 线程池 任务投递 + 工作线程复用 立即回到 accept 大规模并发,线程数可控

七、常见问题与避坑指南

7.1 服务端重启后 bind: Address already in use

现象: 关闭服务器后立刻重启,bind() 失败报 EADDRINUSE

原因: TCP 主动关闭方进入 TIME_WAIT 状态(持续 2MSL ≈ 2 分钟),端口仍被占用。

解决:socket() 之后、bind() 之前调用 setsockopt 设置端口复用:

cpp 复制代码
int opt = 1;
setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

7.2 子进程忘记关闭监听 fd

现象: 客户端断开后,服务端端口仍然无法被新进程 bind

原因: fork() 后子进程继承了父进程的 _listensock。即使父进程 close,子进程仍持有引用,fd 的引用计数不为 0,端口不释放。

解决: 子进程中第一时间 close(_listensock)------用不到就别持有。

7.3 read 返回 0 不是错误

现象: 误把 read() 返回 0 当作接收了空数据,继续循环导致死循环。

原因: TCP 中 read() 返回 0 表示对端关闭了连接(发送了 FIN)。继续读只会不断返回 0。

解决: 检查 n == 0 的分支,立即 break 并 close 通信 fd:

cpp 复制代码
if (n == 0) {  // 对端关闭连接
    LOG_INFO("client quit");
    break;
}

7.4 多线程下 ThreadData 用栈变量

现象: 线程中读取 ThreadData 内容全是乱码。

原因: ProcessConnection 返回后,栈上的 ThreadData 已被销毁,但线程还在用这个地址。

解决: new 在堆上分配,由线程自行 delete

cpp 复制代码
ThreadData *td = new ThreadData(sockfd, peer);  // 堆上分配
pthread_create(&tid, nullptr, threadExcute, td);
// 在 threadExcute 中 delete td;

7.5 listen 的 backlog 设太小

现象: 高并发下客户端 connect 偶尔被拒绝(ECONNREFUSED)。

原因: backlog 限制了全连接队列大小。新连接完成三次握手后若队列满,直接丢弃或被拒绝。

解决:backlog 设为合理值(如 128 或 SOMAXCONN)。内核实际值会受 net.core.somaxconn 参数上限影响。


八、多进程版本的补充------SIGCHLD 信号处理

V2 多进程版本中有一个隐含问题:如果服务端被 kill 或者异常退出,子进程会变成僵尸进程(defunct)。正规的做法是注册 SIGCHLD 信号处理器:

cpp 复制代码
#include <signal.h>
#include <sys/wait.h>

// SIGCHLD 信号处理器------子进程退出时内核发送此信号
void SigChildHandler(int signo)
{
    // waitpid(-1, ...) 回收任意子进程
    // WNOHANG: 非阻塞------如果有多个子进程同时退出,
    // 信号可能被合并,需要循环回收直到没有更多退出的子进程
    while (waitpid(-1, nullptr, WNOHANG) > 0)
        ;
}

int main()
{
    // 在主函数最开始注册信号处理
    signal(SIGCHLD, SigChildHandler);
    // ... 后续 socket/bind/listen/accept 流程不变
}

关键点:SIGCHLD 信号可能被合并。 如果两个子进程几乎同时退出,内核可能只发送一次 SIGCHLD。所以信号处理器中要用 while (waitpid(-1, ..., WNOHANG) > 0) 循环回收,而不是只调用一次 wait。这是 C++ 服务端面试中的高频考点。


九、V1~V4 性能对比与选型建议

理解每个版本的瓶颈,才能在工程中做出正确选择:

9.1 性能维度对比

版本 并发数上限 内存占用(per 连接) 创建/销毁开销 适合场景
V1 串行 1 极低 调试、演示、单客户端工具
V2 多进程 ~几千(受 PID 和内存限制) 高(独立地址空间,~几MB per 进程) 大(fork 复制页表) 需要进程级隔离、安全性优先
V3 多线程 几千几万 中(共享地址空间,~几MB per 线程栈) 中(内核线程创建) 中等并发、业务简单
V4 线程池 线程数固定,队列无界 → 受内存和 CPU 限制 低(线程数固定,任务在堆上) 极低(复用线程) 高并发生产环境

9.2 一个实际的计算

假设每个连接处理耗时 10ms(纯计算耗时),线程池有 4 个线程:

复制代码
单线程处理能力:1000ms / 10ms = 100 连接/秒
4 线程处理能力:400 连接/秒
如果客户端以 500 连接/秒的速度涌入:每秒积压 100 个任务

线程池不是魔法------它不增加 CPU 算力,只是避免线程创建/销毁的开销。如果业务处理本身慢(磁盘 IO、数据库查询),线程池的队列会越来越长,最终内存耗尽。解决方案:用 非阻塞 IO + epoll(下一篇 HTTP 服务器中会涉及)。

9.3 选型决策树

复制代码
是否需要严格的进程隔离(安全性第一)?
  ├─ 是 → V2 多进程(如 sshd:每个 ssh 会话一个独立进程)
  └─ 否 → 并发量多大?
           ├─ < 100 并发 → V3 多线程(简单直接)
           └─ > 1000 并发 → V4 线程池 + epoll(生产级)

十、TCP 连接的生命周期与调试工具

理解 TCP 连接从建立到关闭的完整生命周期,是排查网络问题的基本功。

10.1 一条 TCP 连接的三个阶段

复制代码
阶段一:建立连接(三次握手)
  CLIENT → [SYN, seq=x]        → SERVER (LISTEN)
  CLIENT ← [SYN+ACK, seq=y, ack=x+1] ← SERVER (SYN_RCVD)
  CLIENT → [ACK, ack=y+1]      → SERVER (ESTABLISHED)
  双方进入 ESTABLISHED 状态,可以开始收发数据

阶段二:数据传输
  CLIENT ⇄ write/read ⇄ SERVER
  数据双向流动,TCP 保证有序、可靠

阶段三:断开连接(四次挥手)
  CLIENT → [FIN, seq=u]        → SERVER
  CLIENT ← [ACK, ack=u+1]      ← SERVER (CLOSE_WAIT)
  ... 服务器处理完剩余数据 ...
  CLIENT ← [FIN, seq=v]        ← SERVER (LAST_ACK)
  CLIENT → [ACK, ack=v+1]      → SERVER
  主动关闭方进入 TIME_WAIT(等待 2MSL≈2分钟)

10.2 ss 和 netstat------查看 TCP 连接状态

bash 复制代码
# 查看所有 TCP 连接及其状态
$ ss -tanp | grep 9999
State      Recv-Q Send-Q Local Address:Port   Peer Address:Port  Process
LISTEN     0      5      0.0.0.0:9999         0.0.0.0:*          ./tcp_server
ESTAB      0      0      192.168.1.10:9999     192.168.1.20:55123 ./tcp_server
TIME-WAIT  0      0      192.168.1.10:9999     192.168.1.20:55124 -

# 查看每个状态的连接数量
$ ss -s
Total: 156
TCP:   45 (estab 12, closed 20, orphaned 0, timewait 13)

如果你看到大量 TIME-WAIT 状态的连接,说明你的程序频繁创建和关闭 TCP 连接------这是 HTTP/1.0 短连接式的设计的自然结果,不是 bug。但如果想减少 TIME-WAIT,要么用长连接(keep-alive),要么用 SO_REUSEADDR(我们已经在 V1 的 Init 中做了)。

10.3 tcpdump------抓包验证 TCP 握手

bash 复制代码
# 抓取本地回环 9999 端口的 TCP 包,显示详细信息
$ sudo tcpdump -i lo port 9999 -S -vvv

# 启动客户端后观察输出:
# 1. IP localhost.55123 > localhost.9999: Flags [S], seq 123456789    ← SYN
# 2. IP localhost.9999 > localhost.55123: Flags [S.], seq 987654321, ack 123456790 ← SYN+ACK
# 3. IP localhost.55123 > localhost.9999: Flags [.], ack 987654322   ← ACK (握手完成)
# 4. IP localhost.55123 > localhost.9999: Flags [P.], seq ..., ack ..., length 5  ← 客户端发送 "hello"
# 5. IP localhost.9999 > localhost.55123: Flags [P.], seq ..., ack ..., length 13 ← 服务端回 "server echo# hello"
# 6. IP localhost.55123 > localhost.9999: Flags [F.], seq ...         ← 客户端发起挥手
# ...

tcpdump 是 TCP 协议学习的终极工具。没有什么比亲眼看到三次握手四次挥手的原始报文更直观的了。每一个网络开发者都应该至少用 tcpdump 完整地看过一次 TCP 连接的全过程。

10.4 常见 TCP 状态异常排查

问题 症状 tcpdump/ss 看到的 可能原因
客户端连不上 connect 返回 -1, ECONNREFUSED 服务端端口不在 LISTEN 状态 服务端没启动、端口号写错、防火墙拦截
连接超时 connect 很长时间后返回 -1, ETIMEDOUT 客户端发 SYN 后无响应 网络不通、防火墙丢弃 SYN
服务端假死 客户端能连上但无响应 连接状态 ESTABLISHED,但 Recv-Q 积压 服务端业务逻辑阻塞,read 被卡住
大量 CLOSE_WAIT --- ss 显示大量 CLOSE_WAIT 服务端收到 FIN 后没有调用 close()------程序忘了在 read 返回 0 后关闭 socket
大量 TIME_WAIT --- ss 显示大量 TIME_WAIT 每秒创建/销毁大量短连接,正常现象,用 SO_REUSEADDR 或长连接优化

十一、优雅关闭与生产化改造

11.1 信号驱动的优雅退出

目前 V1~V4 的 Start()while (_isrunning) 循环,但没有机制将 _isrunning 设为 false。注册信号处理实现 Ctrl+C 时的优雅关闭:

cpp 复制代码
#include <signal.h>

bool g_running = true;

void SigIntHandler(int signo) {
    g_running = false;  // 指示主循环退出,不强制 kill 当前连接
}

int main() {
    signal(SIGINT, SigIntHandler);   // Ctrl+C
    signal(SIGTERM, SigIntHandler);  // kill 命令

    TcpServer server(9999);
    server.Init();
    while (g_running) {
        int sockfd = accept(...);
        // ...
    }
    LOG_INFO("Server shutting down gracefully...");
}

"优雅"意味着完成当前连接的处理后才退出------不是在 read/write 中途被强制 kill,导致客户端收到不完整的响应。

11.2 有界队列------防止内存耗尽

V4 的线程池队列是无界的------不管积压多少任务,Push 都不会拒绝。10000/s 涌入时内存会爆:

cpp 复制代码
class BoundedThreadPool {
    size_t _max_size;
    bool Push(task_t task) {
        std::unique_lock<std::mutex> lock(_mutex);
        if (_tasks.size() >= _max_size) {
            return false;  // 拒绝新任务
        }
        _tasks.push(std::move(task));
        _cv.notify_one();
        return true;
    }
};
// 拒绝后返回 503 Service Unavailable,而不是让系统崩溃

有界队列 + 拒绝策略是生产级线程池的标配。"Fail Fast"------快速失败比慢慢耗尽资源然后全面崩溃要好得多。


总结

从 V1 到 V4 的演进,本质是将"accept 等待连接"和"Service 服务连接"这一对矛盾不断解耦的过程:

版本 并发模型 核心机制 瓶颈
V1 串行 单进程 accept → Service → close 串行 只能服务一个连接
V2 多进程 fork per 连接 孙子进程 + 孤儿进程回收 进程创建开销大
V3 多线程 pthread_create per 连接 ThreadData 传参 + detach 线程数无限增长
V4 线程池 任务投递 + 工作线程复用 std::bind 包装 Service 线程数可控,生产可用

动手试试

  1. 在 V3 多线程版本的 Service 中加入心跳检测 ------如果 30 秒内没有收到客户端任何数据,主动 close 连接(提示:使用 setsockopt 设置 SO_RCVTIMEO 读超时)。
  2. 在 V4 线程池版本的基础上,加入最大连接数限制 ------当活跃连接数超过阈值时,新的 accept 直接返回"服务器繁忙"消息然后 close(提示:维护一个全局 atomic<int> 计数器,accept 后 +1,线程退出前 -1)。

之后我们将深入 TCP 协议细节与底层机制,理解三次握手四次挥手的完整状态机、TIME_WAIT 的本质,以及如何用 tcpdump 和 Wireshark 抓包验证。

尾声

本章讲解就到此结束了,若有纰漏或不足之处欢迎大家在评论区留言或者私信,同时也欢迎各位一起探讨学习。感谢您的观看!

更多内容可见主页

相关推荐
AlfredZhao14 小时前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户97183563346620 小时前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪1 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5202 天前
Linux 11 动态监控指令top
linux
网络研究院2 天前
2026年网络安全
网络·安全·法律·法规·趋势·发展
酣大智2 天前
ARP代理--工作原理
运维·网络·arp·arp代理
treesforest2 天前
AI安全系统如何识别异常访问?IP风险识别正在成为关键能力
网络·人工智能·tcp/ip·安全·web安全
不会C语言的男孩2 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言