网络计算器:理解序列化与反序列化(中)-CSDN博客


一、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);  // 创建→绑定→监听
    }
};

为什么 _listensockptrunique_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 无法测试服务 完整客户端生命周期
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐