在网络编程中,TCP 流式协议的粘包 / 拆包问题、模块化设计、日志调试是核心痛点。本文将通过一个完整的「TCP 计算器项目」,带你从 0 到 1 理解如何用 C++ 封装 Socket、设计自定义协议、实现日志系统,并完成客户端与服务端的通信逻辑。项目支持加减乘除取模运算,包含错误处理(如除零错误),且可灵活切换序列化方式(自定义格式 / JSON)。
一、项目整体架构
先看项目的文件结构与模块分工,清晰的模块化设计是代码可维护性的关键:
| 文件名 | 核心功能 | 所属模块 |
|---|---|---|
Log.hpp | 多级别日志(Info/Debug/Warning 等)、多输出方式 | 日志模块 |
Socket.hpp | 封装 Socket API(创建 / 绑定 / 监听 / 连接 / 关闭) | 网络通信模块 |
Protocol.hpp | 自定义 TCP 协议(解决粘包)、Request/Response 序列化 | 协议与序列化模块 |
ServerCal.hpp | 计算器核心逻辑(运算处理、错误码定义) | 业务逻辑模块 |
TcpServer.hpp | 服务端框架(多进程处理客户端、信号处理) | 服务端框架 |
client.cpp | 客户端入口(生成随机请求、发送 / 接收数据) | 客户端 |
server.cpp | 服务端入口(初始化服务、绑定业务回调) | 服务端 |
二、核心模块详解
1. 日志模块:Log.hpp—— 调试与问题排查的基石
日志是开发和线上问题排查的核心工具。本模块支持5 级日志级别和3 种输出方式,且通过全局单例lg简化调用。
1.1 核心设计
-
日志级别:从低到高分为
Info(普通信息)、Debug(调试信息)、Warning(警告)、Error(错误)、Fatal(致命错误,触发程序退出)。 -
输出方式:
-
Screen:输出到控制台(开发阶段用); -
Onefile:所有日志写入单个文件(./log/log.txt); -
Classfile:按级别分文件(如log.txt.Debug、log.txt.Error)。
-
-
日志格式:
[级别][年-月-日 时:分:秒] 自定义信息,例如:[Info][2024-05-20 15:30:00] init server .... done。
1.2 关键代码片段
// 日志级别转字符串
std::string levelToString(int level) {
switch (level) {
case Info: return "Info";
case Debug: return "Debug";
case Warning: return "Warning";
case Error: return "Error";
case Fatal: return "Fatal";
default: return "None";
}
}
// 核心日志输出(支持可变参数,类似printf)
void operator()(int level, const char *format, ...) {
// 1. 格式化时间
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
char time_buf[1024];
snprintf(time_buf, sizeof(time_buf), "[%s][%d-%d-%d %d:%d:%d]",
levelToString(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
// 2. 格式化自定义参数(可变参数处理)
va_list args;
va_start(args, format);
char content_buf[1024];
vsnprintf(content_buf, sizeof(content_buf), format, args);
va_end(args);
// 3. 拼接日志并输出
char log_buf[2048];
snprintf(log_buf, sizeof(log_buf), "%s %s\n", time_buf, content_buf);
printLog(level, log_buf); // 根据输出方式分发
}
1.3 使用示例
// 输出Info级日志(服务端初始化完成)
lg(Info, "init server .... done");
// 输出Debug级日志(打印接收的数据包)
lg(Debug, "debug:\n%s", inbuffer_stream.c_str());
// 输出Fatal级日志(Socket创建失败,触发退出)
lg(Fatal, "socker error, %s: %d", strerror(errno), errno);
2. 网络通信模块:Socket.hpp—— 简化 Socket API 调用
原始的 Socket API(socket/bind/listen等)参数繁琐且错误处理重复,本模块将其封装为Sock类,统一错误处理(结合日志),降低调用成本。
2.1 核心方法解析
| 方法名 | 功能描述 | 关键细节 |
|---|---|---|
Socket() | 创建 TCP Socket 文件描述符 | 失败时输出 Fatal 日志并退出 |
Bind() | 绑定端口(服务端用) | 支持INADDR_ANY(绑定所有网卡 IP) |
Listen() | 监听端口(服务端用) | _backlog_设为 10(等待连接队列长度) |
Accept() | 接收客户端连接(服务端用) | 返回新的通信 Socket,获取客户端 IP 和端口 |
Connect() | 连接服务端(客户端用) | 失败时返回false,方便客户端重试 |
Fd() | 获取 Socket 文件描述符 | 供读写操作(read/write)使用 |
Close() | 关闭 Socket | 释放资源,避免文件描述符泄漏 |
2.2 服务端绑定端口示例
Sock listensock;
listensock.Socket(); // 创建Socket
listensock.Bind(8080); // 绑定8080端口
listensock.Listen(); // 开始监听
2.3 客户端连接服务端示例
Sock sockfd;
sockfd.Socket();
// 连接服务端(IP:127.0.0.1,端口:8080)
if (!sockfd.Connect("127.0.0.1", 8080)) {
std::cerr << "连接服务端失败" << std::endl;
return 1;
}
3. 协议与序列化模块:Protocol.hpp—— 解决 TCP 粘包问题
TCP 是流式协议,发送方多次发送的数据可能被合并,接收方一次读取可能包含多个数据包(粘包),或一次只读取部分数据包(拆包)。本模块通过「自定义协议」和「序列化」解决该问题。
3.1 自定义协议设计
核心思路:长度前缀 + 分隔符,确保接收方能准确拆分数据包。
-
编码(Encode):将原始内容(如
"10+20")包装为长度\n内容\n的格式。示例:原始内容
"10+20"(长度 5)→ 编码后"5\n10+20\n"。 -
解码(Decode):先读取长度,再按长度读取对应内容,最后移除已处理的数据包。
3.2 协议核心代码
// 编码:内容 → 长度\n内容\n
std::string Encode(std::string &content) {
std::string package = std::to_string(content.size()); // 长度
package += "\n"; // 分隔符
package += content; // 原始内容
package += "\n"; // 分隔符
return package;
}
// 解码:长度\n内容\n → 内容(成功返回true)
bool Decode(std::string &package, std::string *content) {
// 1. 找到第一个\n(分隔长度和内容)
std::size_t pos = package.find("\n");
if (pos == std::string::npos) return false; // 未找到分隔符,数据包不完整
// 2. 解析长度
std::string len_str = package.substr(0, pos);
std::size_t len = std::stoi(len_str); // 内容长度
// 3. 检查数据包是否完整(总长度 = 长度字符串长度 + 内容长度 + 2个\n)
std::size_t total_len = len_str.size() + len + 2;
if (package.size() < total_len) return false; // 数据包不完整,等待后续数据
// 4. 提取内容并移除已处理的数据包
*content = package.substr(pos + 1, len);
package.erase(0, total_len); // 关键:删除已解码的部分,避免重复处理
return true;
}
3.3 Request 与 Response 序列化
Request(客户端请求)和Response(服务端响应)是业务数据载体,支持两种序列化方式(通过#define MySelf切换):
-
自定义格式:简单字符串拼接(如
Request为"10 + 20",Response为"30 0"); -
JSON 格式:基于
JsonCpp库,可读性更强,适合复杂数据结构。
3.3.1 Request 类(客户端请求)
class Request {
public:
int x; // 第一个操作数
int y; // 第二个操作数
char op; // 运算符(+/-/*///%)
// 序列化:Request → 字符串(自定义格式)
bool Serialize(std::string *out) {
#ifdef MySelf
*out = std::to_string(x) + " " + op + " " + std::to_string(y);
// 示例:x=10, y=20, op='+' → "10 + 20"
#else
// JSON序列化(需要链接JsonCpp库)
Json::Value root;
root["x"] = x;
root["y"] = y;
root["op"] = op;
Json::StyledWriter writer;
*out = writer.write(root);
#endif
return true;
}
// 反序列化:字符串 → Request
bool Deserialize(const std::string &in) {
#ifdef MySelf
// 解析"10 + 20" → x=10, op='+', y=20
std::size_t left = in.find(" ");
std::size_t right = in.rfind(" ");
if (left == std::string::npos || right == std::string::npos) return false;
x = std::stoi(in.substr(0, left));
op = in[left + 1];
y = std::stoi(in.substr(right + 1));
#else
// JSON反序列化
Json::Value root;
Json::Reader reader;
if (!reader.parse(in, root)) return false;
x = root["x"].asInt();
y = root["y"].asInt();
op = root["op"].asInt();
#endif
return true;
}
};
3.3.2 Response 类(服务端响应)
class Response {
public:
int result; // 运算结果(正确时有效)
int code; // 错误码(0=成功,1=除零,2=模零,3=非法运算符)
// 序列化/反序列化逻辑与Request类似,此处省略...
};
4. 业务逻辑模块:ServerCal.hpp—— 计算器核心
该模块封装运算逻辑和错误处理,与网络框架解耦,便于后续扩展(如增加浮点数运算)。
4.1 错误码定义
enum {
Div_Zero = 1, // 除零错误
Mod_Zero, // 模零错误
Other_Oper // 非法运算符
};
4.2 运算核心方法
// 运算逻辑(根据Request计算,返回Response)
Response CalculatorHelper(const Request &req) {
Response resp(0, 0); // 初始:result=0,code=0(成功)
switch (req.op) {
case '+': resp.result = req.x + req.y; break;
case '-': resp.result = req.x - req.y; break;
case '*': resp.result = req.x * req.y; break;
case '/':
if (req.y == 0) resp.code = Div_Zero; // 除零错误
else resp.result = req.x / req.y;
break;
case '%':
if (req.y == 0) resp.code = Mod_Zero; // 模零错误
else resp.result = req.x % req.y;
break;
default: resp.code = Other_Oper; // 非法运算符
}
return resp;
}
// 对接协议:解码→反序列化→计算→序列化→编码
std::string Calculator(std::string &package) {
std::string content;
// 1. 解码(解决粘包)
if (!Decode(package, &content)) return "";
// 2. 反序列化(字符串→Request)
Request req;
if (!req.Deserialize(content)) return "";
// 3. 计算(核心业务逻辑)
Response resp = CalculatorHelper(req);
// 4. 序列化(Response→字符串)
resp.Serialize(&content);
// 5. 编码(准备发送)
return Encode(content);
}
5. 服务端框架:TcpServer.hpp—— 多进程处理客户端
服务端需要同时处理多个客户端连接,本项目采用「多进程模型」:父进程监听端口,每接收一个客户端连接就fork一个子进程处理,父进程继续监听。
5.1 核心设计
-
信号处理:
-
SIGCHLD:忽略该信号,避免子进程成为僵尸进程; -
SIGPIPE:忽略该信号,避免客户端断开后服务端写操作崩溃。
-
-
回调函数:通过
std::function将业务逻辑(如Calculator)与网络框架解耦,便于替换业务(如改为 echo 服务)。
5.2 服务端启动核心代码
void Start() {
// 信号处理
signal(SIGCHLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
while (true) {
// 接收客户端连接
std::string clientip;
uint16_t clientport;
int sockfd = listensock_.Accept(&clientip, &clientport);
if (sockfd < 0) continue;
lg(Info, "新连接:sockfd=%d, IP=%s, 端口=%d", sockfd, clientip.c_str(), clientport);
// fork子进程处理客户端
if (fork() == 0) {
listensock_.Close(); // 子进程不需要监听Socket,关闭节省资源
std::string inbuffer_stream; // 缓存接收的数据(解决拆包)
while (true) {
char buffer[1280];
ssize_t n = read(sockfd, buffer, sizeof(buffer));
if (n <= 0) break; // 客户端断开或读取错误
buffer[n] = 0;
inbuffer_stream += buffer; // 追加到缓存
// 循环解码(处理粘包:可能包含多个数据包)
while (true) {
std::string resp = callback_(inbuffer_stream); // 调用业务回调(如Calculator)
if (resp.empty()) break; // 数据包不完整,等待后续数据
write(sockfd, resp.c_str(), resp.size()); // 发送响应
}
}
close(sockfd);
exit(0); // 子进程处理完连接后退出
}
close(sockfd); // 父进程关闭通信Socket(子进程已复制该fd)
}
}
三、客户端与服务端入口实现
1. 服务端入口:server.cpp
#include "TcpServer.hpp"
#include "ServerCal.hpp"
#include <unistd.h>
// 打印用法
static void Usage(const std::string &proc) {
std::cout << "Usage: " << proc << " port" << std::endl;
}
int main(int argc, char *argv[]) {
if (argc != 2) {
Usage(argv[0]);
exit(0);
}
uint16_t port = std::stoi(argv[1]);
ServerCal cal; // 业务逻辑实例
// 创建TcpServer,绑定Calculator回调
TcpServer server(port, std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1));
server.InitServer(); // 初始化服务(创建Socket、绑定、监听)
daemon(0, 0); // 后台运行(守护进程)
server.Start(); // 启动服务,开始处理客户端
return 0;
}
2. 客户端入口:client.cpp
客户端生成 10 次随机请求(操作数 1-100,运算符随机选+-*/%=-=&^),发送给服务端并打印响应。
#include <iostream>
#include <string>
#include <ctime>
#include <cassert>
#include <unistd.h>
#include "Socket.hpp"
#include "Protocol.hpp"
static void Usage(const std::string &proc) {
std::cout << "Usage: " << proc << " serverip serverport" << std::endl;
}
int main(int argc, char *argv[]) {
if (argc != 3) {
Usage(argv[0]);
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
// 连接服务端
Sock sockfd;
sockfd.Socket();
if (!sockfd.Connect(serverip, serverport)) return 1;
// 初始化随机数(结合PID避免多客户端重复)
srand(time(nullptr) ^ getpid());
const std::string opers = "+-*/%=-=&^"; // 支持的运算符
std::string inbuffer_stream; // 接收缓存
// 发送10次请求
for (int cnt = 1; cnt <= 10; cnt++) {
std::cout << "===============第" << cnt << "次测试===============" << std::endl;
// 生成随机请求
int x = rand() % 100 + 1; // 1-100
usleep(1234); // 避免随机数重复
int y = rand() % 100; // 0-99(故意包含0,测试除零错误)
usleep(4321);
char oper = opers[rand() % opers.size()];
Request req(x, y, oper);
req.DebugPrint(); // 打印请求(如“新请求构建完成: 10+20=?”)
// 序列化→编码→发送
std::string package;
req.Serialize(&package);
package = Encode(package);
write(sockfd.Fd(), package.c_str(), package.size());
// 接收响应→解码→反序列化→打印
char buffer[128];
ssize_t n = read(sockfd.Fd(), buffer, sizeof(buffer));
if (n > 0) {
buffer[n] = 0;
inbuffer_stream += buffer;
std::string content;
assert(Decode(inbuffer_stream, &content)); // 解码
Response resp;
assert(resp.Deserialize(content)); // 反序列化
resp.DebugPrint(); // 打印响应(如“结果响应完成, result: 30, code: 0”)
}
std::cout << "=================================================" << std::endl;
sleep(1); // 间隔1秒
}
sockfd.Close();
return 0;
}
四、项目编译与运行
1. 依赖安装
项目使用JsonCpp库(JSON 序列化),Ubuntu 下安装:
sudo apt-get install libjsoncpp-dev
2. 编译命令
# 编译服务端
g++ server.cpp -o server -ljsoncpp
# 编译客户端
g++ client.cpp -o client -ljsoncpp
3. 运行步骤
- 启动服务端(端口 8080):
./server 8080
- 启动客户端(连接本地服务端):
./client 127.0.0.1 8080
4. 运行效果示例

五、项目优化方向
-
用线程池替代多进程:多进程内存开销大,线程池可减少资源消耗(推荐用
pthread或 C++11std::thread); -
Protobuf 替代 JSON:JSON 可读性强但效率低,Protobuf 是二进制格式,序列化 / 反序列化速度更快;
-
增加配置文件:将端口、日志级别、日志路径等参数写入配置文件(如
ini/yaml),避免硬编码; -
增加监控告警:统计客户端连接数、请求成功率、错误率,异常时触发告警(如邮件 / 短信);
-
支持浮点数运算:当前仅支持整数,可扩展
Request为double x/double y,处理浮点数运算。
六、总结
本项目通过模块化设计,将网络通信、协议封装、业务逻辑、日志系统解耦,解决了 TCP 粘包、错误处理、多客户端并发等核心问题。无论是学习 C++ 网络编程,还是理解分布式系统中的协议设计,都具有参考价值。
如果你有任何疑问或优化建议,欢迎在评论区交流!
1918





