基于 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();   // 创建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. 运行步骤

  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++ 网络编程,还是理解分布式系统中的协议设计,都具有参考价值。

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

评论 40
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值