【Linux网络】一文吃透 TCP Socket 编程

🔥个人主页:Cx330🌸

❄️个人专栏:《C语言》《LeetCode刷题集》《数据结构-初阶》《C++知识分享》

《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔

《Git深度解析》:版本管理实战全解 《Qt 极境架构》

🌟心向往之行必能


🎥Cx330🌸的简介:


目录

前言:

[一、 TCP Socket 核心 API 深度剖析](#一、 TCP Socket 核心 API 深度剖析)

[📊 TCP 与 UDP 的核心差异对比](#📊 TCP 与 UDP 的核心差异对比)

[1. 创建套接字:socket()](#1. 创建套接字:socket())

[2. 绑定地址与端口:bind()](#2. 绑定地址与端口:bind())

[3. 开始监听:listen()](#3. 开始监听:listen())

[4. 接受连接:accept()](#4. 接受连接:accept())

[5. 客户端发起连接:connect()](#5. 客户端发起连接:connect())

[二. 项目前置工具组件解析](#二. 项目前置工具组件解析)

[2.1 网络地址封装:InetAddr.hpp](#2.1 网络地址封装:InetAddr.hpp)

[2.2 线程安全保障:Mutex.hpp 与 LockGuard](#2.2 线程安全保障:Mutex.hpp 与 LockGuard)

[2.3 日志系统:Logger.hpp](#2.3 日志系统:Logger.hpp)

[三. TCP EchoServer 的演进式实现](#三. TCP EchoServer 的演进式实现)

[3.1 三版本完整代码](#3.1 三版本完整代码)

[3.2 V1 版本:单进程基础版 EchoServer](#3.2 V1 版本:单进程基础版 EchoServer)

[3.2 V2 版本:多进程并发版 EchoServer](#3.2 V2 版本:多进程并发版 EchoServer)

[3.3 V3 版本:多线程高并发版 EchoServer](#3.3 V3 版本:多线程高并发版 EchoServer)

[四、 多进程 vs 多线程高并发方案终极横向对比](#四、 多进程 vs 多线程高并发方案终极横向对比)

[🌟 结语](#🌟 结语)


前言:

在网络编程的浩瀚海洋中,TCP Socket 编程 是每个后端开发、系统开发绕不开的"定海神针"。很多初学者常常卡在"为什么 listen之后还要 accept?"、"多进程下怎么优雅地处理僵尸进程?"等核心痛点上。

今天,博主完全拆解 Linux 环境下的 TCP 套接字编程。我们不仅会深度剖析核心 API(如 socketbindlistenacceptconnect),还将手写实现从单进程、多进程、多线程,直到高并发线程池的 5 个版本服务器演进。废话不多说,直接上干货!


一、 TCP Socket 核心 API 深度剖析

在进行代码实战前,我们必须彻底吃透 Linux 系统提供的底层网络 API(定义在 <sys/socket.h> 中)。

在深入具体的 API 之前,我们首先需要搞清楚 TCP 与 UDP 在传输层协议设计上的核心差异,这直接决定了为什么 TCP 的套接字编程逻辑要比 UDP 复杂得多:

📊 TCP 与 UDP 的核心差异对比

特性维度 TCP (传输控制协议) UDP (用户数据报协议)
连接性 面向连接(通信前必须通过"三次握手"建立连接) 无连接(发送数据前不需要建立连接,直接发送)
可靠性 可靠传输(通过确认应答、超时重传、去重、排队等机制保证不丢包、不乱序) 不可靠传输(尽最大努力交付,不保证按序到达,可能会丢包)
传输形式 面向字节流(数据没有边界,像流水一样,需要应用层自己处理粘包问题) 面向数据报(保留了应用层报文边界,一次发送对应一次接收)
通信对象 一对一双工通信(一个连接只对应一个客户端和一个服务器端) 支持多播/广播(支持一对一、一对多、多对一、多对多的交互)
控制机制 有流量控制与拥塞控制(会根据网络和接收方状况动态调节发送速率) 无任何控制机制(即使网络拥塞,发送端也会以恒定速率继续发送)
首部开销 较大(固定首部 20 字节,可能含有可选项,最长可达 60 字节) 极小(固定首部 8 字节)
适用场景 网页浏览 (HTTP/HTTPS)、文件传输 (FTP)、电子邮件 (SMTP) 视频会议、直播、实时游戏、域名解析 (DNS)、网络通话 (VoIP)

1. 创建套接字:socket()

复制代码
int socket(int domain, int type, int protocol);
  • 作用 :打开一个网络通讯端口。成功时返回一个文件描述符(就像 open一样),失败时返回 -1

  • 参数解析

    • domain:对于 IPv4 网络通信,指定为 AF_INET

    • type:对于 TCP 协议,指定为 SOCK_STREAM(表示面向字节流的传输协议)。

    • protocol:指定为 0即可,系统会自动根据前两个参数匹配默认协议。

2. 绑定地址与端口:bind()

复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 作用 :将一个套接字 sockfd绑定到一个固定的网络地址和端口上。

  • 注意 :服务器的 IP 和端口通常是固定不变的,客户端需要据此发起连接。成功返回 0,失败返回 -1

3. 开始监听:listen()

复制代码
int listen(int sockfd, int backlog);
  • 作用 :将主动套接字(Active Socket)转换为被动监听套接字(Listening Socket),使得服务器准备好接受外来的连接请求。

  • 参数解析

    • sockfd:已被 bind绑定的套接字。

    • backlog:底层 TCP 半连接队列与全连接队列的长度限制(通常设为 5、10 或更高)。

  • 重要概念

    • 只有监听套接字才能调用 accept

    • listen的成功意味着 TCP 三次握手可以开始由操作系统底层自动完成。

4. 接受连接:accept()

复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 作用:从已完成连接队列(Completed Connection Queue)中获取一个已建立好的 TCP 连接。

  • 参数解析

    • sockfd:这是监听套接字(Listening Socket)

    • addr:输出型参数,用于获取客户端的 IP 和端口信息。

    • addrlen:输入输出型参数,代表地址结构体的长度。

  • 返回值 :成功时返回一个全新 的文件描述符,专门用于和该客户端进行数据收发(我们称之为服务套接字/通信套接字 Service Socket );失败返回 -1

💡 经典比喻(保安与服务员)

  • 监听套接字 (listen_fd) 就像是一家高档餐厅门口的迎宾保安,他的工作就是源源不断地吸引客人、把客人迎进门,但他自己绝不进入包间为客人点菜。

  • 服务套接字 (service_fd) 则是保安迎客入座后,专门分配给该桌客人的专属服务员 。后续所有的点单、送菜(数据读写 read/write)都由该服务员负责。

5. 客户端发起连接:connect()

复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 作用:客户端通过此接口向服务器发起 TCP 三次握手连接。

  • 参数解析addr传入目标服务器的 IP 和端口。成功返回 0,失败返回 -1

为了让大家对三次握手及 API 调用时机有直观感受,请看下方 TCP Socket 经典交互时序图:

复制代码
       服务器端 (Server)                     客户端 (Client)
      +------------------+                 +------------------+
      |     socket()     |                 |                  |
      +--------+---------+                 |                  |
               |                           |                  |
      +--------v---------+                 |                  |
      |      bind()      |                 |                  |
      +--------+---------+                 |                  |
               |                           |                  |
      +--------v---------+                 |                  |
      |     listen()     |                 |                  |
      +--------+---------+                 |                  |
               |                           |                  |
      +--------v---------+                 +--------v---------+
      |     accept()     | <-------------> |     socket()     |
      | (阻塞等待客户端)  |  三次握手建立   +--------+---------+
      +--------+---------+                 |        |
               |                           +--------v---------+
               | <------------------------ |    connect()     |
               |                           +--------+---------+
      +--------v---------+                          |
      |  read() / recv() | <------------------------+ (发送请求)
      +--------+---------+
               | (业务处理: 英译汉)
      +--------v---------+
      | write() / send() | ------------------------> +------------------+
      +--------+---------+                           |  read() / recv() |
               |                                     +--------+---------+
      +--------v---------+                                    |
      |     close()      | <----------------------------------+ (close() 关闭)
      +------------------+

二. 项目前置工具组件解析

本项目复用了工业级的基础组件,屏蔽底层细节的同时保证代码的健壮性与可维护性,核心组件如下:

2.1 网络地址封装:InetAddr.hpp

核心设计思路 :将sockaddr_in结构体、网络 / 主机字节序转换、地址判等、格式化输出等操作完全封装,屏蔽原生 socket 接口的底层细节,让上层代码无需关心字节序转换。

核心源码片段与解读

复制代码
// 网络转本地:recvfrom/accept后解析对端地址
InetAddr(struct sockaddr_in &addr): _net_addr(addr)
{
    // ntohs:16位网络字节序转主机字节序
    _port = ntohs(_net_addr.sin_port);
    // inet_ntoa:32位网络IP转点分十进制字符串
    _ip = inet_ntoa(_net_addr.sin_addr);
}

// 本地转网络:构建服务端绑定地址/客户端目标地址
InetAddr(uint16_t port, const std::string ip = "0.0.0.0")
    : _port(port), _ip(ip)
{
    _net_addr.sin_family = AF_INET;
    // htons:主机字节序转网络字节序
    _net_addr.sin_port = htons(_port);
    _net_addr.sin_addr.s_addr = inet_addr(_ip.c_str()); 
}

// 重载==运算符:IP+端口唯一标识一个网络端点
bool operator==(const InetAddr &who) const
{
    return (_ip == who._ip) && (_port == who._port);
}

2.2 线程安全保障:Mutex.hpp 与 LockGuard

核心设计思路:基于 RAII(资源获取即初始化)机制封装互斥锁,实现锁的自动生命周期管理,彻底避免手动加解锁导致的死锁、资源泄漏问题。

核心源码片段与解读

复制代码
// 互斥锁基础封装
class Mutex
{
public:
    Mutex() { pthread_mutex_init(&_lock, nullptr); }
    ~Mutex() { pthread_mutex_destroy(&_lock); }
    void Lock() { pthread_mutex_lock(&_lock); }
    void UnLock() { pthread_mutex_unlock(&_lock); }
    pthread_mutex_t* Origin() { return &_lock; }
private:
    pthread_mutex_t _lock;
};

// RAII锁守卫
class LockGuard
{
public:
    LockGuard(Mutex* lockptr) : _lockptr(lockptr)
    {
        _lockptr->Lock(); // 构造时立即加锁
    }
    ~LockGuard()
    {
        _lockptr->UnLock(); // 离开作用域析构时自动解锁
    }
private:
    Mutex* _lockptr;
};

解读:LockGuard 通过构造与析构函数实现了锁的自动管理,无论代码正常执行还是异常抛出,都能保证锁被正确释放,是多线程编程中线程安全的核心保障。

2.3 日志系统:Logger.hpp

核心设计思路:采用策略模式解耦日志的生成与输出,支持控制台 / 文件双输出策略,通过 RAII 机制实现日志的自动刷新,同时保证多线程环境下的输出安全。

复制代码
#ifndef __LOGGER_HPP
#define __LOGGER_HPP

#include <iostream>
#include <cstdio>
#include <string>
#include <ctime>
#include <filesystem> // C++17
#include "Mutex.hpp"
#include <fstream>
#include <sstream>
#include <memory>
#include <unistd.h>

namespace LogModule
{
    // 1.获取时间
    std::string GetTimeStamp()
    {
        time_t timestamp = time(nullptr);
        struct tm data_time;
        localtime_r(&timestamp, &data_time);

        char data_time_str[128];
        snprintf(data_time_str, sizeof(data_time_str), "%4d-%02d-%02d %02d:%02d:%02d",
                 data_time.tm_year + 1900, // 从1900开始记的
                 data_time.tm_mon + 1,     // 默认月份从0开始记的
                 data_time.tm_mday,
                 data_time.tm_hour,
                 data_time.tm_min,
                 data_time.tm_sec);
        return data_time_str;
    }

    enum LogLevel
    {
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };

    // 2.日志等级
    std::string LogLevel2String(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::DEBUG: 
            return "DEBUG";
        case LogLevel::INFO: 
            return "INFO";
        case LogLevel::WARNING: 
            return "WARNING";
        case LogLevel::ERROR: 
            return "ERROR";
        case LogLevel::FATAL: 
            return "FATAL";
        default:
            return "UNKNOWN";
        }
    }

    // 3.日志刷新
    // 基类:策略基类,设置刷新策略的
    class LogStrategy
    {
    public:
        virtual ~LogStrategy() = default;
        virtual void SyncLog(const std::string &logmessage) = 0;
    };

    // 子类:继承纯虚接口类
    // 策略1
    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        ConsoleLogStrategy(){}
        ~ConsoleLogStrategy(){}
        virtual void SyncLog(const std::string &logmessage) override
        {
            LockGuard lockguard(&_mutex); 
            std::cout<<logmessage<<std::endl;
        }
    private:
        Mutex _mutex;
    };

    static const std::string glogdir = "./log/";
    static const std::string glogfilename = "log.log";
    // 子类:继承纯虚接口类
    // 策略2
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const std::string &dir = glogdir,const std::string &filename = glogfilename)
            :_logdir(dir),_logfilename(filename)
        {
            // log/log.txt
            LockGuard lockguard(&_mutex);
            if(std::filesystem::exists(_logdir))
            {
                return;
            }
            else
            {
                try
                {
                    std::filesystem::create_directories(_logdir);
                }
                catch (const std::filesystem::filesystem_error &e)
                {
                    std::cerr<< e.what() <<std::endl;
                }
            }
        }
        ~FileLogStrategy()
        {}
        void SyncLog(const std::string &logmessage) override
        {
            std::string target = _logdir + _logfilename;
            std::ofstream out(target,std::ios::app); // 追加写入文件
            if(!out.is_open())
            {
                return;
            }

            // 方法1:
            // out.write(logmessage.c_str(), logmessage.size());
            // out.write("\n", 1); // 写入换行符

            // 方法2:
            // std::string line = logmessage + '\n';
            // out.write(line.c_str(), line.size());
            
            // 方法3:
            out << logmessage << '\n';

            out.close();
        }
    private:
        std::string _logdir;
        std::string _logfilename; // ./log/XXX.log
        Mutex _mutex;
    };

    // 真正要的日志类
    class Logger
    {
    public: 
        Logger()
        {
            UseConsoleLogStrategy();
        }
        ~Logger(){}
        // 显示器的刷新策略
        void UseConsoleLogStrategy()
        {
            _strategy = std::make_unique<ConsoleLogStrategy>();
        }
        // 文件的刷新策略
        void UseFileLogStrategy()
        {
            _strategy = std::make_unique<FileLogStrategy>();
        }

        // 内部类:一条日志
        // 目标是把一个类对象,变成一个string
        class LogMessage
        {
        public:
            LogMessage(LogLevel level,std::string &filename,int line,Logger &self)
                :_level(level),
                 _curr_time(GetTimeStamp()),
                 _pid(getpid()),
                 _filename(filename),
                 _line(line),
                 _logger(self)
            {   
                std::stringstream ss; 
                ss << "[" << _curr_time << "] "
                   << "[" << LogLevel2String(_level) << "] "
                   << "[" << _pid << "] "
                   << "[" << _filename << "] "
                   << "[" << _line << "] "
                   << "- ";
                _loginfo = ss.str();
            }

            template<typename T>
            LogMessage &operator << (const T &info) 
            {
                std::stringstream ss;
                ss << info;
                _loginfo += ss.str();
                return  *this;
            }

            ~LogMessage() // RAII风格的日志刷新
            {
                if(_logger._strategy)
                {
                    _logger._strategy->SyncLog(_loginfo);
                }
            }
        private:
            LogLevel _level;         // 日志等级
            std::string _curr_time;  // 当前时间
            pid_t _pid;              // 进程pid
            std::string _filename;   // 文件名
            int _line;               // 行号
            std::string _loginfo;    // 一条完整的日志
            Logger &_logger;         // 外部类的引用
        };

        // LogMessage 对象打印日志的时候,故意返回一个临时的 LogMessage对象
        // 为什么要返回临时内部类对象?
        LogMessage operator()(LogLevel level,std::string filename, int line)
        {
            return LogMessage(level,filename,line,*this);
        }
    private:
        std::unique_ptr<LogStrategy> _strategy; // 刷新日志的策略
    };
    
    Logger logger;
    // 使用宏,包装我们的日志打印过程,宏有一个特点,#define A B,B替换成A
    #define LOG(level) logger(level,__FILE__,__LINE__)

    // 动态调整日志策略
    #define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleLogStrategy()
    #define ENABLE_FILE_LOG_STRATEGY() logger.UseFileLogStrategy()
}

#endif

三. TCP EchoServer 的演进式实现

3.1 三版本完整代码

  • TcpEchoServer.hpp

    #ifndef __TCP__ECHOSERVER__HPP
    #define __TCP__ECHOSERVER__HPP

    #include
    #include
    #include <pthread.h>
    #include
    #include <string.h>
    #include <unistd.h>
    #include <sys/wait.h>

    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include "InetAddr.hpp"
    #include "Logger.hpp"
    using namespace LogModule;

    static const uint16_t gdefaultport = 8080;
    static const int gbacklog = 32;

    class TcpEchoServer
    {
    private:
    // sockfd: 既可以支持读,又可以支持写, TCP socket也是全双工的.
    // 该函数为具体的业务处理逻辑,扮演"服务员"的角色
    void Service(int sockfd, InetAddr client)
    {
    // 长连接服务:只要客户端不主动退出,服务器就一直为其提供服务
    while (true)
    {
    char inbuffer[1024];

    复制代码
              // 1. 读取数据 (类似于读取文件)
              int n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
              if(n > 0)
              {
                  // n > 0 表示成功读取到了 n 个字节的数据
                  inbuffer[n] = 0; // 将读取到的字节流转换为 C 风格的字符串
                  LOG(LogLevel::INFO) << client.StringAddress() << " say# " << inbuffer;
              }
              else if(n == 0)
              {
                  // 注意:n == 0 在 TCP 中是一个强信号,代表对端(客户端)已经关闭了连接 (EOF)
                  // 此时服务端也应该随之结束服务并退出循环
                  LOG(LogLevel::INFO) << client.StringAddress() << " close sockfd: " << sockfd << ", me too!";
                  break;
              }
              else 
              {
                  // n < 0 表示读取发生错误(如被信号中断等)
                  LOG(LogLevel::ERROR) << "read socket error";
                  break;
              }
    
              // 加工处理数据:体现 Echo (回显) 的核心业务逻辑
              std::string echo_string = "server echo# ";
              echo_string += inbuffer;
    
              // 2. 写回数据 (将加工后的字符串发回给客户端)
              int m = write(sockfd, echo_string.c_str(),  echo_string.size());
              if(m < 0)
              {
                  LOG(LogLevel::ERROR) << "write socket error";
                  break;
              }
          }
    
          // 极其重要:服务结束后,必须关闭该客户端对应的通信套接字,否则会导致系统文件描述符(fd)泄漏
          close(sockfd);
      }

    public:
    // 构造函数:初始化监听端口,并将监听套接字初始化为 -1(表示无效状态)
    TcpEchoServer(uint16_t port = gdefaultport): _port(port), _listensockfd(-1)
    {}

    复制代码
      // 服务器初始化:完成网络编程经典的"三板斧" (socket -> bind -> listen)
      void Init()
      {
          // 1. 创建套接字 (买手机)
          // AF_INET: IPv4 网络协议; SOCK_STREAM: 面向字节流的 TCP 协议
          _listensockfd = socket(AF_INET, SOCK_STREAM, 0); // 设置成0就可以了,系统会自动推导为 IPPROTO_TCP
          if(_listensockfd < 0)
          {
              LOG(LogLevel::FATAL) << "create socket error: " << _listensockfd;
              exit(2);
          }
          LOG(LogLevel::INFO) << "create socket success: " << _listensockfd;
    
          // 2.bind (办手机卡,绑定号码)
          // 我们这里是可以直接使用我们的InetAddr的,但是我们后面再用
          struct sockaddr_in local;
          socklen_t len = sizeof(local);
          memset(&local, 0, len); // 结构体清零,养成良好习惯
          local.sin_family = AF_INET;
          local.sin_port = htons(_port); // 主机字节序(Host) 转 网络字节序(Network) -> Short(16位端口)
          local.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有网卡的 IP 地址
    
          int n = bind(_listensockfd, (struct sockaddr*)&local, len);
          if(n < 0)
          {
              // 常见错误:端口被占用时会 bind 失败
              LOG(LogLevel::FATAL) << "bind error: " << _listensockfd;
              exit(3);
          }
          LOG(LogLevel::INFO) << "bind success: " << _listensockfd;
    
          // 3. 设置成监听 (开机并设置铃声,准备接听电话)
          // gbacklog (全连接队列长度) 决定了底层最多能缓存多少个未被 accept 的连接
          n = listen(_listensockfd, gbacklog);
          if(n < 0)
          {
              LOG(LogLevel::FATAL) << "listen error: " << _listensockfd;
              exit(3);
          }
          LOG(LogLevel::INFO) << "listen success: " << _listensockfd;
      }
    
      // 内部类:用于在多线程环境下向新线程传递参数
      // 因为 pthread_create 的回调函数只接受一个 void* 参数,所以需要封装为一个结构体/类
      class ThreadData
      {
      public:
          ThreadData(int sockfd, InetAddr addr, TcpEchoServer *owner)
                  : _sockfd(sockfd)
                  , _address(addr)
                  , _owner(owner)       
          {}
          ~ThreadData(){}
      public:
          int _sockfd;           // 与客户端通信的文件描述符
          InetAddr _address;     // 客户端的地址信息
          TcpEchoServer *_owner; // 指向服务器对象本身的指针,用于调用类内部的非静态成员函数 (如 Service)
      };
    
      // 静态的
      // 必须声明为 static:因为类的非静态成员函数默认带有 this 指针,会导致参数类型与 pthread_create 不匹配
      static void* threadRun(void* args)
      {
          // 分离:让线程执行结束后由操作系统自动回收资源,主线程无需阻塞等待 (join)
          pthread_detach(pthread_self());
          
          // 还原数据类型
          ThreadData *td = static_cast<ThreadData*>(args);
          
          // 利用传入的 _owner 指针,调用服务器对象的 Service 方法开始通信
          td->_owner->Service(td->_sockfd, td->_address);
          
          // 资源清理:由于 ThreadData 是在堆上 new 出来的,用完必须释放,防止内存泄漏
          delete td;
          return nullptr;
      }
    
      void Start()
      {
          // 多进程版本等待的最佳实践 (如果你启用多进程版本,建议打开此注释)
          // signal(SIGCHLD, SIG_IGN); // 忽略子进程退出信号,让内核自动回收僵尸进程
    
          while(true)
          {
              struct sockaddr_in clientaddr;
              socklen_t len = sizeof(clientaddr);
              
              // accept: 从全连接队列中取出一个已经建立好的连接
              // _listensockfd 负责拉客,返回的 sockfd 负责专门为这个客人服务
              int sockfd = accept(_listensockfd, (struct sockaddr *)&clientaddr, &len);
              if (sockfd < 0)
              {
                  // accept 失败不代表服务器崩溃,可能是信号打断,继续接待下一个客人即可
                  LOG(LogLevel::WARNING) << "accept error";
                  continue;
              }
              // 网络转主机:解析出客户端的 IP 和 端口
              InetAddr clientaddress(clientaddr);
              LOG(LogLevel::INFO) << "get a new link: " << clientaddress.StringAddress() << " sockfd: " << sockfd;
              
          
              // 处理连接, 进行IO通信
    
              // ==============================================================================
              // 【Version 2 -- 多线程版本 (当前生效)】
              // 优势:比多进程轻量,创建成本低,所有线程共享进程的文件描述符表。
              // ==============================================================================
              pthread_t tid;
              // 必须在堆上 new 对象:如果在栈上创建,当前循环结束时 td 会被销毁,新线程访问会引发野指针崩溃
              ThreadData* td = new ThreadData(sockfd, clientaddress, this);
              pthread_create(&tid, nullptr, threadRun, (void*)td);
              // 我们这里不要去等待, 而是在回调的函数里面使用线程分离
    
    
              // ==============================================================================
              // 【Version 1 -- 多进程版本 (已注释)】
              // 优势:进程间强隔离,一个子进程崩溃不会波及主进程和其他客户端。
              // 劣势:每次 fork 开销较大,不适合高并发海量连接。
              // ==============================================================================
              // pid_t pid = fork();
              // if(pid < 0)
              // {
              //     LOG(LogLevel::ERROR) << "fork error";
              //     close(sockfd);
              // }
              // else if(pid == 0)
              // {
              //     // 子进程,拷贝父进程的文件描述符表,从而和父进程看到同一批文件
              //     // 关闭自己不需要的文件fd (子进程只负责通信,不需要监听拉客)
              //     close(_listensockfd);
              //     // **************************************
              //     // 优雅处理僵尸进程的方案二:孙子进程法 (两次 fork)
              //     // if(fork() > 0)
              //     //     exit(0); // 子进程直接退出,让孙子进程变成孤儿进程,由系统 init/systemd 接管回收
              //     // // 孙子进程 -- 你去执行
              //     // Service(sockfd, clientaddress);
              //     // **************************************
              //     Service(sockfd, clientaddress);
              //     exit(0); // 业务处理完毕,子进程退出
              // }
              // else{}
              // // 父进程不需要这个:父进程已经把这个客户端交给了子进程处理,自己必须关闭,否则会导致 fd 耗尽
              // close(sockfd);
              // // 父进程
              // pid_t rid = waitpid(pid, nullptr, 0); // 如果这里阻塞等待,就又变成了串行。若用孙子进程法,这里会瞬间返回
    
    
              // ==============================================================================
              // 【Version 0 -- 单进程串行版本 (已注释)】
              // 致命缺陷:Service 内部是死循环,会导致主进程卡死在这里,永远无法执行下一次 accept 接待新客人。
              // 一般根本不会使用这种长连接的单进程写法。
              // ==============================================================================
              // Service(sockfd, clientaddress);
          }
      }
      ~TcpEchoServer(){}

    private:
    uint16_t _port; // 服务器绑定的端口号
    int _listensockfd; // 监听套接字 (拉客经理)
    };
    #endif

  • TcpEchoServer.cpp

    #include "TcpEchoServer.hpp"
    #include "Logger.hpp"
    #include
    #include // 引入 库以提供智能指针 std::unique_ptr 的支持

    // 打印程序的使用说明手册
    // 当用户在命令行启动程序时如果没有带上正确的参数,调用此函数进行提示
    void Usage(std::string procname)
    {
    // procname 接收的通常是 argv[0],即程序本身的运行路径和名称
    std::cout << "Usage: " << procname << " ServerPort" << std::endl;
    }

    // ./tcp_echo_server 8080
    int main(int argc, char *argv[])
    {
    // 1. 参数校验:检查命令行参数的个数
    // argc 必须等于 2。因为 argv[0] 是 "./tcp_echo_server" (程序名),argv[1] 是 "8080" (端口号)
    if(argc != 2)
    {
    Usage(argv[0]);
    exit(1); // 校验失败,异常退出程序,返回状态码 1
    }

    复制代码
      // 2. 初始化日志系统
      // 启用控制台日志输出策略,后续底层调用的 LOG() 宏都会将信息直接打印到显示器上
      ENABLE_CONSOLE_LOG_STRATEGY();
      
      // 3. 解析并转换端口号
      // argv[1] 拿到的 "8080" 只是一个字符串,需要通过 std::stoi (string to integer) 转换为 16位无符号整数
      uint16_t ServerPort = std::stoi(argv[1]);
      
      // 4. 实例化服务器对象 (现代 C++ 最佳实践:智能指针)
      // std::make_unique 是 C++14 引入的工厂函数,它安全地在堆上创建对象,并交由 unique_ptr 管理。
      // 好处 (RAII 机制):我们不需要手动去写 delete tsvr。当 main 函数运行结束时,
      // 智能指针会自动调用 TcpEchoServer 的析构函数并释放堆内存,彻底杜绝内存泄漏。
      std::unique_ptr<TcpEchoServer> tsvr = std::make_unique<TcpEchoServer>(ServerPort);
    
      // 5. 驱动服务器生命周期
      // 第一步:Init()。在底层执行 socket() -> bind() -> listen(),完成服务端网卡的"挂号待命"。
      tsvr->Init();
      
      // 第二步:Start()。进入死循环,开始 accept() 接待客户端,并根据配置(单进程/多进程/多线程)提供读写服务。
      tsvr->Start();
      
      return 0; // 程序正常结束

    }

3.2 V1 版本:单进程基础版 EchoServer

实现思路:遵循 TCP 服务端四步核心流程,单进程循环接收客户端连接,连接建立后同步处理客户端的读写请求,实现基础的回显功能。

核心源码片段

复制代码
// 核心业务:回显服务
void Service(int sockfd, InetAddr client)
{
    // TCP长连接循环读写
    while (true) 
    {
        char inbuffer[1024];
        // 1. 读取客户端数据
        int n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
        if(n > 0)
        {
            inbuffer[n] = 0;
            LOG(LogLevel::INFO) << client.StringAddress() << " say# " << inbuffer;
        }
        else if(n == 0) // 对端关闭连接
        {
            LOG(LogLevel::INFO) << client.StringAddress() << " close connection";
            break;
        }
        else // 读取异常
        {
            LOG(LogLevel::ERROR) << "read socket error";
            break;
        }
        // 2. 回写数据给客户端
        std::string echo_string = "server echo# ";
        echo_string += inbuffer;
        write(sockfd, echo_string.c_str(),  echo_string.size());
    }
    close(sockfd); // 关闭IO套接字
}

// 服务启动主循环
void Start()
{
    while(true)
    {
        struct sockaddr_in clientaddr;
        socklen_t len = sizeof(clientaddr);
        // 获取已建立的客户端连接
        int sockfd = accept(_listensockfd, (struct sockaddr *)&clientaddr, &len);
        if (sockfd < 0)
        {
            LOG(LogLevel::WARNING) << "accept error";
            continue;
        }
        InetAddr clientaddress(clientaddr);
        LOG(LogLevel::INFO) << "get a new link: " << clientaddress.StringAddress();
        
        // 同步处理业务,阻塞主循环
        Service(sockfd, clientaddress);
    }
}

源码核心解读

  • 监听套接字与 IO 套接字分离_listensockfd是监听套接字,仅用于接收客户端连接;accept返回的sockfd是专属 IO 套接字,用于和对应客户端的双向数据传输,这是 TCP 并发编程的核心设计。
  • read 返回值处理n>0为成功读取的字节数,n=0表示客户端关闭了连接,n<0表示读取异常,三种情况必须分别处理,否则会导致连接泄漏或程序崩溃。
  • 版本核心缺陷 :单进程串行处理,Service函数会阻塞主循环,同一时间只能处理一个客户端的连接与请求,无法支持多客户端并发访问。

3.2 V2 版本:多进程并发版 EchoServer

实现思路accept获取新连接后,通过**fork()**创建子进程处理客户端业务,父进程继续循环监听新连接,实现多客户端并发处理。

核心源码片段

复制代码
void Start()
{
    // 忽略子进程退出信号,自动回收僵尸进程,最佳实践
    signal(SIGCHLD, SIG_IGN);
    while(true)
    {
        struct sockaddr_in clientaddr;
        socklen_t len = sizeof(clientaddr);
        int sockfd = accept(_listensockfd, (struct sockaddr *)&clientaddr, &len);
        if (sockfd < 0)
        {
            LOG(LogLevel::WARNING) << "accept error";
            continue;
        }
        InetAddr clientaddress(clientaddr);
        LOG(LogLevel::INFO) << "get a new link: " << clientaddress.StringAddress();
        
        // 创建子进程处理业务
        pid_t pid = fork();
        if(pid == 0) // 子进程
        {
            close(_listensockfd); // 子进程不需要监听套接字,关闭避免资源泄漏
            Service(sockfd, clientaddress); // 处理客户端业务
            exit(0); // 业务处理完毕子进程退出
        }
        // 父进程
        close(sockfd); // 父进程不需要IO套接字,关闭文件描述符
    }
}

源码核心解读

  • 僵尸进程处理:通过signal(SIGCHLD, SIG_IGN)让操作系统自动回收退出的子进程,避免僵尸进程占用系统资源,是 Linux 多进程服务的最佳实践。也可以使用我们上面写的孙子进程的方式
  • 文件描述符管理:父子进程会共享文件描述符表,子进程必须关闭不需要的监听套接字,父进程必须关闭不需要的 IO 套接字,否则会导致文件描述符泄漏。
  • 版本优缺点:优点是实现简单,进程间地址空间隔离,单个客户端业务崩溃不会影响整个服务;缺点是进程创建 / 销毁开销大,并发量过高时系统资源占用严重。

3.3 V3 版本:多线程高并发版 EchoServer

实现思路accept获取新连接后,创建独立的子线程处理客户端业务,主线程继续监听新连接,通过线程分离实现自动资源回收,兼顾高并发与低资源开销。

核心源码片段

复制代码
// 线程参数传递类
class ThreadData
{
public:
    ThreadData(int sockfd, InetAddr addr, TcpEchoServer *owner)
            : _sockfd(sockfd), _address(addr), _owner(owner) {}
public:
    int _sockfd;
    InetAddr _address;
    TcpEchoServer *_owner; // 传递this指针,访问类内成员方法
};

// 线程入口函数必须为静态,无隐含this指针,适配pthread库接口
static void* threadRun(void* args)
{
    // 线程分离:线程退出时系统自动回收资源,无需主线程join
    pthread_detach(pthread_self());
    ThreadData *td = static_cast<ThreadData *>(args);
    // 调用业务处理方法
    td->_owner->Service(td->_sockfd, td->_address);
    // 释放堆上的参数对象
    delete td;
    close(td->_sockfd);
    return nullptr;
}

// 服务启动主循环
void Start()
{
    while(true)
    {
        struct sockaddr_in clientaddr;
        socklen_t len = sizeof(clientaddr);
        int sockfd = accept(_listensockfd, (struct sockaddr *)&clientaddr, &len);
        if (sockfd < 0)
        {
            LOG(LogLevel::WARNING) << "accept error";
            continue;
        }
        InetAddr clientaddress(clientaddr);
        LOG(LogLevel::INFO) << "get a new link: " << clientaddress.StringAddress() << " sockfd: " << sockfd;
        
        // 创建子线程处理业务
        pthread_t tid;
        ThreadData* td = new ThreadData(sockfd, clientaddress, this);
        pthread_create(&tid, nullptr, threadRun, (void*)td);
    }
}

源码核心解读

  • 静态线程入口函数 :pthread 库要求线程入口函数必须是**void* ()(void)**格式,类的普通成员函数隐含 this 指针,因此必须设为静态函数,通过参数传递 this 指针访问类内成员。
  • 线程分离机制pthread_detach(pthread_self())将线程设置为分离状态,线程退出时系统自动回收资源,无需主线程调用pthread_join阻塞等待,避免主线程阻塞。
  • 线程资源管理:ThreadData 对象在堆上创建,在线程业务处理完毕后释放,避免内存泄漏;多线程共享进程的文件描述符表,因此无需关闭监听套接字,仅需在业务处理完毕后关闭 IO 套接字。
  • 版本优势:线程创建 / 销毁开销远小于进程,支持更高的并发量,多线程共享进程地址空间,数据交互更便捷,是 TCP 高并发服务的主流基础方案。

四、 多进程 vs 多线程高并发方案终极横向对比

为了让读者在实际工程开发中能自如地进行技术选型,我们对目前研究过的并发架构进行一次最直观的横向评测:

维度对比 多进程并发(信号忽略版) 孙子进程法(双重 fork) 多线程并发(Detached 线程) 工业级线程池(ThreadPool)
创建/销毁开销 较大(每次连接创建/回收进程) 最大(每次连接执行两次 fork) 较小(每次连接创建/回收线程) 极小(初始化一次,后续无创建损耗)
内存占用 较高(独立的进程控制块及页表) 最高 较低(共享进程虚拟地址空间) 最低且恒定(线程上限完全可控)
资源隔离性 最高(一客一进程,崩溃绝不波及他人) 最高 较低(一个线程崩溃,整个进程覆灭) 较低
文件描述符拷贝 深拷贝(容易发生 fd 继承泛滥泄漏) 深拷贝 不拷贝(线程间天然共享同一个 fd 表) 不拷贝
并发承载能力 一般(受限于系统 PID 数量和内存) 较差 较强(线程轻量,但频繁创建仍有瓶颈) 极强(高频长连接首选)
工业适用场景 稳定性要求极高的系统服务 传统 Unix、不支持信号自动回收的场景 快速迭代、轻量级网络长连接调试 高并发长连接(非多路复用)最佳实践

🌟 结语

从最基础的单进程阻塞版本 ,到利用 Linux 底层孤儿领养特性的双重 fork 避坑版本 ,再到现代高并发生产级标配的线程池版本,我们完成了一次扎实的 TCP 网络程序重构演进。

网络编程的核心不仅在于 API 的调用,更在于系统资源(进程、线程、文件描述符)的优雅控制和回收。这也是大厂面试中最爱考查的系统级编程内功。

相关推荐
砍材农夫1 小时前
物联网实战:Spring Boot MQTT | 模拟器Paho客户端拆解核心点
java·javascript·网络·spring boot·后端·物联网
zizle_lin1 小时前
WSL初始化Ubuntu的使用
linux·运维·ubuntu·wsl
J-Tony111 小时前
【计算机网络】TCP粘包和拆包
网络·tcp/ip·计算机网络
志栋智能1 小时前
轻量级 vs. 重平台:巡检超自动化的两种路径选择
运维·网络·人工智能·自动化
衫水1 小时前
项目后端服务 Docker 部署SOP (2026-06-04)
运维·docker·容器
我命由我123452 小时前
Excel - Excel 覆盖模式与编辑模式
运维·学习·职场和发展·excel·求职招聘·职场发展·运维开发
wb043072012 小时前
外卖大战——从阿明的“3 秒生死线“,看系统性能优化的全链路方法论
开发语言·性能优化·架构·php
小的~~2 小时前
Java线程及线程池的相关的问题
java·开发语言·多线程
c++之路2 小时前
Linux 下 C++ 开发环境搭建
linux·运维·c++