当前位置: 首页 > news >正文

基于 TCP 协议的 C++ 计算器项目实现:从网络通信到协议封装

在网络编程中,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.Debuglog.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();   // 创建Socketlistensock.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);
#endifreturn true;}// 反序列化:字符串 → Requestbool Deserialize(const std::string &in) {
#ifdef MySelf// 解析"10 + 20" → x=10, op='+', y=20std::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();
#endifreturn 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-100usleep(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. 运行步骤

  1. 启动服务端(端口 8080):
./server 8080
  1. 启动客户端(连接本地服务端):
./client 127.0.0.1 8080

4. 运行效果示例

五、项目优化方向

  1. 用线程池替代多进程:多进程内存开销大,线程池可减少资源消耗(推荐用pthread或 C++11std::thread);

  2. Protobuf 替代 JSON:JSON 可读性强但效率低,Protobuf 是二进制格式,序列化 / 反序列化速度更快;

  3. 增加配置文件:将端口、日志级别、日志路径等参数写入配置文件(如ini/yaml),避免硬编码;

  4. 增加监控告警:统计客户端连接数、请求成功率、错误率,异常时触发告警(如邮件 / 短信);

  5. 支持浮点数运算:当前仅支持整数,可扩展Requestdouble x/double y,处理浮点数运算。

六、总结

本项目通过模块化设计,将网络通信、协议封装、业务逻辑、日志系统解耦,解决了 TCP 粘包、错误处理、多客户端并发等核心问题。无论是学习 C++ 网络编程,还是理解分布式系统中的协议设计,都具有参考价值。

如果你有任何疑问或优化建议,欢迎在评论区交流!


文章转载自:

http://ER3Tc3Ub.jqsyp.cn
http://LfIYktv5.jqsyp.cn
http://BTEaz7ZR.jqsyp.cn
http://3cZgDKQ1.jqsyp.cn
http://h3Si8TR9.jqsyp.cn
http://DJJlmUq0.jqsyp.cn
http://HVb81cCk.jqsyp.cn
http://fGhQA1eM.jqsyp.cn
http://yCk99iFk.jqsyp.cn
http://pMRyQk5P.jqsyp.cn
http://UYkJ9i37.jqsyp.cn
http://wCFALQ4N.jqsyp.cn
http://Ls1XrfdH.jqsyp.cn
http://xzm5MWfE.jqsyp.cn
http://TUPraDNR.jqsyp.cn
http://fj67Dsmz.jqsyp.cn
http://Ke9GNHIu.jqsyp.cn
http://kZJzMMQl.jqsyp.cn
http://Phr7mpZX.jqsyp.cn
http://KAuDp277.jqsyp.cn
http://fn9H7oQ5.jqsyp.cn
http://Q5cIIwDX.jqsyp.cn
http://lkMsYClf.jqsyp.cn
http://HDgrBcmt.jqsyp.cn
http://CbYRVh5N.jqsyp.cn
http://nDGNFC5I.jqsyp.cn
http://Lo0CiyUk.jqsyp.cn
http://s4k15ZSA.jqsyp.cn
http://WqHEU1Ey.jqsyp.cn
http://ctElwtwv.jqsyp.cn
http://www.dtcms.com/a/387597.html

相关文章:

  • 【分布式技术】深入理解AMQP(高级消息队列协议)
  • 海外短剧分销系统开发:技术栈选型与核心模块实现指南
  • 每日前端宝藏库 | Toastify.js ✨
  • Nuxt3:自动导入渲染模式服务器引擎生产部署模块化
  • 打造高效对账单管理组件:Vue3 + Element Plus 实现客户账单与单据选择
  • 第二章 Arm C1-Premium Core技术架构
  • Bartender 6 多功能菜单栏管理(Mac)
  • 嵌入式科普(38)C语言预编译X-Macros深度分析和实际项目代码分享
  • Docker compose 与 docker swarm 的区别
  • 【嵌入式硬件实例】-555定时器实现水位检测
  • AbMole小课堂丨R-spondin-1(RSPO1):高活性Wnt通路激活剂,如何在多种类器官/干细胞培养中发挥重要功能
  • 【C语言代码】打印九九乘法口诀表
  • vue3和element plus, node和express实现大文件上传, 分片上传,断点续传完整开发代码
  • electron-egg使用ThinkPHP项目指南
  • 温州工业自动化科技工厂如何实现1台服务器10个研发设计同时用
  • 如何用PM2托管静态文件
  • Java程序设计:基本数据类型
  • 在k8s环境下部署kanboard项目管理平台
  • 为什么 MySQL utf8 存不下 Emoji?utf8mb4 实战演示
  • 2025 年 PHP 常见面试题整理以及对应答案和代码示例
  • (二十五)、在 k8s 中部署证书,为网站增加https安全认证
  • 风机巡检目前有什么新技术?
  • 震坤行工业超市开放平台接口实战:工业品精准检索与详情解析全方案
  • 河南萌新联赛2025第(八)场:南阳理工学院
  • docker回收和mysql备份导入导致数据丢失恢复---惜分飞
  • 「Memene 摸鱼日报 2025.9.17」上海张江人工智能创新小镇正式启动,华为 DCP 技术获网络顶会奖项
  • 【数据结构】顺序表,ArrayList
  • 第十二章 Arm C1-Premium GIC CPU接口详解
  • 【数据结构---并查集】(并查集的原理,实现与应用)
  • 【数据结构-KMP算法(学习篇)】