一、TcpServer.hpp:多进程并发引擎
1.1 文件存在的意义
这是服务器的"发动机",负责:
-
创建监听套接字
-
循环接受客户端连接
-
为每个客户端创建独立进程处理
-
避免僵尸进程
1.2 构造函数
class TcpServer {
uint16_t _port;
std::unique_ptr<Socket> _listensockptr; // 监听套接字
bool _isrunning;
ioservice_t _service; // 回调函数:处理客户端IO
public:
TcpServer(uint16_t port, ioservice_t service)
: _port(port),
_listensockptr(std::make_unique<TcpSocket>()), // 创建 TCP 套接字
_isrunning(false),
_service(service)
{
_listensockptr->BuildTcpSocketMethod(_port); // 创建→绑定→监听
}
};
为什么
_listensockptr是unique_ptr<Socket>而不是TcpSocket?
多态:基类指针可以指向任意子类对象(未来支持 UDP 时只需换子类)
unique_ptr自动管理内存,服务器析构时自动close()监听套接字
1.3 Start 函数:多进程模型的核心
void Start() {
_isrunning = true;
while (_isrunning) {
InetAddr client;
auto sock = _listensockptr->Accept(&client); // 阻塞等待连接
if (sock == nullptr) continue;
LOG(LogLevel::DEBUG) << "accept success ..." << client.StringAddr();
// ========== 多进程模型开始 ==========
pid_t id = fork();
if (id < 0) {
LOG(LogLevel::FATAL) << "fork error ...";
exit(FORK_ERR);
}
else if (id == 0) {
// ========== 子进程 ==========
_listensockptr->Close(); // 子进程不需要监听套接字!
if (fork() > 0) exit(OK); // 子进程再 fork,自己立即退出
// ========== 孙子进程(孤儿进程)==========
// 此时子进程已 exit,孙子进程被 init 领养
_service(sock, client); // 处理客户端请求(长连接)
sock->Close();
exit(OK); // 处理完自动退出,由 init 回收
}
else {
// ========== 父进程 ==========
sock->Close(); // 父进程不需要连接套接字!
pid_t rid = ::waitpid(id, nullptr, 0); // 瞬间回收子进程
(void)rid; // 消除未使用变量警告
}
}
}
逐行逻辑拆解 :
| 步骤 | 执行者 | 动作 | 目的 |
|---|---|---|---|
| 1 | 父进程 | Accept() 成功 |
得到连接套接字 sock |
| 2 | 父进程 | fork() 创建子进程 |
复制地址空间 |
| 3 | 子进程 | 关闭 listen_fd |
子进程不需要监听 |
| 4 | 子进程 | 再次 fork() 创建孙子进程 |
关键技巧! |
| 5 | 子进程 | exit(0) |
立即退出 |
| 6 | 孙子进程 | 执行 _service() |
实际处理客户端 |
| 7 | 父进程 | 关闭 conn_fd |
父进程不直接处理连接 |
| 8 | 父进程 | waitpid() |
回收已 exit 的子进程(瞬间完成) |
| 9 | 孙子进程 | 处理完 exit() |
成为孤儿,由 init 回收 |
为什么要 fork 两次?
如果只 fork 一次:
-
父进程必须
waitpid()等待子进程处理完客户端(可能几分钟) -
期间父进程无法
Accept新连接 → 无法并发
fork 两次的 trick:
-
子进程创建孙子进程后立即
exit -
父进程的
waitpid()瞬间完成(子进程已经是僵尸态) -
孙子进程成为孤儿进程 ,被
init(PID 1)领养 -
孙子进程处理客户端(可能很久),处理完后由
init自动回收 -
父进程完全不用关心孙子进程 ,专心
Accept
文件描述符的关闭策略:
父进程:持有 listen_fd,关闭 conn_fd
子进程:关闭 listen_fd,持有 conn_fd(但立即 exit)
孙子进程:关闭 listen_fd(继承自子进程),持有 conn_fd(处理业务)
为什么要关闭不需要的 fd?
-
不关闭
listen_fd:子进程/孙子进程也能Accept,导致混乱 -
不关闭
conn_fd:父进程持有但不使用,客户端断开后无法感知#include "Socket.hpp"
#include <iostream>
#include <memory>
#include <sys/wait.h>
#include <functional>using namespace SocketModule;
using namespace LogModule;using ioservice_t = std::function<void(std::shared_ptr<Socket> &sock, InetAddr &client)>;
// 主要解决:连接的问题,IO通信的问题
// 细节: TcpServer,需不需要关心自己未来传递的信息是什么?不需要关心!!
// 网络版本的计算器,长服务
class TcpServer
{
public:
TcpServer(uint16_t port, ioservice_t service) : _port(port),
_listensockptr(std::make_unique<TcpSocket>()),
_isrunning(false),
_service(service)
{
_listensockptr->BuildTcpSocketMethod(_port);
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
InetAddr client;
auto sock = _listensockptr->Accept(&client); // 1. 和client通信sockfd 2. client 网络地址
if (sock == nullptr)
{
continue;
}
LOG(LogLevel::DEBUG) << "accept success ..." << client.StringAddr();// sock && client pid_t id = fork(); if (id < 0) { LOG(LogLevel::FATAL) << "fork error ..."; exit(FORK_ERR); } else if (id == 0) { // 子进程 -> listensock _listensockptr->Close(); if (fork() > 0) exit(OK); // 孙子进程在执行任务,已经是孤儿了 _service(sock, client); sock->Close(); exit(OK); } else { // 父进程 -> sock sock->Close(); pid_t rid = ::waitpid(id, nullptr, 0); (void)rid; } } _isrunning = false; } ~TcpServer() {}private:
uint16_t _port;
std::unique_ptr<Socket> _listensockptr;
bool _isrunning;
ioservice_t _service;
};
二、main.cc:服务器的"组装车间"
2.1 完整代码
#include "NetCal.hpp" // Cal 类
#include "Protocol.hpp" // Protocol 类
#include "TcpServer.hpp" // TcpServer 类
#include <memory>
void Usage(std::string proc) {
std::cerr << "Usage: " << proc << " port" << std::endl;
}
int main(int argc, char *argv[]) {
if (argc != 2) {
Usage(argv[0]);
exit(USAGE_ERR);
}
// ========== 第1层:业务层 ==========
std::unique_ptr<Cal> cal = std::make_unique<Cal>();
// ========== 第2层:协议层 ==========
std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>(
[&cal](Request &req) -> Response {
return cal->Execute(req); // 把 Cal::Execute 包装成回调
}
);
// ========== 第3层:服务器层 ==========
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(
std::stoi(argv[1]), // 端口
[&protocol](std::shared_ptr<Socket> &sock, InetAddr &client) {
protocol->GetRequest(sock, client); // 把 Protocol::GetRequest 包装成回调
}
);
tsvr->Start(); // 启动服务器
return 0;
}
2.2 三层组装的"连线"逻辑
这是整个项目最精妙的设计 ------通过 lambda 回调 把三层解耦:

数据流向 :

为什么用 lambda 而不是直接传对象指针?
-
解耦 :
TcpServer不需要知道Protocol的存在 -
灵活性 :可以替换
Protocol的实现(如换成 protobuf 版本),TcpServer不用改 -
生命周期安全:
unique_ptr管理对象生命周期,lambda 捕获引用不会悬空
三、TcpClient.cc:客户端的完整生命周期
3.1 完整代码
#include "Socket.hpp"
#include "Common.hpp"
#include "Protocol.hpp"
#include <iostream>
#include <string>
#include <memory>
using namespace SocketModule;
void Usage(std::string proc) {
std::cerr << "Usage: " << proc << " server_ip server_port" << std::endl;
}
void GetDataFromStdin(int *x, int *y, char *oper) {
std::cout << "Please Enter x: ";
std::cin >> *x;
std::cout << "Please Enter y: ";
std::cin >> *y;
std::cout << "Please Enter oper: ";
std::cin >> *oper;
}
int main(int argc, char *argv[]) {
if (argc != 3) {
Usage(argv[0]);
exit(USAGE_ERR);
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
// ========== 步骤1:创建并连接 ==========
std::shared_ptr<Socket> client = std::make_shared<TcpSocket>();
client->BuildTcpClientSocketMethod(); // 只创建 socket
if (client->Connect(server_ip, server_port) != 0) {
std::cerr << "connect error" << std::endl;
exit(CONNECT_ERR);
}
// ========== 步骤2:创建协议对象 ==========
std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>();
std::string resp_buffer; // 客户端的累积缓冲区
// ========== 步骤3:业务循环 ==========
while (true) {
// 3.1 获取用户输入
int x, y;
char oper;
GetDataFromStdin(&x, &y, &oper);
// 3.2 构建请求字符串(序列化 + 编码)
std::string req_str = protocol->BuildRequestString(x, y, oper);
// 3.3 发送
client->Send(req_str);
// 3.4 接收并解析响应
Response resp;
bool res = protocol->GetResponse(client, resp_buffer, &resp);
if (res == false) break; // 服务器断开或出错
// 3.5 显示结果
resp.ShowResult();
}
client->Close();
return 0;
}
3.2 客户端 vs 服务器对比
| 维度 | 客户端 | 服务器 |
|---|---|---|
| Socket 创建 | BuildTcpClientSocketMethod()(只创建) |
BuildTcpSocketMethod(port)(创建+绑定+监听) |
| 连接方式 | Connect(ip, port) 主动连接 |
Accept() 被动接受 |
| 协议对象 | Protocol() 无参构造(不需要回调) |
Protocol(func_t) 带回调构造 |
| 循环模式 | 用户输入 → 发送 → 等待响应 | 阻塞接收 → 处理 → 发送(长连接) |
| 缓冲区 | resp_buffer 累积响应 |
buffer_queue 累积请求 |
3.3 为什么客户端的 Protocol 不需要回调?
// 客户端
std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>(); // 无参构造
// 服务器
std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>([&cal](Request &req)->Response{...});
-
客户端只需要发请求、收响应,不需要处理业务逻辑
-
服务器需要把收到的
Request交给Cal计算,所以必须传入_func回调
四、Makefile:构建系统
.PHONY:all
all:server_netcal client_netcal
server_netcal:main.cc
g++ -o $@ $^ -std=c++17 -ljsoncpp
client_netcal:TcpClient.cc
g++ -o $@ $^ -std=c++17 -ljsoncpp
.PHONY:clean
clean:
rm -f server_netcal client_netcal
关键点:
-
-std=c++17:代码使用了std::filesystem(C++17 特性) -
-ljsoncpp:链接 jsoncpp 库 -
$@表示目标文件名,$^表示所有依赖文件



总结:每个文件的价值
| 文件 | 如果删掉它 | 为什么不可替代 |
|---|---|---|
Common.hpp |
魔法数字满天飞,资源泄漏 | 统一错误码、禁止拷贝 |
InetAddr.hpp |
手动处理大小端转换 | 双向自动转换、类型安全 |
Mutex.hpp |
死锁、资源泄漏 | RAII 自动管理锁生命周期 |
Log.hpp |
无法调试、输出混乱 | 策略模式、流式接口、线程安全 |
Socket.hpp |
裸系统调用、流程错误 | 模板方法、多态、智能指针 |
Protocol.hpp |
粘包、格式混乱 | 序列化、自定义协议、粘包处理 |
Cal.hpp |
网络代码与业务耦合 | 纯粹业务层、错误码体系 |
TcpServer.hpp |
单进程阻塞、僵尸进程 | 多进程并发、fork trick |
main.cc |
无法启动服务 | 三层组装、lambda 解耦 |
TcpClient.cc |
无法测试服务 | 完整客户端生命周期 |