跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++

C++ 网络编程入门:TCP 协议下的简易计算器项目

本项目基于 C++ 与 TCP 协议实现了一个简易的网络计算器。采用 Fork 多进程模型处理并发连接,封装了 Socket 基础操作类以简化网络编程。通过自定义协议头解决粘包问题,支持序列化与反序列化,实现了加减乘除及取余运算,包含除零错误处理。代码结构清晰,适合初学者理解网络编程核心流程。

云间漫步发布于 2026/3/22更新于 2026/10/2487 浏览
C++ 网络编程入门:TCP 协议下的简易计算器项目

C++ 网络编程入门:TCP 协议下的简易计算器项目

基于之前的经验,结合网络套接字、序列化与反序列化以及守护进程等知识,我们来实现一个小型的 TCP 计算器项目。

文件组成

.vscode/
log.hpp          // 日志记录头文件
Makefile         // 构建脚本
Protocol.hpp     // 通信协议定义
ServerCal.hpp    // 服务器计算逻辑声明
Socket.hpp       // 套接字操作封装
TcpClient/       // 客户端相关文件夹
TcpClient.cc     // 客户端实现
TcpClient.hpp    // 客户端声明
TcpServer/       // 服务端相关文件夹
TcpServer.cc     // 服务端实现
TcpServer.hpp    // 服务端声明

TCP 服务端设计

TcpServer.hpp 核心逻辑

这里我们使用回调函数机制来处理业务逻辑,这样可以让服务器框架和具体业务解耦。

#include <functional>
#include "Socket.hpp"
#include "ServerCal.hpp"

using func_t = std::function<std::string(std::string& package)>;

class TcpServer {
public:
    TcpServer(uint16_t port, func_t callback) : port_(port), callback_(callback) {}

    void Init() {
        listensock_.Socket();
        listensock_.Bind(port_);
        listensock_.Listen();
        lg(INFO, "Server Init");
    }

    void Start() {
        while (true) {
            std::string ip;
            uint16_t port;
             fd = listensock_.(&ip, &port);
            (INFO, );

             (fork() == ) {
                (INFO, );
                listensock_.();
                std::string inbuffer_stream;
                 () {
                     buffer[];
                     n = (fd, &buffer, (buffer));
                     (n > ) {
                        (INFO, );
                        buffer[n] = ;
                        inbuffer_stream += buffer;
                         () {
                            std::string info = (inbuffer_stream);
                             (info.()) ;
                            (fd, info.(), info.());
                        }
                    }   (n == ) ;
                     ;
                }
                ();
            }
            (fd);
        }
    }

:
     port_;
    Sock listensock_;
     callback_;
};
int
Accept
lg
"Server Accept"
if
0
lg
"fork success"
Close
while
true
char
1024
int
read
sizeof
if
0
lg
"read success"
0
while
true
callback_
if
empty
break
write
c_str
size
else
if
0
break
else
break
exit
0
close
private
uint16_t
func_t

代码解析:

  • 构造函数:接收端口号和回调函数。回调函数是处理数据的核心入口。
  • Init 方法:完成 Socket 创建、绑定和监听的标准流程。
  • Start 方法:这是主循环。每次有新连接时,通过 fork() 创建子进程处理该客户端。父进程继续等待下一个连接,实现了简单的并发模型。子进程中读取数据,调用回调处理,然后写回响应。

TcpServer.cc 启动入口

#include <iostream>
#include <string>
#include <functional>
#include "log.hpp"
#include "TcpServer.hpp"
#include "Protocal.hpp"

void Usage() {
    std::cout << "\n\r" << "[Usage]:prot" << "\n" << std::endl;
}

int main(int argc, char* argv[]) {
    if (argc != 2) {
        Usage();
        return -1;
    }
    lg(INFO, "Server start");
    ServerCal cal;
    std::string port_1 = argv[1];
    uint16_t port = std::stoi(port_1);
    
    TcpServer* tsur = new TcpServer(port, std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1));
    tsur->Init();
    tsur->Start();
    return 0;
}

这里主要做了参数校验,实例化计算类对象,并使用 std::bind 将成员函数绑定为回调函数传递给服务器。

TCP 客户端实现

客户端主要负责发起连接、构造请求并解析响应。

#include <iostream>
#include <unistd.h>
#include <time.h>
#include <string>
#include "log.hpp"
#include "Socket.hpp"
#include "Protocal.hpp"

void Usage() {
    std::cout << "\n\r" << "[Usage]:port" << "\n" << std::endl;
}

int main(int argc, char* argv[]) {
    if (argc != 3) {
        Usage();
        return -1;
    }
    srand(time(nullptr));
    Sock sock;
    sock.Socket();

    std::string ip = argv[1];
    std::string port_1 = argv[2];
    uint16_t port = std::stoi(port_1);

    sock.Connect(ip, port);
    lg(INFO, "connect successful");

    std::string op = "+-*/%";
    int cnt = 1;
    while (true) {
        cnt++;
        Requset req;
        req.x_ = rand() % 100 + 1;
        req.y_ = rand() % 100;
        req.op_ = op[rand() % op.size()];

        std::string con;
        std::string out_stream;
        bool r = req.Serialize(&con);
        if (!r) lg(WARNING, "Serialize failure");
        out_stream = Encode(con);

        std::cout << "============" << " The " << cnt << " Request " << "============" << std::endl;
        std::cout << "x: " << req.x_ << std::endl;
        std::cout << "op: " << req.op_ << std::endl;
        std::cout << "y: " << req.y_ << std::endl;

        write(sock.GetFd(), out_stream.c_str(), out_stream.size());

        std::string in_stream;
        char inbuffer[1024];
        int n = read(sock.GetFd(), &inbuffer, sizeof(inbuffer));
        if (n > 0) {
            inbuffer[n] = 0;
        }
        in_stream += inbuffer;

        std::string bon;
        Response resp;
        Decode(in_stream, &bon);
        resp.Deserialize(bon);
        resp.Print();

        sleep(1);
        std::cout << "========================================" << std::endl;
    }
    return 0;
}

关键点:

  • 使用 srand(time(nullptr)) 确保每次运行生成不同的随机数。
  • 在循环中不断生成随机请求,序列化后发送,接收后解码并打印结果。
  • 注意 sleep(1) 是为了模拟真实场景中的请求间隔,避免瞬间流量过大。

计算器业务逻辑

ServerCal 类

#pragma once
#include <iostream>
#include "log.hpp"
#include "Protocal.hpp"

enum err_symbol {
    div_zero = 1,
    mod_zero
};

class ServerCal {
public:
    ServerCal() {}

    Response Calculatate(Requset &req) {
        Response resp(0, 0);
        switch (req.op_) {
            case '+': resp.result_ = req.Getx() + req.Gety(); break;
            case '-': resp.result_ = req.Getx() - req.Gety(); break;
            case '*': resp.result_ = req.Getx() * req.Gety(); break;
            case '/':
                if (req.Gety() == 0) { resp.code_ = div_zero; break; }
                resp.result_ = req.Getx() / req.Gety();
                break;
            case '%':
                if (req.Gety() == 0) { resp.code_ = mod_zero; break; }
                resp.result_ = req.Getx() % req.Gety();
                break;
            default: break;
        }
        return resp;
    }

    std::string Calculator(std::string &package) {
        std::string context;
        bool rDecode = Decode(package, &context);
        if (!rDecode) return "";

        Requset req;
        req.Deserialize(context);
        Response resp;
        resp = Calculatate(req);
        sleep(3); // 模拟计算延时

        std::string in;
        bool r = resp.Serialize(&in);
        if (!r) return "";
        context = "";
        context = Encode(in);
        return context;
    }

    ~ServerCal() {}
};

这里处理了基本的四则运算及取余,特别增加了除零检查,返回相应的错误码。

协议与数据封装

编码与解码

为了处理粘包问题,我们在应用层设计了简单的长度前缀协议。

const std::string space_sep = " ";
const std::string protocal_sep = "\n";

std::string Encode(std::string &context) {
    std::string s = std::to_string(context.size());
    s += protocal_sep;
    s += context;
    s += protocal_sep;
    return s;
}

bool Decode(std::string &package, std::string *context) {
    auto pos = package.find(protocal_sep);
    if (pos == std::string::npos) return false;
    std::string len_str = package.substr(0, pos);
    std::size_t len = std::stoi(len_str);
    int total_len = len + len_str.size() + 2;
    if (package.size() < total_len) return false;
    *context = package.substr(pos + 1, len);
    package.erase(0, total_len);
    return true;
}

请求与响应结构

支持两种模式:自定义字符串格式或 JSON 格式(通过宏控制)。

class Requset {
public:
    Requset(int data1, char op, int data2) : x_(data1), op_(op), y_(data2) {}
    Requset() {}
    ~Requset() {}

    void Print() {
        std::cout << x_ << op_ << y_ << std::endl;
    }

    bool Serialize(std::string *out) {
#ifdef Myself
        std::string s = std::to_string(x_);
        s += space_sep;
        s += op_;
        s += space_sep;
        s += std::to_string(y_);
        *out = s;
        return true;
#else
        Json::Value root;
        root["x"] = x_;
        root["y"] = y_;
        root["op"] = op_;
        Json::FastWriter w;
        *out = w.write(root);
        return true;
#endif
    }

    bool Deserialize(const std::string &in) {
#ifdef Myself
        auto pos1 = in.find(space_sep);
        if (pos1 == std::string::npos) return false;
        std::string part_x = in.substr(0, pos1);
        auto pos2 = in.rfind(space_sep);
        std::string oper = in.substr(pos1 + 1, pos2);
        std::string part_y = in.substr(pos2 + 1);
        if (pos2 != pos1 + 2) return false;
        op_ = in[pos1 + 1];
        x_ = std::stoi(part_x);
        y_ = std::stoi(part_y);
        return true;
#else
        Json::Value root;
        Json::Reader r;
        r.parse(in, root);
        x_ = root["x"].asInt();
        y_ = root["y"].asInt();
        op_ = root["op"].asString()[0];
        return true;
#endif
    }

    int Getx() { return x_; }
    int Gety() { return y_; }
    char Getop() { return op_; }

private:
    int x_;
    char op_;
    int y_;
};

class Response {
public:
    Response(int ret, int code) : result_(ret), code_(code) {}
    Response() {}
    ~Response() {}

    bool Serialize(std::string *out) {
#ifdef Myself
        std::string s = std::to_string(result_);
        s += space_sep;
        s += std::to_string(code_);
        *out = s;
        return true;
#else
        Json::Value root;
        root["result"] = result_;
        root["code"] = code_;
        Json::FastWriter w;
        *out = w.write(root);
        return true;
#endif
    }

    bool Deserialize(const std::string &in) {
#ifdef Myself
        auto pos = in.find(space_sep);
        std::string res = in.substr(0, pos);
        std::string code = in.substr(pos + 1);
        if (pos != in.rfind(space_sep)) return false;
        result_ = std::stoi(res);
        code_ = std::stoi(code);
        return true;
#else
        Json::Value root;
        Json::Reader r;
        r.parse(in, root);
        result_ = root["result"].asInt();
        code_ = root["code"].asInt();
        return true;
#endif
    }

    void Print() {
        std::cout << "result_: " << result_ << " code_: " << code_ << std::endl;
    }

private:
    int result_;
    int code_;
};

网络组件封装

为了简化 Socket 操作,我们封装了一个 Sock 类。

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"

int backlog = 10;

enum err {
    Socketerr = 1,
    Bindeterr,
    Listeneterr,
    Accepteterr
};

class Sock {
public:
    Sock() {}
    ~Sock() {}

    void Socket() {
        sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd_ < 0) {
            lg(FATAL, "Socket error: %d,%s", errno, strerror(errno));
            exit(Socketerr);
        }
    }

    void Bind(uint16_t port) {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        bzero(&peer, len);
        peer.sin_port = htons(port);
        peer.sin_family = AF_INET;
        peer.sin_addr.s_addr = INADDR_ANY;
        if (bind(sockfd_, (struct sockaddr*)&(peer), len) < 0) {
            lg(FATAL, "Bind error: %d,%s", errno, strerror(errno));
            exit(Bindeterr);
        }
    }

    void Listen() {
        if (listen(sockfd_, backlog) < 0) {
            lg(FATAL, "Listen error: %d,%s", errno, strerror(errno));
            exit(Listeneterr);
        }
    }

    int Accept(std::string *clientip, uint16_t *clientport) {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        bzero(&peer, len);
        int newfd = accept(sockfd_, (struct sockaddr*)&(peer), &len);
        if (newfd < 0) {
            lg(FATAL, "Accept error: %d,%s", errno, strerror(errno));
            exit(Accepteterr);
        }
        char ip[64];
        inet_ntop(AF_INET, &peer.sin_addr.s_addr, ip, sizeof(ip));
        *clientip = ip;
        *clientport = ntohs(peer.sin_port);
        return newfd;
    }

    bool Connect(const std::string &ip, const uint16_t &port) {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        bzero(&peer, len);
        peer.sin_addr.s_addr = inet_addr(ip.c_str());
        peer.sin_port = htons(port);
        peer.sin_family = AF_INET;
        int n = connect(sockfd_, (struct sockaddr*)&(peer), len);
        if (n < 0) {
            lg(WARNING, "Connect error: %d,%s", errno, strerror(errno));
            return false;
        }
        return true;
    }

    void Close() {
        close(sockfd_);
    }

    int GetFd() {
        return sockfd_;
    }

private:
    int sockfd_;
};

该类封装了标准的 Socket 生命周期管理,包括创建、绑定、监听、接受连接和关闭,并在出错时输出详细日志。

效果展示

效果图

源码地址

https://gitee.com/chian-ocean/linux/commit/b5a52c8da2fde96cbd683201e63353fa5dbc9b27

目录

  1. C++ 网络编程入门:TCP 协议下的简易计算器项目
  2. 文件组成
  3. TCP 服务端设计
  4. TcpServer.hpp 核心逻辑
  5. TcpServer.cc 启动入口
  6. TCP 客户端实现
  7. 计算器业务逻辑
  8. ServerCal 类
  9. 协议与数据封装
  10. 编码与解码
  11. 请求与响应结构
  12. 网络组件封装
  13. 效果展示
  14. 源码地址

更多推荐文章

查看全部
  • 以太坊 Optimism 关键漏洞修复:攻击者可无限铸造代币获百万赏金
  • 求两个单链表公共后缀的起始位置算法实现与分析
  • C++中string的常用函数用法总结
  • OpenClaw 汉化中文版部署指南:npm、Docker 安装与环境配置
  • C++ 多线程:深入理解 Atomic 与 Volatile 的语义区别
  • OpenClaw 接入 QQ 机器人完整指南
  • 网络安全基础知识详解与常见问答
  • 基于 AutoGPT 与 Python 的 AI 智能体开发实战
  • ESLint 从原理到实践:构建高质量 JS/TS 代码
  • Ollama v0.17.0 更新:OpenClaw 自动安装、Web 搜索与 Tokenizer 性能优化
  • 浏览器缓存机制详解:彻底解决前端代码更新缓存问题
  • Rust WebAssembly 开发实战:构建高性能前端应用
  • 知网 AIGC 检测率过高应对策略与实践
  • OpenClaw 跨平台 AI 助手使用指南:安装配置与核心功能详解
  • Windows Docker 命令行使用手册
  • Spring Boot 虚拟线程时代:WebFlux 与 WebMVC 选型指南
  • Linux 调试器 GDB/Cgdb 使用指南
  • WhisperLiveKit 实时语音识别指南:从安装到生产部署
  • Qt 前后端通信:QWebChannel Js 与 C++ 互操作原理及示例
  • Java 常见排序算法实现与解析

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online