《Linux 网络编程》应用层自定义协议(二):工业级分层 TCP 自定义协议网络计算器服务从零实现 小叶-duck个人主页❄️个人专栏《Data-Structure-Learning》《C入门到进阶自我学习过程记录》《Linux系统从入门到实践》《Linux网络从入门到实践》《Qt 方寸极境》 《MySQL》✨未择之路不须回头已择之路纵是荆棘遍野亦作花海遨游目录前言一、核心模块源码深度解析1.1 Socket 封装Socket.hpp模板方法模式1.1.1 模板方法模式思想1.1.2 Socket 基类设计1.1.3 子类 TcpSocket 实现Accept Recv 设计注意事项1.2 网络地址封装InetAddr.hpp1.3 线程安全互斥锁封装Mutex.hpp1.4 通用工具头文件Common.hpp二、整体架构设计三层解耦的工业级架构2.1 三层架构设计2.2 三层架构的核心职责2.3 核心设计思想回调函数解耦2.4 网络传输层TCP 服务器实现(TcpServer.hpp)2.5 协议层核心实现Protocol.hppGetRequest GetResponse 设计注意事项2.6 业务层计算器实现(Calculator.hpp)三. 服务端与客户端完整实现3.1 服务端实现OnlineCalServer.cc3.2 客户端实现OnlineCalClient.cc3.3 编译与运行结束语前言在上一篇文章《应用层自定义协议(一)序列化、sk_buff 与粘包半包处理》中我们已经梳理清楚 TCP 粘包问题成因、自定义报文协议、序列化反序列化等网络编程相关理论知识点。纸上得来终觉浅理论知识需要落地到真实项目当中加深理解。本文依托一套完整可运行的工业级 C 代码手把手带领大家完成一套三层解耦架构的在线计算器 TCP 服务。我们会从底层 Socket 类封装、网络地址封装、互斥锁与通用工具组件开始讲解逐层向上完成协议层封包解包逻辑再编写上层计算器业务代码最后完成服务端、客户端完整工程实现以及编译部署完整复刻企业网络服务的开发流程。文中全部代码均经过实测验证读完本篇你不仅可以吃透 TCP 自定义协议的工程落地细节还能掌握模板方法模式、回调解耦、RAII 资源管理等经典技术思想在网络项目中的实践以上内容同样也是后端开发面试的高频考察点。一、核心模块源码深度解析1.1 Socket 封装Socket.hpp模板方法模式1.1.1 模板方法模式思想设计模式模板方法模式定义算法骨架具体步骤延迟到子类实现。设计目的统一 TCP/UDP/Unix 域套接字的创建流程代码复用、解耦、易扩展。核心基类Socket定义纯虚接口模板方法组合接口流程子类TcpSocket重写接口。优势新增套接字类型只需继承基类无需修改原有代码符合开闭原则。1.1.2 Socket 基类设计基类定义套接字必备操作创​建、绑定、监听、接收、连接、收发数据、关闭。 ​模板方法BuildTcpSocketMethod服务端套接字流程创建→绑定→监听。BuildTcpClientSocketMethod客户端套接字流程创建→连接。析构函数设为虚函数保证子类析构能被正确调用。const static int g_backlog 16; class Socket { public: // 纯虚接口子类必须实现 virtual void SocketOrDie() 0; virtual void BindOrDie(uint16_t port) 0; virtual void ListenOrDie(int backlog) 0; virtual std::shared_ptrSocket Accept(InetAddr *clientptr) 0; virtual void Close() 0; virtual int Recv(std::string *out) 0; virtual int Send(const std::string in) 0; virtual int Connect(uint16_t port, const std::string ip) 0; virtual ~Socket() {} // 虚析构防止内存泄漏 public: // 纯虚接口子类必须实现 void BuildTcpSocketMethod(uint16_t port, int backlog g_backlog) { SocketOrDie(); BindOrDie(port); ListenOrDie(backlog); } // 模板方法客户端套接字创建流程 void BuildTcpClientSocketMethod() { SocketOrDie(); } // void BuildUdpSocketMethod() // { // SocketOrDie(); // BindOrDie(); // } };1.1.3 子类 TcpSocket 实现继承Socket基类重写所有纯虚接口实现 TCP 套接字具体操作。CreateSocketOrDie创建 TCP 套接字失败直接退出。BindSocketOrDie绑定端口号失败直接退出。ListenSocketOrDie启动监听队列长度gbacklog16。Accepter接收客户端连接返回新套接字的智能指针。Recv/Send封装recv/send系统调用Recv自动拼接缓冲区 数据。Close关闭文件描述符防止资源泄漏。class TcpSocket : public Socket { public: TcpSocket() : _sockfd(-1) { } // 构造函数重载 TcpSocket(int sockfd) : _sockfd(sockfd) { } void SocketOrDie() override { _sockfd socket(AF_INET, SOCK_STREAM, 0); if (_sockfd 0) { LOG(LogLevel::FATAL) socket error; exit(SOCKET_ERR); } LOG(LogLevel::INFO) socket success; } void BindOrDie(uint16_t port) override { InetAddr addr(port); int n bind(_sockfd, addr.Addr(), addr.AddrLen()); if (n 0) { LOG(LogLevel::FATAL) bind error; exit(BIND_ERR); } LOG(LogLevel::INFO) bind success; } void ListenOrDie(int backlog) override { int n listen(_sockfd, backlog); if (n 0) { LOG(LogLevel::FATAL) listen error; exit(LISTEN_ERR); } LOG(LogLevel::INFO) listen success; } // int Accept(InetAddr *clientptr) override std::shared_ptrSocket Accept(InetAddr *clientptr) override { struct sockaddr_in peer; socklen_t len sizeof(peer); int fd accept(_sockfd, (struct sockaddr *)peer, len); if (fd 0) { // return -1; return nullptr; } // 关键点clientptr是输出型参数我们需要将获取到的peer传入到clientptr进行向外输出 clientptr-SetAddr(peer); // 这样外部传入InetAddr指针就能获取到accept输出的struct sockaddr_in的信息字段 // return fd; return std::make_sharedTcpSocket(fd); } void Close() { if (_sockfd 0) close(_sockfd); } int Recv(std::string *out) override { char buffer[1024]; int n recv(_sockfd, buffer, sizeof(buffer), 0); // int n read(_sockfd, buffer, sizeof(buffer)); //也可以 if (n 0) { buffer[n] 0; *out buffer; } return n; } int Send(const std::string in) override { return send(_sockfd, in.c_str(), in.size(), 0); // return write(_sockfd, in.c_str(), in.size()); //也可以 } int Connect(uint16_t port, const std::string ip) override { InetAddr server(port, ip); return connect(_sockfd, server.Addr(), server.AddrLen()); } ~TcpSocket() { } private: int _sockfd; };核心设计解读模板方法模式基类定义了BuildTcpSocketMethod和BuildTcpClientSocketMethod两个模板方法封装了服务端和客户端 Socket 构建的固定流程。子类只需实现具体的步骤无需关心整体流程。OrDie 设计对于 Socket 创建、绑定、监听这些关键操作如果失败直接退出程序。因为这些操作失败意味着服务无法正常启动继续运行没有意义。关键的拼接逻辑Recv方法中使用*outstr buffer而不是覆盖这是解决 TCP 半包问题的基础。不完整的报文会被拼接在缓冲区中等待下次读取。面向对象封装将文件描述符封装在类内部通过成员函数操作避免了全局文件描述符带来的混乱。Accept Recv 设计注意事项1.2 网络地址封装InetAddr.hpp在网络编程开发过程中IP 地址、端口号的处理十分高频其中就包含主机字节序与网络字节序之间的相互转换。原生 Socket 的 API 接口较为底层每次开发都要手动调用转换函数重复代码多也容易出现字节序遗忘转换的低级 bug。InetAddr类把这部分繁琐的底层逻辑做封装对外暴露简洁统一的调用接口简化网络地址相关业务开发。封装sockaddr_in结构体统一处理网络字节序↔主机字节序转换。提供Addr()/AddrLen()方法直接适配bind/connect/accept系统调用。StringAddress()将地址转为[IP:端口]格式用于日志打印。#ifndef INETADDR_HPP #define INETADDR_HPP #include Common.hpp // 网络地址和主机地址之间进行转换的类 class InetAddr { public: // 无参构造 InetAddr() {} // 构造函数的函数重载 // 网络转本地主要用于接收来自网络客户端消息后解析获取本地ip地址和端口号 InetAddr(struct sockaddr_in addr) { // _ip inet_ntoa(_addr.sin_addr); // _port ntohs(_addr.sin_port); // 复用SetAddr函数 SetAddr(addr); } // 本地转网络主要用于客户端已知服务端ip地址和端口号填充获取struct sockaddr_in _addr向服务端发送消息 InetAddr(uint16_t port, const std::string ip) : _port(port), _ip(ip) { bzero(_addr, sizeof(_addr)); _addr.sin_family AF_INET; _addr.sin_port htons(_port); _addr.sin_addr.s_addr inet_addr(_ip.c_str()); } // 本地转网络我们还能再写一个没有ip参数的函数重载 // 为了方便服务端的使用构建struct sockaddr_in(服务端无需传入ip地址INADDR_ANY) InetAddr(uint16_t port) : _port(port), _ip(0) { bzero(_addr, sizeof(_addr)); _addr.sin_family AF_INET; _addr.sin_port htons(_port); _addr.sin_addr.s_addr INADDR_ANY; } // 获取主机字节序的端口号 uint16_t Port() { return _port; } // 获取点分十进制字符串 IP std::string Ip() { return _ip; } struct sockaddr_in Addr_in() { return _addr; } // 获取指向底层 sockaddr 结构的指针用于 sendto 等系统调用 struct sockaddr *Addr() { return (struct sockaddr *)_addr; } // 获取底层结构体的大小用于套接字系统调用时的长度参数 socklen_t AddrLen() { return sizeof(_addr); } // 将地址信息转化为易读的字符串格式如 [127.0.0.1:8080] // 常用于打印日志信息 std::string StringAddress() { return [ _ip : std::to_string(_port) ]; } // 补充功能重载运算符用于在在线用户列表中查找指定客户端 bool operator(const InetAddr who) { return this-_ip who._ip this-_port who._port; } // 补充功能accept获取对于客户端套接字用于InetAddr参数向外输出 void SetAddr(struct sockaddr_in addr) { _ip inet_ntoa(addr.sin_addr); _port ntohs(addr.sin_port); _addr addr; } ~InetAddr() { } private: // 本地主机格式的地址信息 uint16_t _port; // 客户端的端口号 std::string _ip; // 客户端的IP地址 // 原始网络格式的地址结构体 struct sockaddr_in _addr; // 客户端的struct sockaddr_in }; #endif核心设计解读双构造函数的设计思想提供两套构造逻辑分别适配「主机字节序转网络字节序」、「网络字节序转主机字节序」两类典型场景。完整覆盖服务端 bind 绑定监听、客户端发起 connect 连接、获取对端连接地址等绝大多数网络地址处理场景。字节序自动屏蔽底层细节字节序转换的逻辑全部封装在类内部完成上层业务代码无需手动调用htons、ntohs等接口开发者不用关心大小端的底层差异降低字节序相关错误的概率。面向调试的日志友好接口对外提供StringAddress()成员方法能够把网络地址格式化输出为[IP:Port]的可读字符串格式方便日志打印、问题排查提升调试效率。1.3 线程安全互斥锁封装Mutex.hpp多线程并发场景下多个执行流会同时访问共享临界资源极易引发数据竞争互斥锁就是用来解决该问题的核心同步工具。本模块对 POSIX 原生互斥锁进行二次封装并且实现 RAII 风格的锁守卫类依靠 C 对象生命周期自动完成锁的申请与释放降低手动操作锁带来的各类隐患。// 互斥锁的封装 #ifndef MUTEX_HPP #define MUTEX_HPP #include iostream #include pthread.h #include string namespace MutexModule { class Mutex { public: Mutex() { pthread_mutex_init(_mutex, nullptr); // std::cout mutex init success std::endl; } void Lock() { int n pthread_mutex_lock(_mutex); if (n ! 0) { std::cerr pthread_mutex_lock false std::endl; } } void Unlock() { int n pthread_mutex_unlock(_mutex); if (n ! 0) { std::cerr pthread_mutex_unlock false std::endl; } } ~Mutex() { int n pthread_mutex_destroy(_mutex); if (n ! 0) { std::cerr pthread_mutex_destroy false std::endl; } else { // std::cout mutex destroy success std::endl; } } pthread_mutex_t *GetMutex() { return _mutex; } private: pthread_mutex_t _mutex; }; // RAII风格的互斥锁的封装(实现自动解锁) class LockGuard { public: LockGuard(Mutex mutex) : _mutex(mutex) { _mutex.Lock(); } ~LockGuard() { _mutex.Unlock(); } private: Mutex _mutex; }; } #endif核心设计解读RAII 自动锁管理LockGuard锁守卫类充分利用 C RAII 特性对象构造阶段完成加锁对象析构时自动执行解锁逻辑。即便函数内部出现异常抛出、提前 return 退出等情况锁依旧可以被正常释放从根源上规避因忘记解锁造成的死锁问题。兼容原生底层接口对外提供origin()接口能够获取底层原生 pthread_mutex_t 锁指针方便对接依赖 POSIX 锁的第三方组件做到上层封装的同时保留底层接口的拓展能力。1.4 通用工具头文件Common.hppCommon.hpp 封装项目通用基础能力统一定义程序退出错误码与不可拷贝基类收拢项目所需各类头文件依赖是整个网络项目的公共基础头文件。#pragma once #include iostream #include string #include memory #include cstdlib #include unistd.h #include functional #include sys/socket.h #include sys/types.h #include netinet/in.h #include arpa/inet.h #include sys/wait.h #include strings.h enum ExitCode { OK 1, USAGE_ERR, SOCKET_ERR, BIND_ERR, LISTEN_ERR, CONNECT_ERR, FORK_ERR, OPEN_ERR }; // 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化 // delete掉唯一父类的拷贝构造和赋值拷贝则所有子类服务器也就全都不能拷贝了 class Nocopy { public: Nocopy() {} ~Nocopy() {} // 禁止父类的拷贝构造和赋值拷贝 Nocopy(const Nocopy ) delete; const Nocopy operator(const Nocopy ) delete; };源码核心解读定义一套统一的程序退出错误码 ExitCode用于标识 socket、bind、listen、fork 等各类系统调用失败场景统一项目异常退出标识实现 Nocopy 禁止拷贝基类继承该类的子类自动禁用拷贝构造与赋值重载避免对象拷贝引发的资源管理问题网络服务类可以直接继承 Nocopy。Common.hpp 集中收纳 C 标准库、Linux 系统网络编程头文件项目其余代码只需要引入这一个头文件就可以获得网络开发需要的基础依赖简化项目头文件引入工作。二、整体架构设计三层解耦的工业级架构2.1 三层架构设计正式编码实现之前需要先完成整体架构规划。一套高质量网络服务程序应当遵循高内聚、低耦合的设计原则方便后续迭代拓展与维护。这里我们选用经典三层分层架构进行开发┌─────────────────┐ │ 业务层 │ Calculator.hpp负责具体的计算逻辑 ├─────────────────┤ │ 协议层 │ Protocol.hpp负责封包、解包、序列化、反序列化 ├─────────────────┤ │ 网络通信层 │ Socket.hpp TcpServer.hpp负责TCP连接与数据收发 └─────────────────┘业务层Calculator负责具体的业务逻辑计算器运算。协议层Protocol负责封包 / 解包、序列化 / 反序列化、粘包解决。网络通信层TcpServer/Socket负责连接管理、IO 收发。解耦方式回调函数注入层与层之间互不感知内部实现。2.2 三层架构的核心职责业务层聚焦业务本身逻辑对底层网络传输细节无感知。接收协议层交付好的结构化请求执行业务处理之后输出结构化响应交还给协议层。协议层充当网络通信层和业务层之间的中间桥梁。把网络传来的原始字节流解析为结构化请求对象同时将业务层输出的响应对象序列化为网络字节流并且处理 TCP 粘包、半包这类经典网络问题。网络通信层只专注原始字节流的接收和发送完全不感知上层业务语义。对外封装统一 Socket 接口接管 TCP 连接创建、端口监听、连接接受、数据读写等底层网络操作。2.3 核心设计思想回调函数解耦网络层收到原始字节数据后不做任何业务解析而是通过预先注册的回调函数把数据原样传递给协议层处理。协议层从字节流中解析出完整的结构化请求后再通过回调函数把请求对象交给业务层执行具体业务逻辑。业务层处理完成将响应结果返回给协议层由协议层负责序列化、封包再交回网络层进行发送给客户端。这种设计的核心优势在于各层可以独立开发、独立测试、独立替换层与层之间只依赖约定好的接口不依赖具体实现。2.4 网络传输层TCP 服务器实现(TcpServer.hpp)基于封装好的 Socket 类我们实现一个多进程的 TCP 服务器每个客户端连接由一个独立的子进程处理。#ifndef TCPSERVER_HPP #define TCPSERVER_HPP #include Socket.hpp #include functional using namespace SocketModule; // 回调处理 using callback_t std::functionbool(InetAddr , std::string , std::string *); class TcpServer : public Nocopy { public: TcpServer(uint16_t port, callback_t service) : _port(port), _listen_sockptr(std::make_uniqueTcpSocket()), _isrunning(false), _service(service) { _listen_sockptr-BuildTcpSocketMethod(_port); } void Start() { _isrunning true; while (_isrunning) { InetAddr client; // int sockfd _listen_sockptr-Accept(client); //也不是不行只是后续不能面向对象使用接口 auto sockptr _listen_sockptr-Accept(client); // 传入client地址就能获取到对应accept的客户端信息 if (sockptr nullptr) { LOG(LogLevel::WARNING) accept error...; continue; } LOG(LogLevel::INFO) client.StringAddress() accpet success...; // sockptr client // 使用多进程 pid_t id fork(); if (id 0) { LOG(LogLevel::FATAL) fork error; exit(FORK_ERR); } else if (id 0) { // 子进程 if (fork() 0) exit(OK); // 孙子进程(子进程的子进程) // 关闭不需要的监听套接字(在哪 —— _listen_sockptr) _listen_sockptr-Close(); Run(sockptr, client); sockptr-Close(); exit(OK); } else { // 父进程 // 关闭不需要的accept返回的文件描述符(在哪 —— sockptr)只负责监听 // close(sockfd); //如果Accept返回值是int那么这里就只能调用系统调用close了 sockptr-Close(); waitpid(id, nullptr, 0); } } _isrunning false; } //TCP层只负责报文的收发 void Run(std::shared_ptrSocket sockptr, InetAddr client) { // 设计的是长服务只要客户端不断开就一直循环处理请求 std::string buffer_queue; while (true) { std::string send_str; int n sockptr-Recv(buffer_queue); if (n 0) { // 执行任务(回调处理TcpServer不关心怎么处理任务) if(!_service(client, buffer_queue, send_str)) { continue; } sockptr-Send(send_str); } else if(n 0) { LOG(LogLevel::INFO) client.StringAddress() Quit...; break; } else { LOG(LogLevel::WARNING) client.StringAddress() recv error; break; } } } ~TcpServer() { } private: uint16_t _port; std::unique_ptrSocket _listen_sockptr; bool _isrunning; callback_t _service; // 回调参数 }; #endif核心设计解读多进程服务模型每接入一个客户端就创建独立子进程专门处理该连接。该方案编码简单、稳定性强单个客户端发生崩溃、异常只会毁掉对应的子进程不会干扰主服务与其他客户端。适合并发连接数量不高的业务场景。子进程资源自动回收注册信号signal(SIGCHLD, SIG_IGN)交给操作系统内核完成子进程的善后清理工作不用手动调用 wait/waitpid从源头规避僵尸进程问题。文件描述符管控fork 之后父子进程会复制文件描述符父子双方分别把自身不再使用的 fd 执行 close 关闭杜绝文件描述符泄漏防止后期出现无法新建连接的问题。回调实现模块解耦TcpServer 模块的职责仅限于网络层面的数据收发完全不介入业务逻辑。依靠回调接口把收到的数据向上交付上层模块处理做到网络通信层和上层协议、业务代码解耦。2.5 协议层核心实现Protocol.hpp协议层是整个自定义协议的核心负责解决序列化 / 反序列化和粘包 / 半包问题两大核心问题。#ifndef PROTOCOL_HPP #define PROTOCOL_HPP #include Common.hpp #include Socket.hpp #include jsoncpp/json/json.h using namespace SocketModule; // 实现一个自定义的网络版本计算器 // 约定好各个字段的含义本质就是约定好协议 // 请求报文client - server // client-server class Request { public: Request() { } Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper) { } // 序列化将结构化的请求对象转换成Json字符串 std::string Serialize() { Json::Value root; root[x] _x; root[y] _y; root[oper] _oper; Json::FastWriter writer; std::string s writer.write(root); return s; } // 反序列化 bool Deserialize(std::string in) { Json::Value root; Json::Reader reader; bool ok reader.parse(in, root); if (ok) { _x root[x].asInt(); _y root[y].asInt(); _oper root[oper].asInt(); } return ok; } int X() const { return _x; } int Y() const { return _y; } char Oper() const { return _oper; } ~Request() { } private: int _x; int _y; char _oper; // - * / % —— 要求:_x _oper _y - 10 20 }; // 应答报文server - client // server-client class Responce { public: Responce() { } Responce(int result, int code) : _result(result), _code(code) { } // 序列化 std::string Serialize() { Json::Value root; root[result] _result; root[code] _code; Json::FastWriter writer; std::string s writer.write(root); return s; } // 反序列化 bool Deserialize(std::string in) { Json::Value root; Json::Reader reader; bool ok reader.parse(in, root); if (ok) { _result root[result].asInt(); _code root[code].asInt(); } return ok; } void SetResult(int res) { _result res; } void SetCode(int code) { _code code; } void ShowResult() { std::cout 计算结果是: _result [ _code ] std::endl; } ~Responce() { } private: int _result; // 运算结果无法区分清楚应答的结果是正常计算结果还是异常值 int _code; // 规定 0success; 1/2/3/4.. : 不同的运算异常情况 }; const std::string sep \r\n; // 分隔符 using func_t std::functionResponce(const Request ); class Protocol { public: Protocol() { } Protocol(func_t func) : _func(func) { } std::string EnCode(const std::string jsonstr) { // 50\r\n{x: 10, y: 20, oper: }\r\n // 1. 将报文长度转为字符串 std::string jsonstr_len std::to_string(jsonstr.size()); // 2. 按照约定格式封装报文 return jsonstr_len sep jsonstr sep; // 其实就可以理解为是在应用层封装报头 } bool DeCode(std::string buffer, std::string *package) { // 1. 查找第一个分隔符提取报文长度 ssize_t pos buffer.find(sep); if (pos std::string::npos) { // 说明根本没有找到分隔符\r\n说明是上面的前两种情况也就说明是没有读取到完整报文(其实都没有报文...) return false; // 让调用方继续从内核中读取数据直到是至少一个完整报文的情况 } // 2. 提取报文长度字符串转为整型 std::string package_len_str buffer.substr(0, pos); // 获取理想一个完整报头长度的字符串 int package_len std::stoi(package_len_str); // 获取理想一个完整报头的长度 // 3. 计算完整报文的总长度 // 总长度 长度字符串长度 2个分隔符长度 业务报文长度 // 到这一定存在报文的内容但是一定是一个完整的报文吗 int target_len package_len_str.size() package_len 2 * sep.size(); // 理想一个封装报头后完整报文的长度 // 4. 判断缓冲区中是否有完整的报文 if (buffer.size() target_len) { // 这种情况就对应于上面的第四种情况也就是存在报文但不是一个完整的报文 // 也就和上面没有找到分隔符一样需要让调用方继续从内核中读取数据直到是至少一个完整报文的情况 return false; } // 5. 提取完整的业务报文 // 到这里就说明了一定是存在至少一个完整的报文内容了(buffer.size() target_len) // 所以接下来我们就需要将一个完整的报文取出来并且在buffer中进行剔除用于后续再次使用 *package buffer.substr(pos sep.size(), package_len); // 所以这就是为什么函数第二个参数是指针因为是输出型参数我们需要将获取到的完整报文传出去 // 我们只需要报文的内容而不需要封装的报头所以截取位置并不是从0开始这其实也就可以理解为是在应用层进行解包 // 6. 从缓冲区中移除已经处理过的报文保留剩余数据 // 获取到了完整报文接下来就需要将完整报文从buffer剔除保留多余部分进行后续使用 buffer.erase(0, target_len); // 我们要剔除的不只是一个完整报文而是包含报头的完整报文 // 并且这也就是为什么我们函数第一个参数传的是引用目的就是为了这个剔除操作能直接影响外部传入的数据 // 这样下次外部同样的字符串buffer_queue获取到新的报文就能继续做判断 return true; } // 协议层只负责处理一次报文判断完整性以及对报文进行处理(序列化/反序列化) bool GetRequest(InetAddr client, std::string buffer_queue, std::string *send_str) { std::string json_package; // 1、解析报文提取完整的json请求判断如果报文不完整就让服务器继续追加读取 if (!DeCode(buffer_queue, json_package)) { // 要么没有找到分隔符要么读取长度不是完整报文长度 // 总之读取到的不是一个完整的报文继续读取 return false; } // 到这里就能说明json_package一定获取到了一个完整的报文 // 2、请求json串反序列化 Request req; req.Deserialize(json_package); // 3、到这我就一定得到了一个内部属性已经被设置了的req了 // 通过req-resp不就是要完成计算功能了嘛 —— 业务 Responce resp _func(req); // 业务处理回调 // 4、序列化 std::string json_str resp.Serialize(); // 5、封装报头(添加自定义长度) *send_str EnCode(json_str); // 携带长度的应答报文 return true; } // 协议层只负责处理一次报文判断完整性以及对报文进行处理(序列化/反序列化) bool GetResponse(std::string resp_buffer, Responce *resp) { std::string json_package; // 1、解析报文 if (!DeCode(resp_buffer, json_package)) { // 获取到的不是一个完整的应答报文返回false继续在tcp中获取 return false; } // 到此就获得了一个去掉报头的完整应答报文 // 2、反序列化 resp-Deserialize(json_package); // resp作为输出型参数已经获取到了_result和_code return true; } std::string BuildRequestString(int x, int y, char oper) { // 1、构建一个完整的请求 Request req(x, y, oper); // 2、序列化 std::string json_req req.Serialize(); // 封装报头 return EnCode(json_req); } ~Protocol() { } private: Request _req; Responce _resp; func_t _func; }; #endif核心设计解读报文格式设计采用「长度\r\nJson字符串\r\n」的报文结构这是工业级网络应用中最经典、最常用的报文格式之一。长度字段让接收方可以精准获知本次业务报文的真实大小分隔符\r\n则用于区分长度字段和后续的业务数据降低解析复杂度。 补充后续学习的 HTTP 协议的头部也大量使用\r\n作为行分隔符这种设计在网络协议中非常普遍调试时也方便肉眼识别报文边界粘包半包问题的彻底解决封包时前置长度头部接收方据此知道每条报文的准确字节数解包时先提取长度值再按照长度精准截取完整报文不完整的半包数据会暂存在接收缓冲区中等待下一轮读取到更多字节后再继续解析多条粘连在一起的报文会被循环逐条解析处理不会遗漏任何一条业务数据。错误处理与安全防护对长度字段做合法性校验一旦收到非法数据比如长度值异常巨大、负数、超出缓冲区上限直接关闭当前连接防止恶意构造的报文造成缓冲区溢出或服务端资源耗尽抵御基础的网络攻击。回调解耦协议层只负责报文的封包、解包、序列化与反序列化完全不关心具体业务逻辑。解析出结构化请求后通过回调函数把请求对象传递给业务层处理实现协议层与业务层的彻底解耦。GetRequest GetResponse 设计注意事项2.6 业务层计算器实现(Calculator.hpp)业务层只负责运算逻辑与网络、协议完全解耦。支持 - * / %五种运算。错误处理除 0、模 0、非法操作码返回对应状态码。#ifndef NETCALCULATOR_HPP #define NETCALCULATOR_HPP #include protocol.hpp // 业务逻辑层Business Logic Layer // 这是一个纯粹的计算组件实现了高度的“模块解耦”。 // 它完全不感知底层的网络通信TCP流或数据的序列化方式JSON格式只负责根据标准化的 Request 对象运算并返回 Response。 class Cal { public: Responce Execute(const Request req) { Responce resp(0, 0); switch (req.Oper()) { case : resp.SetResult(req.X() req.Y()); break; case -: resp.SetResult(req.X() - req.Y()); break; case *: resp.SetResult(req.X() * req.Y()); break; case /: if (req.Y() 0) { resp.SetCode(1); // 1除零错误 } else { resp.SetResult(req.X() / req.Y()); } break; case %: if (req.Y() 0) { resp.SetCode(2); // 2余零错误 } else { resp.SetResult(req.X() % req.Y()); } break; default: resp.SetCode(3); // 3非法操作 break; } return resp; } private: }; #endif核心设计解读纯业务逻辑Calculator 类只包含计算逻辑不关心网络、协议等任何底层细节。错误码设计通过_exitcode字段返回错误信息客户端可以根据错误码进行相应的处理。日志记录在关键节点添加日志方便调试和问题定位。三. 服务端与客户端完整实现3.1 服务端实现OnlineCalServer.cc服务端将各个模块组合起来通过回调函数将三层连接在一起。#include TcpServer.hpp #include protocol.hpp #include NetCalculator.hpp // ./tcpserver port int main(int argc, char *argv[]) { if (argc ! 2) { std::cerr Usage: argv[0] port std::endl; exit(USAGE_ERR); } // std::unique_ptrTcpServer tsvr std::make_uniqueTcpServer(std::stoi(argv[1])); // 1. 业务层 - OSI 会话层 - TCP/IP 应用层 // 最内层的“小鱼”纯业务计算对象没有任何网络属性只知道如何根据 Request 算出 Response std::unique_ptrCal calculator std::make_uniqueCal(); // 4. 协议层 — OSI 表示层 - TCP/IP 应用层 // 中间的“中鱼”负责报文定界解决粘包和序列化/反序列化。 // 【依赖注入】通过 Lambda 表达式 [cal] 捕获业务层对象。 // 当 Protocol 层成功解析出一个 Request 对象后会调用这个 Lambda从而触发 cal-Exec() 进行计算。 std::unique_ptrProtocol protocol std::make_uniqueProtocol([calculator] (const Request req)-Responce { return calculator-Execute(req); }); // 5. 网络通信层 - OSI 应用层 - TCP/IP 应用层 (注在OSI中TcpServer实际偏向会话层/传输层接口但在TCP/IP中统归应用层) // 最外层的“大鱼”负责底层的 Socket 建立、监听、多进程并发和字节流收发。 // 【依赖注入】通过 Lambda 表达式 [protocol] 捕获协议层对象。 // 当 TcpServer 从网络中 Recv 到一串字节流streamstr时会调用这个 Lambda将其全权交给 protocol 去做拆包和解析。 std::unique_ptrTcpServer tsvr std::make_uniqueTcpServer(std::stoi(argv[1]), [protocol] (InetAddr client, std::string buffer_queue, std::string *send_str){ return protocol-GetRequest(client, buffer_queue, send_str); }); tsvr-Start(); return 0; }3.2 客户端实现OnlineCalClient.cc客户端实现与服务端对称同样需要进行序列化、封包、解包、反序列化操作。#include TcpServer.hpp #include Socket.hpp #include protocol.hpp void GetDataFromStdin(int *x, int *y, char *oper) { std::cout Please Enter x: ; std::cin *x; std::cout Please Enter y: ; std::cin *y; std::cout Please Enter oper: ; std::cin *oper; } bool _isrunning true; // ./tcpserver ip port int main(int argc, char *argv[]) { if (argc ! 3) { std::cerr Usage: argv[0] ip port std::endl; exit(USAGE_ERR); } std::string ip argv[1]; uint16_t port std::stoi(argv[2]); // std::unique_ptrSocket client std::make_uniqueTcpSocket(); std::shared_ptrSocket client std::make_sharedTcpSocket(); client-BuildTcpClientSocketMethod(); if (client-Connect(port, ip) ! 0) { // connect失败 std::cout connect error std::endl; exit(CONNECT_ERR); } // 成功连接到服务端 // 创建协议保证协议服务端、客户端都能看到 std::unique_ptrProtocol protocol std::make_uniqueProtocol(); // TCP处理读写IO操作 std::string req_buffer; while (true) { int x, y; char oper; // 1、输入请求 GetDataFromStdin(x, y, oper); // 2、构建出序列化后的请求字符串 std::string req_str protocol-BuildRequestString(x, y, oper); // 3、发送请求 client-Send(req_str); Responce resp; // 4、获取服务端发送的应答报文(未处理) while (true) { int n client-Recv(req_buffer); if (n 0) { if (!protocol-GetResponse(req_buffer, resp)) continue; break; } else if (n 0) { // 服务端退出 std::cout Server quit... std::endl; _isrunning false; break; } else { // 获取异常 std::cout recv error... std::endl; _isrunning false; break; } } if (_isrunning false) break; // 5、到这resp就已经得到结果了显示出来 resp.ShowResult(); } client-Close(); return 0; }3.3 编译与运行编写 Makefile 文件.PHONY:all all:tcpserver tcpclient tcpserver:TcpServer.cc g -o $ $^ -stdc17 -ljsoncpp tcpclient:TcpClient.cc g -o $ $^ -stdc17 -ljsoncpp .PHONY:clean clean: rm -rf tcpserver tcpclient结束语本篇我们完成了一套三层解耦的 TCP 在线计算器服务完整实现从底层 Socket 封装、基础工具组件到协议层自定义报文设计再到上层计算器业务逻辑走完了一个小型工业级网络服务的完整开发流程。我们不仅解决 TCP 粘包半包的工程难题同时实践了模板方法模式、回调解耦、RAII 资源管理、多进程服务模型等关键技术点。网络编程不只是会调用 socket、connect、recv 等系统调用更考验架构分层、异常处理、资源管控的工程思维。这份代码可以作为学习 TCP 网络编程的参考范本你也可以在此基础上继续拓展比如把多 进程模型替换为多线程、增加更多运算能力、完善错误码体系进一步打磨属于自己的网络服务项目。希望本篇内容能够帮你夯实网络编程基础为后续学习高并发网络框架打下扎实根基。

相关新闻

最新新闻

iQOO Z11 Turbo与Z12 Turbo怎么选?二手淘机验机避坑指南

iQOO Z11 Turbo与Z12 Turbo怎么选?二手淘机验机避坑指南

这次我们来看一个二手机圈最近讨论度不低的换机话题:iQOO Z11 Turbo 和 iQOO Z12 Turbo,到底该等新机,还是去二手市场掏一台性价比更高的旧款。标题里的“拍机堂淘机”“淘机认准极光”已经说明,这大概率不是在聊官方发布会&#…

2026/9/1 16:52:12
电台老鼠与MPX清图:SDR调频接收链路优化指南

电台老鼠与MPX清图:SDR调频接收链路优化指南

先把话说在前面:如果你也听别人反复提到“电台老鼠”“MPX”“清图”这几个词,又找不到一篇能讲清楚的说明,那这篇文章就是给你的。我在折腾调频广播接收时,最常看到的一个场景是——有人用很便宜的 USB 式 SDR 接收器听广播&…

2026/9/1 16:52:12
滑动窗口注意力下的KV缓存:为什么必须用环形缓存?

滑动窗口注意力下的KV缓存:为什么必须用环形缓存?

滑动窗口注意力在模型推理中经常和 KV 缓存一起出现。很多人第一次接触这个概念时,会先想到注意力矩阵从 N x N 变成 N x W,于是认为计算量变小了。但真正影响长上下文部署的往往不是那一次矩阵乘,而是 Key 和 Value 缓存的存放方式。这里要回…

2026/9/1 16:52:12
解决macOS定时任务失效:构建菜单栏守护进程确保cron准时执行

解决macOS定时任务失效:构建菜单栏守护进程确保cron准时执行

在 macOS 上,定时任务(cron)是一个经典但有时会“失灵”的工具。许多开发者都遇到过这样的场景:精心配置了一个crontab,期望它在凌晨执行数据备份、日志清理或 API 同步,结果第二天检查时发现任务根本没有运…

2026/9/1 16:52:12
基于SpringBoot的车辆维修保养系统的设计与实现(源码+文档+部署讲解等)

基于SpringBoot的车辆维修保养系统的设计与实现(源码+文档+部署讲解等)

联系博主 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 …

2026/9/1 16:52:12
【智能制造】2026零碳工厂建设方案【附全文阅读】

【智能制造】2026零碳工厂建设方案【附全文阅读】

本方案面向制造企业高管、EHS 与能源管理负责人、双碳咨询从业者,以光伏行业为样本,指导工业企业零碳工厂落地建设。文档立足国内双碳目标,剖析工业领域减排紧迫性,围绕范围一、二、三全维度碳排放,输出清洁能源替代、…

2026/9/1 16:47:12