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

在网络编程中,我们经常需要把内存中的结构体、类对象、整数、字符串 等数据通过 socket 发送给另一端。但是 socket 只认识字节流 。怎样把 {x=10, y=20, oper='+'} 这样一个请求对象变成字节流发出去?接收方又怎样从字节流中还原出原来的对象?这两个过程就是 序列化(Serialize)反序列化(Deserialize)

所有代码均为 C++17,依赖 jsoncpp 库。编译命令见 Makefile。


一、 项目整体概览

这是一个典型的 C/S 架构 应用:

  • 服务器监听端口,接收客户端发来的运算请求(如 10 + 20),计算后返回结果。

  • 客户端:从标准输入读取运算表达式,发送给服务器,并打印结果。

文件名 类型 核心职责
Common.hpp 头文件 全局常量、枚举、工具宏、禁止拷贝基类
InetAddr.hpp 头文件 网络地址 ↔ 主机地址 的双向转换封装
Mutex.hpp 头文件 互斥锁 的 RAII 封装
Log.hpp 头文件 策略模式 日志系统(控制台/文件)
Socket.hpp 头文件 模板方法模式 Socket 抽象层 + TCP 实现
Protocol.hpp 头文件 序列化/反序列化 + 自定义协议编解码 + 粘包处理
Cal.hpp 头文件 业务层:计算器逻辑 + 错误码体系
TcpServer.hpp 头文件 服务器层:TCP 监听 + 多进程并发模型
main.cc 源文件 服务端入口:三层组装(业务→协议→服务器)
TcpClient.cc 源文件 客户端入口:用户交互 + 请求发送 + 响应接收
Makefile 构建文件 编译规则

二、Common.hpp:地基文件

2.1 文件存在的意义

这是整个项目的**"基础设施层"** 。所有其他文件都 #include "Common.hpp",因为它提供了:

  • 统一的错误码枚举

  • 通用的类型转换宏

  • 禁止拷贝的基类(防止资源管理类被误拷贝)

2.2 代码解析(错误码)

复制代码
enum ExitCode {
    OK = 0,        // 正常退出
    USAGE_ERR,     // 命令行参数错误
    SOCKET_ERR,    // socket() 系统调用失败
    BIND_ERR,      // bind() 系统调用失败
    LISTEN_ERR,    // listen() 系统调用失败
    CONNECT_ERR,   // connect() 系统调用失败
    FORK_ERR,      // fork() 系统调用失败
    OPEN_ERR       // 文件打开失败
};

为什么需要这个枚举?

  • exit()语义化 的参数,而不是 exit(1)exit(2) 这种魔法数字

  • 方便调试时快速定位是哪一步系统调用出错

2.3 禁止拷贝/赋值

复制代码
class NoCopy {
public:
    NoCopy() {}
    ~NoCopy() {}
    NoCopy(const NoCopy &) = delete;           // 禁止拷贝构造
    const NoCopy &operator=(const NoCopy&) = delete;  // 禁止拷贝赋值
};

存在价值TcpSocket 持有文件描述符 _sockfd,如果允许拷贝,两个对象会指向同一个 fd,析构时重复 close() 导致错误。继承 NoCopy 就从编译期杜绝这个问题。

2.4 宏封装

复制代码
#define CONV(addr) ((struct sockaddr*)&addr)

存在价值bind()accept()connect() 等系统函数要求 struct sockaddr* 参数,但代码中使用的是 struct sockaddr_in。每次强转很麻烦,用宏封装后代码更简洁。

复制代码
#pragma once

#include <iostream>
#include <functional>
#include <unistd.h>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>

enum ExitCode
{
    OK = 0,
    USAGE_ERR,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR,
    FORK_ERR,
    OPEN_ERR
};

class NoCopy
{
public:
    NoCopy(){}
    ~NoCopy(){}
    NoCopy(const NoCopy &) = delete;
    const NoCopy &operator = (const NoCopy&) = delete;
};

#define CONV(addr) ((struct sockaddr*)&addr)

三、InetAddr.hpp:地址转换专家

3.1 文件存在的意义

网络编程中,IP 地址和端口有两种表示形式

  • 主机字节序 :人类可读(如 192.168.1.1:8080

  • 网络字节序 :大端整数(如 0xC0A80101:0x1F90

这个类负责双向无缝转换,让上层代码完全不用关心大小端问题。

3.2 三个构造函数详解

复制代码
// 构造函数1:默认构造
InetAddr() {}

场景 :先定义一个空对象,后续用 SetAddr() 填充。比如 Accept() 时先定义 InetAddr client,再传入 &client 让系统调用填充。

复制代码
// 构造函数2:从已有的 sockaddr_in 构造(网络→主机)
InetAddr(struct sockaddr_in &addr) { SetAddr(addr); }

场景accept() 返回的是 struct sockaddr_in,需要转成人类可读的 IP:Port 用于日志打印。

复制代码
// 构造函数3:从 IP + Port 构造(主机→网络)
InetAddr(const std::string &ip, uint16_t port) : _ip(ip), _port(port) {
    memset(&_addr, 0, sizeof(_addr));
    _addr.sin_family = AF_INET;
    inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);  // 点分十进制 → 网络整数
    _addr.sin_port = htons(_port);                     // 主机端口 → 网络端口
}

场景 :客户端连接服务器时,用户输入的是 "127.0.0.1"8080,需要转成 sockaddr_inconnect() 使用。

核心函数对比

函数 方向 用途
inet_pton() 主机 → 网络 字符串 IP 转整数
inet_ntop() 网络 → 主机 整数 IP 转字符串
htons() 主机 → 网络 端口转大端
ntohs() 网络 → 主机 端口转小端
复制代码
void SetAddr(struct sockaddr_in &addr) {
    _addr = addr;
    _port = ntohs(_addr.sin_port);  // 网络端口 → 主机端口
    char ipbuffer[64];
    inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(ipbuffer));  // 网络IP → 字符串
    _ip = ipbuffer;
}

存在价值Accept() 后调用,把系统填充的 sockaddr_in 转成可打印的 _ip_port

复制代码
const struct sockaddr *NetAddrPtr() { return CONV(_addr); }
socklen_t NetAddrLen() { return sizeof(_addr); }

存在价值 :这两个函数是胶水代码 ,让 InetAddr 对象能无缝传给 bind()connect()accept() 等系统调用。

复制代码
#pragma once
#include "Common.hpp"
// 网络地址和主机地址之间进行转换的类

class InetAddr
{
public:
    InetAddr() {}
    InetAddr(struct sockaddr_in &addr)
    {
        SetAddr(addr);
    }
    InetAddr(const std::string &ip, uint16_t port) : _ip(ip), _port(port)
    {
        // 主机转网络
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
        _addr.sin_port = htons(_port);
        // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO
    }
    InetAddr(uint16_t port) : _port(port), _ip()
    {
        // 主机转网络
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_addr.s_addr = INADDR_ANY;
        _addr.sin_port = htons(_port);
    }
    void SetAddr(struct sockaddr_in &addr)
    {
        _addr = addr;
        // 网络转主机
        _port = ntohs(_addr.sin_port); // 从网络中拿到的!网络序列
        // _ip = inet_ntoa(_addr.sin_addr); // 4字节网络风格的IP -> 点分十进制的字符串风格的IP
        char ipbuffer[64];
        inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(_addr));
        _ip = ipbuffer;
    }
    uint16_t Port() { return _port; }
    std::string Ip() { return _ip; }
    const struct sockaddr_in &NetAddr() { return _addr; }
    const struct sockaddr *NetAddrPtr()
    {
        return CONV(_addr);
    }
    socklen_t NetAddrLen()
    {
        return sizeof(_addr);
    }
    bool operator==(const InetAddr &addr)
    {
        return addr._ip == _ip && addr._port == _port;
    }
    std::string StringAddr()
    {
        return _ip + ":" + std::to_string(_port);
    }
    ~InetAddr()
    {
    }

private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};

四、Mutex.hpp:线程安全的 RAII 封装

4.1 文件存在的意义

日志系统需要线程安全 (多个客户端进程可能同时写日志),但裸 pthread_mutex_t 容易忘了解锁导致死锁。RAII 封装让锁的生命周期与对象绑定

4.2 逐类解析

复制代码
class Mutex {
    pthread_mutex_t _mutex;
public:
    Mutex() { pthread_mutex_init(&_mutex, nullptr); }
    void Lock() { pthread_mutex_lock(&_mutex); }
    void Unlock() { pthread_mutex_unlock(&_mutex); }
    ~Mutex() { pthread_mutex_destroy(&_mutex); }
    pthread_mutex_t *Get() { return &_mutex; }
};

存在价值 :封装了 pthread_mutex_t 的完整生命周期(创建→加锁→解锁→销毁),避免资源泄漏。

复制代码
class LockGuard {
    Mutex &_mutex;
public:
    LockGuard(Mutex &mutex) : _mutex(mutex) { _mutex.Lock(); }
    ~LockGuard() { _mutex.Unlock(); }
};

这是 RAII 的经典应用

  • 构造时加锁

  • 析构时解锁(无论是否发生异常!)

使用场景

复制代码
void SyncLog(const std::string &message) {
    LockGuard lockguard(_mutex);  // 构造 = 加锁
    std::cout << message;         // 临界区操作
}                                 // 析构 = 解锁(自动!)

#pragma once

#include <iostream>
#include <pthread.h>

namespace MutexModule
{
    class Mutex
    {
    public:
        Mutex()
        {
            pthread_mutex_init(&_mutex, nullptr);
        }
        void Lock()
        {
            int n = pthread_mutex_lock(&_mutex);
            (void)n;
        }
        void Unlock()
        {
            int n = pthread_mutex_unlock(&_mutex);
            (void)n;
        }
        ~Mutex()
        {
            pthread_mutex_destroy(&_mutex);
        }
        pthread_mutex_t *Get()
        {
            return &_mutex;
        }
    private:
        pthread_mutex_t _mutex;
    };

    class LockGuard
    {
    public:
        LockGuard(Mutex &mutex) : _mutex(mutex)
        {
            _mutex.Lock();
        };
        ~LockGuard()
        {
            _mutex.Unlock();
        };

    private:
        Mutex &_mutex;
    };
}

五、Log.hpp:策略模式日志系统

5.1 文件存在的意义

一个优秀的网络服务必须可观测。日志系统需要:

  • 支持多种输出目标(控制台、文件、甚至未来可能的数据库)

  • 线程安全:多进程/多线程并发写日志不能乱序

  • 使用简单:一行宏就能记录时间、级别、文件、行号

5.2 策略模式架构

复制代码
// 抽象策略
class LogStrategy {
    virtual void SyncLog(const std::string &message) = 0;
};

// 具体策略1:控制台
class ConsoleLogStrategy : public LogStrategy {
    void SyncLog(const std::string &message) override {
        LockGuard lockguard(_mutex);
        std::cout << message << gsep;
    }
};

// 具体策略2:文件
class FileLogStrategy : public LogStrategy {
    void SyncLog(const std::string &message) override {
        LockGuard lockguard(_mutex);
        std::ofstream out(filename, std::ios::app);  // append 模式
        out << message << gsep;
    }
};

策略模式的好处

  • 运行时切换策略:logger.EnableFileLogStrategy()

  • 新增策略无需修改现有代码(如未来加 DatabaseLogStrategy

5.3 流式日志接口(最精妙的设计)

复制代码
#define LOG(level) logger(level, __FILE__, __LINE__)

这个宏是"语法糖"的核心

  • __FILE__:当前文件名(编译器内置宏)

  • __LINE__:当前行号(编译器内置宏)

复制代码
// 使用示例
LOG(LogLevel::DEBUG) << "收到请求: " << json_package;

背后发生了什么?

  1. 宏展开为:logger(LogLevel::DEBUG, "main.cc", 42)

  2. logger() 返回一个 LogMessage 临时对象

  3. << "收到请求" 调用 LogMessage::operator<<,拼接字符串

  4. 语句结束,LogMessage 析构 ,自动调用 SyncLog() 输出

    class LogMessage {
    public:
    LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger)
    : _curr_time(GetTimeStamp()),
    _level(level),
    _pid(getpid()),
    _src_name(src_name),
    _line_number(line_number),
    _logger(logger)
    {
    // 构造时拼接"日志头部":[时间] [级别] [PID] [文件] [行号] -
    std::stringstream ss;
    ss << "[" << _curr_time << "] "
    << "[" << Level2Str(_level) << "] "
    << "[" << _pid << "] "
    << "[" << _src_name << "] "
    << "[" << _line_number << "] "
    << "- ";
    _loginfo = ss.str();
    }

    复制代码
     template <typename T>
     LogMessage &operator<<(const T &info) {
         std::stringstream ss;
         ss << info;
         _loginfo += ss.str();  // 拼接"日志内容"
         return *this;           // 链式调用
     }
     
     ~LogMessage() {
         if (_logger._fflush_strategy) {
             _logger._fflush_strategy->SyncLog(_loginfo);  // 析构时刷新!
         }
     }

    };

为什么设计成"析构时刷新"?

  • 保证一行日志是原子输出的,不会被其他线程插进来

  • 支持链式 <<LOG(INFO) << "a" << "b" << 123; 最终是一整条日志

    #ifndef LOG_HPP
    #define LOG_HPP

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

    namespace LogModule
    {
    using namespace MutexModule;

    复制代码
      const std::string gsep = "\r\n";
      // 策略模式,C++多态特性
      // 2. 刷新策略 a: 显示器打印 b:向指定的文件写入
      //  刷新策略基类
      class LogStrategy
      {
      public:
          ~LogStrategy() = default;
          virtual void SyncLog(const std::string &message) = 0;
      };
    
      // 显示器打印日志的策略 : 子类
      class ConsoleLogStrategy : public LogStrategy
      {
      public:
          ConsoleLogStrategy()
          {
          }
          void SyncLog(const std::string &message) override
          {
              LockGuard lockguard(_mutex);
              std::cout << message << gsep;
          }
          ~ConsoleLogStrategy()
          {
          }
    
      private:
          Mutex _mutex;
      };
    
      // 文件打印日志的策略 : 子类
      const std::string defaultpath = "/var/log/";
      const std::string defaultfile = "my.log";
      class FileLogStrategy : public LogStrategy
      {
      public:
          FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile)
              : _path(path),
                _file(file)
          {
              LockGuard lockguard(_mutex);
              if (std::filesystem::exists(_path))
              {
                  return;
              }
              try
              {
                  std::filesystem::create_directories(_path);
              }
              catch (const std::filesystem::filesystem_error &e)
              {
                  std::cerr << e.what() << '\n';
              }
          }
          void SyncLog(const std::string &message) override
          {
              LockGuard lockguard(_mutex);
    
              std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file; // "./log/" + "my.log"
              std::ofstream out(filename, std::ios::app);                              // 追加写入的 方式打开
              if (!out.is_open())
              {
                  return;
              }
              out << message << gsep;
              out.close();
          }
          ~FileLogStrategy()
          {
          }
    
      private:
          std::string _path; // 日志文件所在路径
          std::string _file; // 日志文件本身
          Mutex _mutex;
      };
    
      // 形成一条完整的日志&&根据上面的策略,选择不同的刷新方式
    
      // 1. 形成日志等级
      enum class LogLevel
      {
          DEBUG,
          INFO,
          WARNING,
          ERROR,
          FATAL
      };
      std::string Level2Str(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";
          }
      }
      std::string GetTimeStamp()
      {
          time_t curr = time(nullptr);
          struct tm curr_tm;
          localtime_r(&curr, &curr_tm);
          char timebuffer[128];
          snprintf(timebuffer, sizeof(timebuffer),"%4d-%02d-%02d %02d:%02d:%02d",
              curr_tm.tm_year+1900,
              curr_tm.tm_mon+1,
              curr_tm.tm_mday,
              curr_tm.tm_hour,
              curr_tm.tm_min,
              curr_tm.tm_sec
          );
          return timebuffer;
      }
    
      // 1. 形成日志 && 2. 根据不同的策略,完成刷新
      class Logger
      {
      public:
          Logger()
          {
              EnableConsoleLogStrategy();
          }
          void EnableFileLogStrategy()
          {
              _fflush_strategy = std::make_unique<FileLogStrategy>();
          }
          void EnableConsoleLogStrategy()
          {
              _fflush_strategy = std::make_unique<ConsoleLogStrategy>();
          }
    
          // 表示的是未来的一条日志
          class LogMessage
          {
          public:
              LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger)
                  : _curr_time(GetTimeStamp()),
                    _level(level),
                    _pid(getpid()),
                    _src_name(src_name),
                    _line_number(line_number),
                    _logger(logger)
              {
                  // 日志的左边部分,合并起来
                  std::stringstream ss;
                  ss << "[" << _curr_time << "] "
                     << "[" << Level2Str(_level) << "] "
                     << "[" << _pid << "] "
                     << "[" << _src_name << "] "
                     << "[" << _line_number << "] "
                     << "- ";
                  _loginfo = ss.str();
              }
              // LogMessage() << "hell world" << "XXXX" << 3.14 << 1234
              template <typename T>
              LogMessage &operator<<(const T &info)
              {
                  // a = b = c =d;
                  // 日志的右半部分,可变的
                  std::stringstream ss;
                  ss << info;
                  _loginfo += ss.str();
                  return *this;
              }
    
              ~LogMessage()
              {
                  if (_logger._fflush_strategy)
                  {
                      _logger._fflush_strategy->SyncLog(_loginfo);
                  }
              }
    
          private:
              std::string _curr_time;
              LogLevel _level;
              pid_t _pid;
              std::string _src_name;
              int _line_number;
              std::string _loginfo; // 合并之后,一条完整的信息
              Logger &_logger;
          };
    
          // 这里故意写成返回临时对象
          LogMessage operator()(LogLevel level, std::string name, int line)
          {
              return LogMessage(level, name, line, *this);
          }
          ~Logger()
          {
          }
    
      private:
          std::unique_ptr<LogStrategy> _fflush_strategy;
      };
    
      // 全局日志对象
      Logger logger;
    
      // 使用宏,简化用户操作,获取文件名和行号
      #define LOG(level) logger(level, __FILE__, __LINE__)
      #define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
      #define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()

    }

    #endif

相关推荐
山有木兮啊1 小时前
libutp 性能分析总结
网络
执笔仗剑天涯1 小时前
WSL安装cc-switch
linux·windows·wsl·cc-switch
Cx330❀1 小时前
从零实现一个 C++ 轻量级日志系统:原理与实践
大数据·linux·运维·服务器·开发语言·c++·搜索引擎
Agent产品评测局1 小时前
国产vs海外AI Agent方案,制造业场景适配性横评:企业级自动化选型全景深度解析
运维·人工智能·ai·chatgpt·自动化
程序leo源1 小时前
Linux深度理解
linux·运维·服务器·c语言·c++·青少年编程·c#
Cowboy hat1 小时前
计算机网络名词解释汇总
网络
Quinn271 小时前
正点原子 RK3562 Android14 Ubuntu 编译 SDK 环境准备:依赖、repo 与 Swap 配置一次搞定
linux·运维·ubuntu·mpu·正点原子·rk3562·arm linux
@insist1231 小时前
信息安全工程师-网络安全风险评估(上篇):框架、流程与量化基础
网络·安全·软考·信息安全工程师·软件水平考试
怀旧,1 小时前
【Linux系统编程】22. 线程同步与互斥(上)
linux·运维·服务器