文章目录
- 基于C++实现TCP通信的自定义协议设计-网络计算器
-
- 一、代码整体设计框架
- 二、TCP粘包/拆包问题
-
- [2.1 底层成因](#2.1 底层成因)
- [2.2 协议层解决方案:长度前缀+分隔符](#2.2 协议层解决方案:长度前缀+分隔符)
- 三、序列化与反序列化
-
- [3.1 为什么选JSON做序列化格式?](#3.1 为什么选JSON做序列化格式?)
- [3.2 Jsoncpp核心用法](#3.2 Jsoncpp核心用法)
- [3.3 核心实现(Request/Response类)](#3.3 核心实现(Request/Response类))
- 四、客户端访问完整实例(全流程运行演示)
-
- [4.1 环境准备](#4.1 环境准备)
- [4.2 全流程分步演示](#4.2 全流程分步演示)
- 关键设计模式与工程实践
基于C++实现TCP通信的自定义协议设计-网络计算器
这篇笔记记录了我用C++做TCP通信时,自定义协议的落地思路和实操细节。核心围绕协议层、业务层、网络层三层拆分,重点解决TCP粘包拆包、跨平台数据传输一致问题,如果未学习网络部分知识,可以看我之前的文章。
一、代码整体设计框架
本项目采用"分层解耦+模块化封装"的设计思路,整体拆成三层架构,每层职责清晰、依赖关系明确,既方便后续维护,也预留了扩展空间。
三层架构划分
-
协议层:对应Protocol.hpp文件,核心是定义通信规则------解决TCP粘包/拆包、跨平台数据传输一致性的问题,同时提供封包/解包、序列化/反序列化的通用接口,相当于客户端和服务端沟通的"语言规范"。
-
业务层:对应NetCal.hpp文件,封装核心业务逻辑(本项目是处理计算请求),和协议层完全解耦,后续想换成数据查询、文件传输等其他业务也能直接替换,符合"开闭原则"。
-
网络层:对应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() 获取服务器地址信息,对应日志系统打印连接信息的需求。
关键设计模式与工程实践
项目结合代码落地了多种设计模式,精准解决网络编程中的解耦、资源管理、扩展性问题,每个模式均有明确的代码对应,以下逐一对接实现逻辑:
-
工厂模式(抽象封装+具体实现) :定义Socket抽象类,TcpSocket类(Socket.hpp)作为具体实现,封装CreatListenSocket(服务端监听)、CreatNomalSocket(客户端连接)、Recv、Send等TCP专属方法。上层代码通过std::shared_ptr< Socket >(如tcpserver类中Sockptr _socket)指向TcpSocket实例,无需关注底层实现。扩展价值:后续新增UdpSocket类时,仅需继承Socket抽象类并重写纯虚方法,服务端、客户端代码无需修改,完全契合"开闭原则"。
-
策略模式(业务逻辑动态替换):用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处理核心代码。
-
观察者模式(监听-响应机制):以tcpserver类的监听socket为"主题",线程处理逻辑为"观察者"。主题通过Loop()方法持续监听客户端连接(AccecptNewLink()),一旦有新连接接入(主题状态变化),立即触发 pthread_create 创建线程,执行IO处理逻辑(观察者响应)。优势:异步处理客户端请求,避免单线程阻塞(如一个客户端长连接占用线程时,不影响其他客户端连接),提升服务端并发能力。
-
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,彻底规避资源泄漏。
-
回调机制(跨层逻辑解耦):结合std::function与std::bind封装回调函数,实现跨层间接通信。例如,服务端主程序将计算逻辑绑定为回调,传入 ioserver 类,ioserver在解析完请求后调用该回调,无需直接依赖 NetCal.hpp 头文件;tcpserver类将IO处理逻辑(ioserver::IOServer)绑定为回调,接收连接后通过线程调用,屏蔽线程管理与IO处理的耦合。优势:降低模块间依赖,便于单独测试(如测试ioserver时,可传入模拟业务回调,无需启动完整计算模块)。