网络计算器:理解序列化与反序列化(下)

网络计算器:理解序列化与反序列化(中)-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 无法测试服务 完整客户端生命周期
相关推荐
木木_王1 小时前
嵌入式学习 | STM32裸板驱动开发(Day01)入门学习笔记(超详细完整版|点灯实验 + 库函数代码 + 原理全解)
linux·驱动开发·笔记·stm32·学习
_waylau1 小时前
“Java+AI全栈工程师”问答02:Spring Boot 自动配置原理
java·开发语言·spring boot·后端·spring
JAVA面经实录9171 小时前
Java架构师最终完整版学习路线图
java·开发语言·学习
沫儿笙1 小时前
库卡机器人二保焊混合气节气装置
网络·人工智能·机器人
SelectDB技术团队1 小时前
强行拍平?全表扫描? AI Agent 动态 JSON 的观测分析
数据库·人工智能·json·apache doris
勤自省2 小时前
ROS2从入门到“重启解决”:21讲8~12章踩坑血泪史与核心总结
linux·开发语言·ubuntu·ssh·ros
原来是猿2 小时前
Linux守护进程(Daemon)完全指南:从原理到实战
linux·运维·服务器·网络·php
TIEM_692 小时前
C++string|遍历、模拟实现、赋值拷贝现代写法
开发语言·c++
iDao技术魔方2 小时前
Bun v1.3.14 深度解析:Image API、HTTP/3、全局虚拟存储与五十项变革
网络·网络协议·http