解决TCP粘包问题,使用C++实现TCP通信的自定义协议设计

文章目录

基于C++实现TCP通信的自定义协议设计-网络计算器

这篇笔记记录了我用C++做TCP通信时,自定义协议的落地思路和实操细节。核心围绕协议层、业务层、网络层三层拆分,重点解决TCP粘包拆包、跨平台数据传输一致问题,如果未学习网络部分知识,可以看我之前的文章。

一、代码整体设计框架

本项目采用"分层解耦+模块化封装"的设计思路,整体拆成三层架构,每层职责清晰、依赖关系明确,既方便后续维护,也预留了扩展空间。

三层架构划分

  1. 协议层:对应Protocol.hpp文件,核心是定义通信规则------解决TCP粘包/拆包、跨平台数据传输一致性的问题,同时提供封包/解包、序列化/反序列化的通用接口,相当于客户端和服务端沟通的"语言规范"。

  2. 业务层:对应NetCal.hpp文件,封装核心业务逻辑(本项目是处理计算请求),和协议层完全解耦,后续想换成数据查询、文件传输等其他业务也能直接替换,符合"开闭原则"。

  3. 网络层:对应Socket.hpp文件,基于TCP套接字实现客户端/服务端的网络交互------客户端负责发起请求、接收响应,服务端负责管理连接、处理请求,只需要调用协议层的接口做数据封装和解析,不用直接操作字节流。

服务器工作过程

客户端工作过程

二、TCP粘包/拆包问题

粘包/拆包是TCP通信的核心痛点,先搞懂底层成因,再看协议层怎么针对性解决。

2.1 底层成因

TCP套接字内部有输入、输出两个缓冲区,应用层调用send/recv只是把数据在用户空间和内核缓冲区之间拷贝,数据什么时候发、怎么分包,全由内核里的TCP协议栈控制,这就导致了两种问题:

  • 粘包:短时间内多个小包被内核合并发送(减少网络开销),或者服务端一次读取到多个合并的包,没法区分边界。比如客户端连续发"10+"和"20",服务端可能收到"10+20"。

  • 拆包:单个大包超过TCP MSS(最大分段大小)或链路限制,会被拆成多个小分段传输,服务端得多次读取才能拿到完整数据。比如发1024字节的JSON数据,服务端可能先收到512字节,剩下的后续才到。

2.2 协议层解决方案:长度前缀+分隔符

这个项目里用"长度前缀+分隔符"的方式,在应用层给字节流加明确的边界,让服务端能精准解析完整数据包,核心靠AppPackHead(封包)和MovePackHead(解包)两个函数实现。

选\r\n做分隔符,用来区分"长度字段"和"业务数据",避免纯用长度前缀时,因为数据里含数字导致解析出错,比单一的长度标识更可靠。

封包函数(AppPackHead):构建可识别数据包

作用是给序列化后的业务数据加长度前缀和分隔符,生成能正常传输的完整数据包。

cpp 复制代码
void AppPackHead(std::string &passagepack)
{
    // 1. 获取原始业务数据长度(JSON字符串长度)
    int len = passagepack.size();
    // 2. 原始数据末尾追加分隔符,避免与后续包混淆
    passagepack += division;
    // 3. 拼接完整包:长度字符串 + 分隔符 + 原始数据 + 分隔符
    passagepack = std::to_string(len) + division + passagepack;
}

最终数据包格式:[数据长度]\r\n[原始业务数据]\r\n。

示例:原始JSON数据 {"x":10,"oper":"+","y":20}(长度20)

封包后为 20\r\n{"x":10,"oper":"+","y":20}\r\n

解包函数(MovePackHead):解析完整数据包

作用是从服务端缓存的字节流里提取完整的业务数据,处理粘包、拆包的情况。

cpp 复制代码
std::string MovePackHead(std::string &passagepack)
{
    // 1. 查找第一个分隔符,定位长度字段结束位置
    int pos = passagepack.find(division);
    if (pos == std::string::npos)
        return ""; // 无分隔符,数据不完整(拆包场景)
    
    // 2. 解析长度字段
    std::string len_str = passagepack.substr(0, pos);
    int len = std::stoi(len_str);
    
    // 3. 校验数据完整性(完整包总长度=长度字段+原始数据+2次分隔符)
    int total = len_str.size() + len + 2 * division.size();
    if (passagepack.size() < total)
        return ""; // 数据不足,等待后续包(拆包场景)
    
    // 4. 提取原始业务数据并清理缓存
    std::string ret = passagepack.substr(pos + division.size(), len);
    passagepack.erase(0, total); // 删除已解析数据(粘包场景)
    return ret;
}

核心逻辑:通过长度字段确定业务数据的长度,先校验数据是否完整,再提取数据、清理缓存,同时适配粘包(删掉已解析的数据)和拆包(等完整数据)的场景。

三、序列化与反序列化

解决了字节流边界的问题后,还要处理"跨平台数据一致性"的问题------直接传结构体的二进制数据,会因为不同平台内存对齐的差异导致解析出错,所以引入了序列化/反序列化,这里选JSON作为中间格式。

3.1 为什么选JSON做序列化格式?

项目里用JSON作为序列化中间格式,核心是适配TCP跨平台通信的需求,对比其他方案有三个实用优势,完全贴合本项目的轻量场景:

  • 跨平台无兼容坑:JSON是纯文本格式,规避了结构体二进制传输的兼容性问题。若直接传输请求数据的二进制形式,不同平台对字符类型的内存对齐规则可能存在差异,容易导致解析错位;而JSON序列化后以字符串传输,完全不受编译器、硬件架构影响,可无缝适配多平台通信场景。

  • 调试成本极低:对比二进制协议,JSON明文可直接查看。FastWriter生成的JSON字符串如{"x":10,"oper":43,"y":20},抓包时能直接确认参数是否正确,无需额外解包工具,排查问题时可快速定位是序列化还是传输环节的问题。

  • 轻量易集成:借助Jsoncpp库实现快速互转,无需维护协议文件。此处仅需少量代码即可完成序列化,通过root["x"] = _x绑定成员变量,writer.write(root)生成字符串,比Protobuf更适配本项目的轻量计算场景,无需定义.proto文件和编译生成代码。

3.2 Jsoncpp核心用法

项目中仅用 Jsoncpp 的三个核心组件,就实现了请求/响应数据的序列化与反序列化,用法很简洁,完全围绕本项目的计算请求场景设计:

  • 核心组件分工与代码落地:用Json::Value存储JSON数据(类似键值对容器),通过 root["x"] = _x 完成赋值;Json::FastWriter 负责序列化,生成紧凑字符串减少网络传输字节数;Json::Reader负责反序列化,同时完成格式校验,解析失败直接返回false。

  • 序列化(结构体→JSON字符串):请求类内置序列化方法,先创建JSON对象,将请求参数逐个绑定到对应键名,再通过快速序列化工具生成紧凑字符串,最终直接传入封包函数添加传输头信息,完成可传输数据的构建。

  • 核心组件分工与代码落地:用 Json::Value 存储JSON数据(类似键值对容器),对应Request类中 root["x"] = _x 的赋值逻辑;Json::FastWriter 负责序列化,生成紧凑字符串以减少网络传输字节数;Json::Reader负责反序列化,同时完成格式校验,解析失败直接返回false。

  • 特殊处理:char类型适配:因JSON无原生char类型,代码中采用"ASCII码存储"方案,将'+'转为43存储;反序列化时通过asInt()取到43,再隐式转为char类型,完美兼容跨平台传输,避免直接存字符可能出现的编码问题。

3.3 核心实现(Request/Response类)

通过Request(客户端请求)和Response(服务端响应)类封装结构化数据,内置序列化/反序列化方法,和JSON解析深度结合。

Request类(请求数据封装)

cpp 复制代码
class Request
{
public:
    Request() {}
    Request(int x, char oper, int y) : _x(x), _y(y), _oper(oper) {}

    // 序列化:结构体→JSON字符串
    bool Serialize(std::string *out)
    {
        Json::Value root;
        root["x"] = _x;
        root["oper"] = _oper; // char以ASCII码存储
        root["y"] = _y;
        Json::FastWriter writer;
        *out = writer.write(root);
        return true;
    }

    // 反序列化:JSON字符串→结构体
    bool DeSerialize(const std::string &in)
    {
        Json::Value root;
        Json::Reader reader;
        if (!reader.parse(in, root))
            return false;
        _x = root["x"].asInt();
        _y = root["y"].asInt();
        _oper = root["oper"].asInt(); // 转换为char
        return true;
    }

    // 只读访问器(封装原则)
    int X() { return _x; }
    int Y() { return _y; }
    char Oper() { return _oper; }

private:
    int _x;    // 操作数1
    int _y;    // 操作数2
    char _oper;// 运算符(+、-、*、/)
};

Response类(响应数据封装)

cpp 复制代码
class Response
{
public:
    Response() {}
    Response(int code, std::string runstatus, std::string result)
        : _code(code), _runstatus(runstatus), _result(result) {}

    // 序列化与反序列化(逻辑与Request一致)
    bool Serialize(std::string *out)
    {
        Json::Value root;
        root["code"] = _code;       // 状态码:0成功,非0失败
        root["runstatus"] = _runstatus; // 状态描述
        root["result"] = _result;   // 计算结果/错误信息
        Json::FastWriter writer;
        *out = writer.write(root);
        return true;
    }

    bool DeSerialize(const std::string &in)
    {
        Json::Value root;
        Json::Reader reader;
        if (!reader.parse(in, root))
            return false;
        _code = root["code"].asInt();
        _runstatus = root["runstatus"].asString();
        _result = root["result"].asString();
        return true;
    }

    // 调试打印
    void Print()
    {
        std::cout << "结果:" << _result << std::endl;
        std::cout << "状态码:" << _code << std::endl;
        std::cout << "状态描述:" << _runstatus << std::endl;
    }

private:
    int _code;
    std::string _runstatus;
    std::string _result;
};

四、客户端访问完整实例(全流程运行演示)

结合上面的协议设计和技术点,以"客户端发起10+20的计算请求"为例,完整走一遍代码的运行流程,把协议层、客户端、服务端的交互逻辑串起来。

4.1 环境准备

  • 依赖库:Jsoncpp(序列化/反序列化)、POSIX线程库(服务端多线程)。

  • 运行环境:Linux客户端 + Linux服务端。

  • 服务端配置:监听8888端口,支持多客户端并发(用多线程处理)。

4.2 全流程分步演示

协议层与网络层公共文件

1. Protocol.hpp:协议定义与数据封装

cpp 复制代码
// 包含头文件
#pragma once
#include <iostream>
#include <jsoncpp/json/json.h>

// 分隔符定义:用于数据包边界标识
static const std::string division = "\r\n";

// 封包函数:添加长度前缀和分隔符
void AppPackHead(std::string &passagepack)
{
    int len = passagepack.size();                // 获取数据长度
    passagepack += division;                     // 数据后添加分隔符
    passagepack = std::to_string(len) + division + passagepack;  // 构建完整包
}

// 解包函数:提取原始数据并处理粘包/拆包
std::string MovePackHead(std::string &passagepack)
{
    int pos = passagepack.find(division);       // 查找第一个分隔符
    if (pos == std::string::npos)               // 未找到分隔符,数据不完整
        return "";
    
    std::string len_str = passagepack.substr(0, pos);  // 提取长度字符串
    int len = std::stoi(len_str);               // 转换为整数
    
    // 计算完整包长度:长度字段 + 原始数据 + 2个分隔符
    int total = len_str.size() + len + 2 * division.size();
    if (passagepack.size() < total)             // 数据不足,等待更多数据
        return "";
    
    // 提取原始数据并从缓冲区删除已处理部分
    std::string ret = passagepack.substr(pos + division.size(), len);
    passagepack.erase(0, total);                // 清除已处理数据
    return ret;
}

// 请求类:封装客户端发送的数据
class Request
{
public:
    Request() {}
    Request(int x, char oper, int y) : _x(x), _y(y), _oper(oper) {}
    
    // 序列化:对象→JSON字符串
    bool Serialize(std::string *out)
    {
        Json::Value root;
        root["x"] = _x;
        root["oper"] = _oper;                   // char存储为ASCII码
        root["y"] = _y;
        Json::FastWriter writer;
        std::string s = writer.write(root);
        *out = s;
        return true;
    }
    
    // 反序列化:JSON字符串→对象
    bool DeSerialize(const std::string &in)
    {
        Json::Value root;
        Json::Reader reader;
        bool ret = reader.parse(in, root);
        if (ret == false)
            return ret;
        _x = root["x"].asInt();
        _y = root["y"].asInt();
        _oper = root["oper"].asInt();           // ASCII码转char
        return true;
    }
    
    // 访问器方法
    int X() { return _x; }
    int Y() { return _y; }
    char Oper() { return _oper; }

private:
    int _x;         // 操作数1
    int _y;         // 操作数2
    char _oper;     // 运算符
};

// 响应类:封装服务器返回的数据
class Response
{
public:
    Response() {}
    Response(int code, std::string runstatus, std::string result) 
        : _code(code), _runstatus(runstatus), _result(result) {}
    
    // 序列化
    bool Serialize(std::string *out)
    {
        Json::Value root;
        root["code"] = _code;
        root["runstatus"] = _runstatus;
        root["result"] = _result;
        Json::FastWriter writer;
        std::string s = writer.write(root);
        *out = s;
        return true;
    }
    
    // 反序列化
    bool DeSerialize(const std::string &in)
    {
        Json::Value root;
        Json::Reader reader;
        bool ret = reader.parse(in, root);
        if (ret == false)
            return ret;
        _code = root["code"].asInt();
        _runstatus = root["runstatus"].asString();
        _result = root["result"].asString();
        return true;
    }
    
    // 结果打印
    void Print()
    {
        std::cout << "result:" << _result << std::endl;
        std::cout << "code:" << _code << std::endl;
        std::cout << "runstatus:" << _runstatus << std::endl;
    }

private:
    int _code;              // 状态码
    std::string _runstatus; // 状态描述
    std::string _result;    // 计算结果
};

该文件是整个TCP通信体系的协议基石,集中定义了客户端与服务端的通信规则及数据处理逻辑,具体实现了三大核心功能:

  • 定义分隔符 \r\n 作为数据包边界标识,一方面清晰区分长度字段与业务数据,形成"长度+分隔符+数据+分隔符"的标准化格式;另一方面可避免纯长度前缀解析时,因数据含数字字符导致的识别错误,提升解析可靠性。

  • 实现封包与解包函数:封包时先获取数据长度,再按"长度+分隔符+数据+分隔符"的格式拼接完整数据包;解包时通过分隔符定位长度字段,校验数据完整性,粘包场景下自动清理已解析数据,拆包场景则等待后续数据补全,彻底解决TCP粘包/拆包问题。

  • 封装请求/响应类:内置序列化与反序列化方法,与JSON解析逻辑深度集成,屏蔽底层数据转换细节;同时提供只读访问接口,避免业务层直接操作内部数据,符合面向对象封装原则,保障数据安全性与接口规范性。

2. Commend.hpp:命令与错误码定义

cpp 复制代码
// 包含头文件
#pragma once

// 错误码枚举
enum ERRORCODE
{
    APPLY_SOCKET_ERROR = 1,    // 创建socket失败
    SOCKET_BIND_ERROR,         // 绑定地址失败
    LISTEN_ERROR,              // 监听失败
    RECVFORM_ERROR,            // 接收数据失败
    ARGV_ERROR,                // 参数错误
    SENDTO_ERROR,              // 发送数据失败
    CONNECT_ERROR,             // 连接失败
    WRITE_ERROR,               // 写数据失败
    READ_ERROR,                // 读数据失败
    ACCEPT_ERROR,              // 接受连接失败
    THREAD_ERROR               // 线程创建失败
};

// 命令字符串定义
const static char *goption_name = "--name";
const static char *goption_quit = "--quit";
const static char *goption_exit = "--exit";
const static char *goption_connect = "--connect";
const static char *goption_response = "--ok";
const static char *goption_show = "--show";

// 网络配置常量
const static char *glocalip = "-1";     // 本地IP地址
const static int gblcklog = 8;          // 监听队列长度

该文件主要用于集中定义项目通用常量,为全工程提供统一的参数基准,减少硬编码带来的维护成本,具体作用如下:

  • 枚举ERRORCODE:涵盖创建socket、绑定、监听等11种异常场景,如APPLY_SOCKET_ERROR=1、CONNECT_ERROR=7,对应服务端代码中LOG(FATAL, "..."); exit(THREAD_ERROR)的用法,通过错误码快速定位异常类型,比直接打印字符串更规范、易排查。

  • 定义命令字符串:如--quit、--connect,为客户端与服务端指令交互提供统一标准,后续若需扩展指令(如--pause),仅需在此处新增定义,无需改动业务逻辑代码。

  • 管理网络配置常量:集中定义监听队列长度、本地IP标识等配置参数,后续调整时无需修改业务逻辑代码,仅需更新常量值即可,提升项目可维护性,适配不同部署环境的配置需求。

3. Addr.hpp:地址信息封装

cpp 复制代码
// 包含头文件
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

class addr_t
{
public:
    addr_t() {}
    addr_t(sockaddr_in addr) : _addr(addr)
    {
        // 从sockaddr_in提取IP和端口
        _ip = inet_ntoa(addr.sin_addr);
        _port = ntohs(addr.sin_port);
    }
    
    // 相等比较运算符
    bool operator==(const addr_t &addr)
    {
        return _ip == addr.Ip() && _port == addr.Port();
    }
    
    // 不等比较运算符
    bool operator!=(const addr_t &addr)
    {
        return _ip != addr.Ip() || _port != addr.Port();
    }
    
    // 获取地址字符串(IP:Port格式)
    std::string Name()
    {
        return _ip + ":" + std::to_string(_port);
    }
    
    // 获取IP地址
    std::string Ip() const
    {
        return _ip;
    }
    
    // 获取端口号
    u_int16_t Port() const
    {
        return _port;
    }
    
    // 获取原始sockaddr_in结构
    sockaddr_in *Addr()
    {
        return &_addr;
    }
    
    // 获取地址结构长度
    socklen_t Len()
    {
        return sizeof(_addr);
    }

private:
    std::string _ip;        // IP地址字符串
    u_int16_t _port;        // 端口号
    sockaddr_in _addr;      // 原始地址结构
};

该文件将底层C语言风格的sockaddr_in地址结构,封装为面向对象的addr_t类,屏蔽底层字节序转换、格式解析的细节,提升开发效率并降低出错率,核心优势如下:

  • 自动格式转换:构造函数接收 sockaddr_in 结构,通过 inet_ntoa(addr.sin_addr) 将二进制IP转为字符串,ntohs(addr.sin_port) 将网络字节序端口转为主机字节序,对应服务端代码中 AccecptNewLink() 获取客户端地址后,直接通过 addr_t 对象获取IP和端口,无需上层手动转换。

  • 标准化访问接口:提供"IP:Port"格式的地址字符串生成接口,适配日志打印、连接管理等场景;IP与端口提供只读访问,避免地址信息被意外篡改,保障连接管理的稳定性与可靠性。

  • 重载比较运算符:简化地址对象的比较逻辑,判断是否为同一客户端时,可直接通过对象比较完成,无需手动拼接IP与端口字符串再对比,大幅提升代码简洁性与可读性。

4. LockGuard.hpp:互斥锁封装

cpp 复制代码
// 包含头文件
#pragma once
#include <pthread.h>

class LockGuard
{
public:
    // 构造函数:初始化互斥锁
    LockGuard()
    {
        pthread_mutex_init(&_mutex, nullptr);
    }
    
    // 析构函数:销毁互斥锁
    ~LockGuard()
    {
        pthread_mutex_destroy(&_mutex);
    }

private:
    pthread_mutex_t _mutex;  // POSIX互斥锁
};

该文件是基于RAII(资源获取即初始化)思想实现的轻量级互斥锁封装类,专门解决多线程场景下的资源竞争问题,保障线程安全,核心设计如下:

  • RAII自动化资源管理:构造函数中 pthread_mutex_init(&_ mutex, nullptr) 初始化互斥锁,析构函数 pthread_ mutex_ destroy(&_ mutex)自动销毁锁,日志系统中通过创建临时 LockGuard 对象保障线程安全,对象析构时自动释放锁,彻底避免手动释放遗漏导致的死锁。

  • 简化锁使用流程:上层代码无需关注 pthread_mutex_lock/unlock 的细节,仅需创建 LockGuard 对象即可实现锁的自动管理,适配服务端多线程打日志、多线程处理客户端连接等场景,降低线程安全编程门槛。

5. Log.hpp:日志系统

cpp 复制代码
// 包含头文件
#pragma once
#include <iostream>
#include <unistd.h>
#include <cstdarg>
#include <ctime>
#include <string>
#include <fstream>
#include <cstring>
#include "LockGuard.hpp"
#include "Commend.hpp"

namespace log_ns
{
    // 日志等级枚举
    enum grade
    {
        DEBUG,      // 调试信息
        INFO,       // 普通信息
        WARRING,    // 警告信息
        ERROR,      // 错误信息
        FATAL       // 致命错误
    };
    
    // 日志消息结构体
    class logmessage
    {
    public:
        int _grade;             // 日志等级
        pid_t _pid;             // 进程ID
        std::string _filename;  // 文件名
        int _filenumber;        // 行号
        std::string _current_time;  // 时间戳
        std::string _message;   // 日志内容
    };
    
    // 输出类型定义
    #define SCREEN_TYPE 1      // 输出到屏幕
    #define FILE_TYPE 2        // 输出到文件
    
    // 默认配置
    std::string glogfile = "./log.txt";    // 默认日志文件
    std::string glogdev = "stderr";        // 默认输出设备
    
    // 日志类
    class Log
    {
    private:
        // 输出到屏幕
        void ShowToScreen(const std::string &show)
        {
            std::cerr << show;  // 使用标准错误输出
        }
        
        // 输出到文件
        void ShowToFile(const std::string &show)
        {
            std::ofstream out("log.txt", std::ios::app);  // 追加模式
            
            if (!out.is_open())
            {
                return;
            }
            out << show;
            out.close();
        }
        
        // 获取当前时间
        std::string GetCurrentTime()
        {
            time_t now = time(nullptr);
            struct tm *str_now = localtime(&now);
            char buffer[1024];
            snprintf(buffer, sizeof(buffer),
                     "%d-%02d-%02d %02d:%02d:%02d",
                     str_now->tm_year + 1900,
                     str_now->tm_mon + 1,
                     str_now->tm_mday,
                     str_now->tm_hour,
                     str_now->tm_min,
                     str_now->tm_sec);
            return buffer;
        }
        
        // 日志等级转字符串
        std::string GetGradeString(int greade)
        {
            switch (greade)
            {
            case 0: return "DEBUG";
            case 1: return "INFO";
            case 2: return "WARRING";
            case 3: return "ERROR";
            case 4: return "FATAL";
            default: return "UNKONW";
            }
        }
        
        // 格式化日志消息
        std::string GetFormatMessage(logmessage *logmes)
        {
            char buffer[2048];
            snprintf(buffer, sizeof(buffer),
                     "[%s][%d][%s][%d][%s] %s",
                     GetGradeString(logmes->_grade).c_str(),
                     logmes->_pid,
                     logmes->_filename.c_str(),
                     logmes->_filenumber,
                     logmes->_current_time.c_str(),
                     logmes->_message.c_str());
            return buffer;
        }
        
        // 显示日志(根据类型选择输出方式)
        void ShowLog(logmessage *logmes)
        {
            std::string final_message = GetFormatMessage(logmes);
            switch (_type)
            {
            case SCREEN_TYPE:
                ShowToScreen(final_message);
                break;
            case FILE_TYPE:
                ShowToFile(final_message);
                break;
            }
        }
        
    public:
        // 构造函数
        Log(int type = FILE_TYPE, std::string logfile = glogfile) 
            : _type(type), _logfile(logfile)
        {
        }
        
        // 切换输出类型
        void Enable(int type)
        {
            _type = type;
        }
        
        // 记录日志的核心方法
        void Logmessage(int grade, std::string filename, int filenumber, const char *format...)
        {
            LockGuard();  // 线程安全保护
            
            logmessage log_message;
            log_message._grade = grade;
            log_message._filename = filename;
            log_message._filenumber = filenumber;
            log_message._pid = getpid();  // 获取进程ID
            
            // 处理可变参数
            va_list ap;
            va_start(ap, format);
            char log_info[1024];
            vsnprintf(log_info, sizeof(log_info), format, ap);
            log_message._message = log_info;
            
            log_message._current_time = GetCurrentTime();
            ShowLog(&log_message);
        }
        
    private:
        int _type;               // 输出类型
        std::string _logfile;    // 日志文件路径
    };
    
    // 全局日志对象
    Log lg;
    
    // 宏定义:简化日志调用
    #define EnableScreen() \
        do { \
            lg.Enable(SCREEN_TYPE); \
        } while (0)
        
    #define EnableFile() \
        do { \
            lg.Enable(FILE_TYPE); \
        } while (0)
        
    #define LOG(Gread, Format, ...) \
        do { \
            lg.Logmessage(Gread, __FILE__, __LINE__, Format, ##__VA_ARGS__); \
        } while (0)
};

该文件实现了一个轻量且线程安全的日志系统,为项目提供标准化的日志记录能力,便于开发调试与线上问题排查,核心特性如下:

  • 分级日志管理:定义DEBUG至FATAL五级日志,对应LOG宏,服务端初始化失败用LOG(FATAL, ...),客户端连接成功用LOG(INFO, ...),可通过切换日志等级过滤无效信息,提升调试效率。

  • 双输出方式:ShowToScreen 输出到 stderr,ShowToFile 追加写入log.txt,通过 EnableScreen()/EnableFile() 切换,开发阶段用屏幕输出快速调试,线上环境用文件输出留存日志。

  • 结构化日志信息:每条日志包含等级、进程ID、文件名、行号、时间戳五大核心要素,形成标准化日志格式,出现问题时可快速定位异常位置,提升问题排查效率。

  • 线程安全保障:通过 LockGuard() 创建互斥锁,保护日志写入操作,避免多线程并发打日志时出现内容重叠、错乱,适配服务端多线程处理客户端连接的场景。

  • 便捷调用封装:通过宏定义简化日志调用流程,自动补充文件名、行号等基础信息,上层代码仅需传入日志等级与内容即可,无需手动拼接辅助信息,提升开发效率。

服务器端文件

1. TcpServerMain.cpp:服务器主程序

cpp 复制代码
// 包含头文件
#include <iostream>
#include <memory>
#include "TcpServer.hpp"
#include "NetCal.hpp"
#include "Server.hpp"

// 类型别名定义
using iofunc_t = std::function<void(std::shared_ptr<Socket>)>;
using cal_func_t = std::function<std::shared_ptr<Response>(std::shared_ptr<Request>)>;

int main(int argc, char *argv[])
{
    // 1. 参数校验
    if (argc != 2)
    {
        std::cout << "Useg: ./tepserver server-port" << std::endl;
        exit(0);
    }
    
    // 2. 创建计算对象并绑定函数
    calculate cal;  // 业务逻辑对象
    cal_func_t cal_func = std::bind(&calculate::Calculate, &cal, std::placeholders::_1);
    
    // 3. 创建IO服务器并绑定计算函数
    ioserver ios(cal_func);
    
    // 4. 绑定IO处理函数
    iofunc_t io_func = std::bind(&ioserver::IOServer, &ios, std::placeholders::_1);
    
    // 5. 创建服务器实例并启动
    u_int16_t local_port = std::stoi(argv[1]);
    std::unique_ptr<tcpserver> server_ptr = std::make_unique<tcpserver>(local_port, io_func);
    server_ptr->Loop();

    return 0;
}

该文件是服务端的入口程序,核心职责是串联业务逻辑、IO处理、服务器框架三大模块,完成服务端的初始化与启动,整体流程清晰且解耦,具体逻辑如下:

  • 命令行参数校验:强制要求传入监听端口号,若 argc!=2 则输出用法 Useg: ./tepserver server-port 并退出,避免用户因参数错误导致服务端异常启动,提升程序健壮性。

  • 业务逻辑绑定:实例化 calculate 对象,通过 std::bind(&calculate::Calculate, &cal, std::placeholders::_1) 将计算方法封装为cal_func_t 回调,实现业务层与框架层解耦,后续替换业务逻辑(如字符串处理),仅需修改绑定的函数即可。

  • IO处理器初始化:创建IO处理实例并传入业务逻辑回调,实现IO层与业务层的解耦,IO处理器解析完请求后自动调用业务逻辑,无需依赖具体业务头文件,降低模块耦合度。

  • IO处理函数绑定:绑定 ioserver::IOServer 方法为io_func_t回调,供tcpserver接收连接后调用,对应 tcpserver 中线程执行函数 td->_self->_func(td->_sockptr),实现IO处理与连接管理的解耦。

  • 服务器启动:解析端口号后,通过智能指针创建服务器实例并自动管理资源,调用监听方法启动主循环,持续接收客户端连接请求,完成服务端初始化与启动全流程。

2. TcpServer.hpp:服务器框架

cpp 复制代码
// 包含头文件
#pragma once

#include <iostream>
#include <functional>
#include "Socket.hpp"
#include <unistd.h>
#include <pthread.h>
#include <wait.h>
#include <memory>
#include "Log.hpp"
#include "Addr.hpp"

using namespace log_ns;
using namespace socket_ns;

// IO函数类型定义
using iofunc_t = std::function<void(std::shared_ptr<Socket>)>;

class tcpserver
{
public:
    // 构造函数:初始化监听socket和IO处理函数
    tcpserver(u_int16_t local_port, iofunc_t func)
        : _socket(std::make_shared<TcpSocket>()), _func(func)
    {
        _socket->CreatListenSocket(local_port);  // 创建并配置监听socket
    }
    
    // 线程参数传递结构体
    class ServerThread_t
    {
    public:
        ServerThread_t(Sockptr sockptr, tcpserver *self)
            : _sockptr(sockptr), _self(self)
        {
        }
        Sockptr _sockptr;     // 客户端socket指针
        tcpserver *_self;     // 服务器实例指针
    };
    
    // 服务器主循环
    void Loop()
    {
        _isruning = true;

        while (_isruning)
        {
            // 1. 接受客户端连接
            Sockptr client_sockptr = _socket->AccecptNewLink();
            
            // 2. 创建线程参数
            pthread_t thread;
            ServerThread_t *td = new ServerThread_t(client_sockptr, this);
            if (td == nullptr)
                continue;
                
            // 3. 创建线程处理客户端请求
            if (pthread_create(&thread, nullptr, Excute, td) != 0)
            {
                LOG(FATAL, "<server> thread creat error:%s\n", strerror(errno));
                exit(THREAD_ERROR);
            }
        }
        _isruning = false;
    }
    
    // 线程执行函数(静态方法)
    static void *Excute(void *args)
    {
        pthread_detach(pthread_self());  // 线程分离,避免僵尸线程
        LOG(INFO, "<server : %d> thread creat success\n", gettid());
        
        // 获取线程参数并执行IO处理
        ServerThread_t *td = static_cast<ServerThread_t *>(args);
        td->_self->_func(td->_sockptr);
        
        delete td;  // 清理线程参数
        return nullptr;
    }

private:
    Sockptr _socket;    // 监听socket
    iofunc_t _func;     // IO处理函数
    bool _isruning;     // 服务器运行状态
};

该文件实现了服务端的核心框架,负责监听客户端连接、创建线程处理连接,屏蔽底层socket监听与线程管理的细节,为上层提供稳定的连接管理能力,核心功能如下:

  • 监听 socket 初始化(构造函数):调用 _socket->CreatListenSocket(local_port),内部完成 socket 创建、bind绑定、listen 监听的全流程,初始化失败则直接退出,对应 TcpSocket 类中封装的底层 socket 操作,屏蔽细节。

  • 连接监听主循环:通过循环持续接收新客户端连接,每次连接成功后通过智能指针管理客户端连接资源,自动完成资源释放,避免手动管理导致的内存泄漏问题。

  • 多线程并发处理:每接收一个连接,创建独立线程,通过 pthread_create 调用 Excute 静态方法,将客户端 socket 指针传入线程,实现多客户端并发处理,避免单线程阻塞导致其他客户端无法连接。

  • 线程资源管理(Excute方法):先通过 pthread_detach(pthread_self()) 将线程设为分离状态,避免产生僵尸线程;线程执行完_ func(td->_sockptr)(IO处理逻辑)后,delete td清理线程参数,确保资源无残留。

3. Server.hpp:IO处理服务器

cpp 复制代码
// 包含头文件
#pragma once
#include <functional>
#include "Socket.hpp"
#include "Protocol.hpp"

using namespace socket_ns;

// 计算函数类型定义
using cal_func_t = std::function<std::shared_ptr<Response>(std::shared_ptr<Request>)>;

class ioserver
{
public:
    // 构造函数:绑定业务逻辑函数
    ioserver(cal_func_t func) : _func(func) {}
    
    // IO处理核心方法
    void IOServer(Sockptr client)
    {
        std::string passagestream;  // 数据流缓冲区
        
        while (true)
        {
            // 1. 接收数据
            int n = client->Recv(&passagestream);
            
            // 2. 解包(处理粘包/拆包)
            std::string passagepack = MovePackHead(passagestream);
            if (passagepack.size() == 0)
                continue;  // 数据不完整,继续等待
                
            // 3. 反序列化:JSON字符串→Request对象
            std::shared_ptr<Request> req = std::make_shared<Request>();
            req->DeSerialize(passagepack);
            
            // 4. 执行业务逻辑
            std::shared_ptr<Response> resp = _func(req);
            
            // 5. 序列化:Response对象→JSON字符串
            std::string respance_str;
            resp->Serialize(&respance_str);
            
            // 6. 封包(添加长度前缀)
            AppPackHead(respance_str);
            
            // 7. 发送响应
            client->Send(respance_str);
        }
    }

private:
    cal_func_t _func;  // 业务逻辑函数
};

该文件是服务端的IO处理核心模块,负责与客户端进行具体的数据交互,串联解包、反序列化、业务处理、序列化、封包、发送等全流程,核心逻辑如下:

  • 业务逻辑解耦(构造函数):接收 cal_func_t 类型回调函数,IOServer 方法仅负责数据流转,通过 _func(req) 调用业务逻辑,无需在 ioserver 类中包含 NetCal.hpp,实现IO层与业务层的彻底解耦。

  • 循环处理请求(IOServer方法):while (true)持续接收客户端数据,保持长连接,直至客户端断开;接收数据存入 passagestream 缓冲区,为后续解包处理提供数据基础,对应 Recv 方法将数据追加到缓冲区的逻辑。

  • 完整解析流程:调用 MovePackHead(passagestream) 解包,数据不完整则 continue 等待;完整则通过 req->DeSerialize(passagepack) 反序列化,再调用 _func(req) 执行业务逻辑,严格遵循"解包-反序列化-业务处理"的流程。

  • 响应封装流程:业务逻辑返回 Response 对象后,通过 resp->Serialize(&respance_str) 序列化,再调用AppPackHead(respance_str) 封包,添加长度前缀和分隔符,确保客户端能正确解析,对应客户端 MovePackHead 的解包逻辑。

  • 响应发送:通过 client->Send(respance_str) 将封好的数据包发送给客户端,完成"请求-响应"闭环,发送失败由 Send 方法内部打日志,无需 ioserver 类额外处理,职责划分清晰。

4. NetCal.hpp:业务逻辑实现

C++ 复制代码
// 包含头文件
#pragma once
#include <iostream>
#include "Protocol.hpp"

class calculate
{
public:
    // 计算核心方法
    std::shared_ptr<Response> Calculate(std::shared_ptr<Request> req)
    {
        char oper = req->Oper();  // 获取运算符
        int x = req->X();         // 获取操作数1
        int y = req->Y();         // 获取操作数2
        int ret = 0;              // 计算结果
        int code = 0;             // 状态码
        std::string status = "success";  // 状态描述
        
        // 根据运算符执行计算
        switch (oper)
        {
        case '+':
            ret = x + y;  // 加法
            break;
        case '-':
            ret = x - y;  // 减法
            break;
        case '*':
            ret = x * y;  // 乘法
            break;
        case '/':
        {
            if (y == 0)   // 除零检查
            {
                code = 1;
                status = "div zero";
            }
            else
                ret = x / y;  // 除法
        }
        break;
        case '%':
        {
            if (y == 0)   // 模零检查
            {
                code = 2;
                status = "mod zero";
            }
            else
                ret = x % y;  // 取模
        }
        break;
        default:
        {
            code = 3;         // 非法操作符
            status = "illegal operation";
        }
        break;
        }

        // 构建并返回Response对象
        return std::make_shared<Response>(code, status, std::to_string(ret));
    }
};

该文件是纯业务逻辑模块,与网络层、协议层完全解耦,仅负责接收请求数据、执行计算逻辑、返回处理结果,专注于核心业务实现,具体设计如下:

  • 标准化数据交互(Calculate方法):接收 std::shared_ptr< Request > req 参数,通过req->Oper()、req->X()、req->Y() 提取数据,避免直接操作Request类私有成员_x、_y、_oper,符合面向对象封装原则,同时通过智能指针管理Request对象,避免内存泄漏。

  • 计算与异常处理:switch (oper) 支持加减乘除取模五种运算,针对除零(y== 0)、模零(y==0)、非法运算符(default)分别设置状态码1、2、3,避免程序崩溃,对应 Response 对象状态码的定义逻辑。

  • 结构化响应封装:通过 std::make_shared < Response > (code, status, std::to_string(ret)) 创建响应对象,将状态码、描述、结果封装后返回,供IO层序列化发送,确保响应数据格式统一,客户端可按固定规则解析。

  • 状态码定义:0表示成功,1除零错误,2模零错误,3非法运算符,对应客户端 Response::Print() 方法打印状态码,让用户清晰知晓计算结果及异常原因。

客户端文件

1. TcpClientMain.cpp:客户端主程序

cpp 复制代码
// 包含头文件
#include <iostream>
#include <memory>
#include "TcpClient.hpp"

int main(int argc, char *argv[])
{
    // 1. 参数校验
    if (argc != 3)
    {
        std::cout << "Useg: ./tepclient server-ip server-port" << std::endl;
        exit(0);
    }

    // 2. 获取服务器地址
    std::string server_ip = argv[1];
    u_int16_t server_port = std::stoi(argv[2]);

    // 3. 创建客户端并启动
    std::unique_ptr<tcpclient> client_ptr = std::make_unique<tcpclient>(server_ip, server_port);
    client_ptr->Loop();
    
    return 0;
}

该文件是客户端的入口程序,逻辑简洁直观,核心职责是解析配置参数、初始化客户端实例并启动交互循环,具体流程如下:

  • 参数校验:强制要求传入服务器IP和端口,argc!=3则输出用法Useg: ./tepclient server-ip server-port,引导用户正确启动,避免因参数缺失导致连接失败。

  • 地址解析:将命令行传入的字符串IP(argv[1])和端口(argv[2])转为std::string和u_int16_t,为tcpclient构造函数提供标准参数,对应tcpclient类连接服务器的地址格式要求。

  • 客户端启动:通过 std::make_unique 创建 tcpclient 实例,智能指针自动管理客户端资源;调用 Loop() 方法启动交互主循环,进入用户输入、请求发送、响应接收的循环流程。

2. TcpClient.hpp:客户端框架

cpp 复制代码
// 包含头文件
#pragma once
#include <iostream>
#include <memory>
#include "Socket.hpp"
#include "Log.hpp"
#include "Commend.hpp"
#include "Addr.hpp"
#include "Protocol.hpp"

using namespace log_ns;
using namespace socket_ns;

class tcpclient
{
public:
    // 构造函数:连接服务器
    tcpclient(std::string server_ip, u_int16_t server_port)
        : _socket(std::make_shared<TcpSocket>())
    {
        _socket->CreatNomalSocket(server_ip, server_port);  // 创建并连接socket
    }
    
    ~tcpclient() {}
    
    // 客户端主循环
    void Loop()
    {
        _isruning = true;
        char buffer[1024] = {0};

        while (true)
        {
            std::string message;

            // 1. 获取用户输入
            std::cout << "Place Enter:";
            int x, y;
            char oper;
            std::cin >> x >> oper >> y;
            
            // 2. 构建请求对象
            std::unique_ptr<Request> request_str = std::make_unique<Request>(x, oper, y);

            // 3. 序列化:Request对象→JSON字符串
            std::string requst_str;
            request_str->Serialize(&requst_str);

            // 4. 封包:添加长度前缀
            AppPackHead(requst_str);

            // 5. 发送请求
            int n = _socket->Send(requst_str);
            if (n < 0)  // 发送失败
            {
                LOG(FATAL, "<client> client write to server error,%s\n", strerror(errno));
                exit(WRITE_ERROR);
            }
            else  // 发送成功
            {
                LOG(INFO, "<client> client write %s to server\n", requst_str.c_str());
            }

            // 6. 接收响应
            std::string response_str;
            n = _socket->Recv(&response_str);
            if (n > 0)  // 接收成功
            {
                buffer[n] = 0;
                LOG(INFO, "<client> client get data from server: %s\n", response_str.c_str());
                std::cout << buffer << std::endl;
            }
            else if (n == 0)  // 服务器断开
            {
                LOG(FATAL, "<client> server quit!\n");
                break;
            }
            else  // 接收失败
            {
                LOG(FATAL, "<client> client read error,%s\n", strerror(errno));
                exit(READ_ERROR);
            }

            // 7. 解包:去除长度前缀
            response_str = MovePackHead(response_str);

            // 8. 反序列化:JSON字符串→Response对象
            std::unique_ptr<Response> reponse_ptr = std::make_unique<Response>();
            reponse_ptr->DeSerialize(response_str);

            // 9. 打印结果
            reponse_ptr->Print();
        }

        _isruning = false;
    }

private:
    std::shared_ptr<TcpSocket> _socket;  // socket对象
    bool _isruning;                      // 运行状态
};

该文件是客户端的核心交互模块,负责串联用户输入、请求封装、数据发送、响应接收、结果展示全流程,适配服务端的协议格式,确保通信正常,核心逻辑如下:

  • 连接初始化(构造函数):调用 _socket->CreatNomalSocket(server_ip, server_port),内部完成 socket 创建、地址结构初始化、connect 连接的全流程,连接失败则通过 LOG(FATAL, ...) 打日志并退出,屏蔽底层socket操作细节。

  • 用户交互循环:while (true) 持续提示用户输入,通过 std::cin >> x >> oper >> y 获取算术表达式,严格限制输入格式为"操作数1 运算符 操作数2",适配Request类的构造参数要求。

  • 请求封装与序列化:创建 Request 对象,调用 Serialize(&requst_str) 生成 JSON 字符串,对应 Request 类 Serialize 方法的逻辑,将用户输入的参数转为可传输的字符串格式。

  • 请求封包与发送:调用 AppPackHead(requst_str) 添加长度前缀和分隔符,避免粘包;通过 _socket->Send(requst_str) 发送数据,发送失败则打 FATAL 日志并退出,发送成功则打 INFO 日志记录发送内容,便于调试。

  • 响应接收与解析:接收服务端数据存入 response_str,调用 MovePackHead(response_str) 解包,提取完整业务数据,对应服务端 AppPackHead 的封包逻辑,确保解包后的数据可正常反序列化。

  • 结果展示:创建Response对象,调用 DeSerialize(response_str) 解析数据,再通过 Print() 方法打印结果、状态码、状态描述,为用户提供清晰的反馈。

3. Socket.hpp中的客户端特定方法

cpp 复制代码
// 客户端连接方法(在TcpSocket类中)
void ConnectServer(std::string server_ip, u_int16_t server_port) override
{
    sockaddr_in server_addr_in;
    memset(&server_addr_in, 0, sizeof(server_addr_in));
    server_addr_in.sin_family = AF_INET;
    server_addr_in.sin_port = htons(server_port);  // 端口转换
    inet_pton(AF_INET, server_ip.c_str(), &server_addr_in.sin_addr);  // IP转换
    _addr = addr_t(server_addr_in);  // 保存地址信息

    // 连接服务器
    if (connect(_sockfd, (struct sockaddr *)&server_addr_in, sizeof(server_addr_in)) < 0)
    {
        LOG(FATAL, "<client> connect error,%s\n", strerror(errno));
        exit(CONNECT_ERROR);
    }
    LOG(INFO, "<client> connect success\n");
}

该方法是Socket.hpp中TcpSocket类的客户端专用接口,负责完成客户端与服务端的TCP连接建立,屏蔽底层地址结构初始化、字节序转换的细节,核心实现如下:

  • 地址结构初始化:初始化 sockaddr_in 结构,memset 清空内存避免脏数据,sin_family = AF_INET 指定IPv4协议,符合TCP通信的地址格式要求。

  • 地址格式转换:htons(server_port) 将主机字节序端口转为网络字节序(大端序),inet_pton(AF_INET, server_ip.c_str(), &server_addr_in.sin_addr) 将字符串IP转为二进制格式,适配 connect 函数对地址参数的要求,避免字节序、格式错误导致连接失败。

  • TCP连接发起:调用connect函数向服务端发起连接,失败则打FATAL日志并退出(exit(CONNECT_ERROR)),成功则打INFO日志;同时将地址信息存入_addr(addr_t对象),供后续日志输出等场景使用。

  • 地址信息留存: _ addr = addr_ t(server_ addr_in) 将 sockaddr_in 结构封装为 addr_t 对象,后续可通过_ addr.Ip()、_addr.Port() 获取服务器地址信息,对应日志系统打印连接信息的需求。

关键设计模式与工程实践

项目结合代码落地了多种设计模式,精准解决网络编程中的解耦、资源管理、扩展性问题,每个模式均有明确的代码对应,以下逐一对接实现逻辑:

  1. 工厂模式(抽象封装+具体实现) :定义Socket抽象类,TcpSocket类(Socket.hpp)作为具体实现,封装CreatListenSocket(服务端监听)、CreatNomalSocket(客户端连接)、Recv、Send等TCP专属方法。上层代码通过std::shared_ptr< Socket >(如tcpserver类中Sockptr _socket)指向TcpSocket实例,无需关注底层实现。扩展价值:后续新增UdpSocket类时,仅需继承Socket抽象类并重写纯虚方法,服务端、客户端代码无需修改,完全契合"开闭原则"。

  2. 策略模式(业务逻辑动态替换):用std::function定义业务逻辑类型 cal_func_t(using cal_func_t = std::function< std::shared_ptr< Response >(std::shared_ptr< Request >)>),服务端主程序通过std::bind绑定calculate::Calculate方法,传入ioserver类。ioserver类仅通过_func(req)调用业务逻辑,不关心具体是计算、查询还是其他业务。扩展价值:若需替换为字符串处理业务,仅需新增StringProcess类及Process方法,绑定后传入ioserver即可,无需改动IO处理核心代码。

  3. 观察者模式(监听-响应机制):以tcpserver类的监听socket为"主题",线程处理逻辑为"观察者"。主题通过Loop()方法持续监听客户端连接(AccecptNewLink()),一旦有新连接接入(主题状态变化),立即触发 pthread_create 创建线程,执行IO处理逻辑(观察者响应)。优势:异步处理客户端请求,避免单线程阻塞(如一个客户端长连接占用线程时,不影响其他客户端连接),提升服务端并发能力。

  4. RAII模式(资源自动管理):两处核心实现,一是LockGuard类,构造函数初始化互斥锁(pthread_mutex_init),析构函数自动销毁(pthread_mutex_destroy),日志系统中通过创建临时LockGuard对象保障线程安全,对象析构时自动释放锁,避免死锁。二是智能指针的使用,如 tcpserver 类中 Sockptr _socket(std::shared_ptr< Socket >)、客户端代码中 std::unique_ptr< Request > request_str,确保socket、Request/Response对象超出作用域后自动释放,无需手动调用close或delete,彻底规避资源泄漏。

  5. 回调机制(跨层逻辑解耦):结合std::function与std::bind封装回调函数,实现跨层间接通信。例如,服务端主程序将计算逻辑绑定为回调,传入 ioserver 类,ioserver在解析完请求后调用该回调,无需直接依赖 NetCal.hpp 头文件;tcpserver类将IO处理逻辑(ioserver::IOServer)绑定为回调,接收连接后通过线程调用,屏蔽线程管理与IO处理的耦合。优势:降低模块间依赖,便于单独测试(如测试ioserver时,可传入模拟业务回调,无需启动完整计算模块)。

相关推荐
比昨天多敲两行2 小时前
C++ 类和对象(中)
开发语言·c++
智者知已应修善业2 小时前
【整数各位和循环求在0-9范围】2024-10-27
c语言·c++·经验分享·笔记·算法
wheeldown2 小时前
【Linux网络编程】应用层自定义协议和序列化
linux·运维·网络
燃于AC之乐2 小时前
我的算法修炼之路--9——重要算法思想:贪心、二分、正难则反、多重与完全背包精练
c++·算法·贪心算法·动态规划·二分答案·完全背包·多重背包
晚风吹长发2 小时前
初步理解Linux中的信号概念以及信号产生
linux·运维·服务器·算法·缓冲区·inode
ccieluo2 小时前
华为eNSP网络工程毕业设计 基于双出口智能选路的中小型企业网络设计 策略路由 IPSec SSL 无线网络 BGP
网络·华为·毕业设计
青火coding2 小时前
ai时代下的RPC传输——StreamObserver
qt·网络协议·microsoft·rpc
历程里程碑2 小时前
Linux 4 指令结尾&&通过shell明白指令实现的原理
linux·c语言·数据结构·笔记·算法·排序算法
zhengtianzuo2 小时前
049-Linux抓屏-xcb
linux·抓屏·xcb