
🔥个人主页:Cx330🌸
❄️个人专栏:《C语言》《LeetCode刷题集》《数据结构-初阶》《C++知识分享》
《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔
🌟心向往之行必能
🎥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(如 socket、bind、listen、accept、connect),还将手写实现从单进程、多进程、多线程,直到高并发线程池的 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(×tamp, &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 的调用,更在于系统资源(进程、线程、文件描述符)的优雅控制和回收。这也是大厂面试中最爱考查的系统级编程内功。