在网络编程中,我们经常需要把内存中的结构体、类对象、整数、字符串等数据通过 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

Logo

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

更多推荐