Linux 网络套接字编程(七)TCP服务端和客户端的实现——网络版本计算器

目录

一、设计框架

[二、Socket.hpp : Socket 的封装与实现](#二、Socket.hpp : Socket 的封装与实现)

[1. 基础定义与命名空间](#1. 基础定义与命名空间)

[2. 抽象基类 Socket:定义接口规范](#2. 抽象基类 Socket:定义接口规范)

[3. 具体实现子类 TcpSocket](#3. 具体实现子类 TcpSocket)

[Accepter() 函数](#Accepter() 函数)

[三、Protocol.hpp 协议模块](#三、Protocol.hpp 协议模块)

[1. Request 请求报文类](#1. Request 请求报文类)

[2. Response 响应报文类](#2. Response 响应报文类)

[3. Protocol 协议处理类](#3. Protocol 协议处理类)

[封包 Packet 与解包 Unpack](#封包 Packet 与解包 Unpack)

[ParseRequest() 服务器端解析函数](#ParseRequest() 服务器端解析函数)

高度总结:

返回值:

粘包问题的补充:

[ParseResponse() 客户端解析函数](#ParseResponse() 客户端解析函数)

逻辑总结:

回调函数的理解:

两个函数的不同:

私有成员变量和回调逻辑

[四、Calculator.hpp 计算器模块](#四、Calculator.hpp 计算器模块)

[五、TcpServer.hpp 服务端网络通信模块](#五、TcpServer.hpp 服务端网络通信模块)

[1. 头文件](#1. 头文件)

[2. 私有成员变量](#2. 私有成员变量)

[3. 构造函数:创建监听套接字](#3. 构造函数:创建监听套接字)

[4. Loop ():服务器主循环](#4. Loop ():服务器主循环)

[5. service ():处理单个客户端的读写](#5. service ():处理单个客户端的读写)

[六、NetCalServer.cc 服务端主文件](#六、NetCalServer.cc 服务端主文件)

[七、NetCalClient.cc 客户端主文件](#七、NetCalClient.cc 客户端主文件)

[1. 创建客户端套接字并连接服务端](#1. 创建客户端套接字并连接服务端)

[2. 初始化协议层(绑定打印回调)](#2. 初始化协议层(绑定打印回调))

[3. 主循环:循环输入、发请求、收响应](#3. 主循环:循环输入、发请求、收响应)

[5. 发送、接收、解析(和服务端收发逻辑闭环)](#5. 发送、接收、解析(和服务端收发逻辑闭环))

八、运行结果:

客户端:

服务端:

[九、深度剖析 OSI 七层模型](#九、深度剖析 OSI 七层模型)

[1. 应用层:Calculator 业务层(对应 OSI 第七层)](#1. 应用层:Calculator 业务层(对应 OSI 第七层))

[2. 表示层:Protocol 协议层(对应 OSI 第六层)](#2. 表示层:Protocol 协议层(对应 OSI 第六层))

[3. 会话层:TcpServer 网络管理层(对应 OSI 第五层)](#3. 会话层:TcpServer 网络管理层(对应 OSI 第五层))

[4. 下四层:操作系统内核全权接管](#4. 下四层:操作系统内核全权接管)

[5. 总结](#5. 总结)

十、代码展示

十一、总结


在上一篇文章中我们已经了解了自定义应用层协议和JSON 序列化与反序列化,并企之前的 UDP 和 TCP 的编码中我们也都清楚了 TCP 网络通信的原理。那这篇文章那个我们将实现一个TCP 网络版本的计算器,把前面学的知识都应用起来。

一、设计框架

我们要做的 TCP 计算器,核心是客户端发送请求、服务端发送结果。第一步就是服务端和客户端建立网络通信。

  1. 之前我们在写 UDP 和 TCP 的代码时都是直接将网络通信接口API直接写在服务端和客户端内的,而今天我们不这样做了,我们直接将这些API封装成一个 Socket.hpp 头文件,后续不管是服务器监听,还是客户端连接,直接调用封装好的方法就行,不用再碰复杂的系统调用。

  2. 下来第二个文件就是 TcpServer.hpp 服务端头文件,搭建 TCP 服务器骨架,默认开启监听、接收连接,用 fork 多进程并发处理客户端。

  3. 再下来我们需要实现第三个文件 Protocol.hpp 文件,这是一个核心协议模块文件,在这个文件中我们要解决两个问题:一个是序列化和反序列化问题,未来我们可能定义结构体,客户端应该把把结构体内容序列化成 JSON 字符串,服务器收到后反序列化出结构体。这里 JSON 格式字符串本质就是适配网络发送的连续字节流,我们在上一篇文章已经讲过。另一个问题就是自定义应用层协议,给数据做 "打包 / 解包" 处理,解决数据的粘包问题,因为 TCP 是流式传输,只管发字节,不管边界。如果直接发 JSON 字符串,服务器有可能出现分不清哪条是完整请求。所以我们要自己定规矩:每条完整报文 = 长度头 + JSON 内容 + 分隔符。客户端给 JSON 字符串加长度头,服务器按长度精准截取完整报文。

  4. 下一个实现 Calculator.hpp 计算逻辑的文件, 接收解析好的请求,做加减乘除,返回计算结果;将业务逻辑和网络、协议分开,这样以后想改计算规则、加新运算,不用动网络和协议代码。保持解耦。

  5. 再下来就是服务端和客户端的两个入口文件 NetCalServer.ccNetCalClinet.cc 文件,把所有模块 "组装" 起来。

  6. 还有日志文件 Logger.hpp,打印关键步骤信息。InetAddr.hpp 文件负责 IP 地址封装。我们前面都实现过,这里就不做过多介绍。

二、Socket.hpp : Socket 的封装与实现

我们在 Socket.hpp 文件中将 Linux 原生的 Socket API 封装好,后续就直接调用封装好的方法进行使用。我们采用了 "模板方法模式" 来实现,通过抽象基类定义接口规范,由具体子类实现不同协议(TCP/UDP)的逻辑:

整个 Socket.hpp 文件分为三层,职责分明:

  1. 命名空间与基础定义:提供统一的日志宏、全局常量和错误码枚举,为整个模块提供基础支持。
  2. 抽象基类 Socket:定义所有 Socket 操作的纯虚接口,以及模板方法,规范 TCP/UDP 等子类的行为。
  3. 具体实现子类 TcpSocket:继承自 Socket,重写基类的纯虚函数,实现 TCP 协议专属的 Socket 逻辑。

这种设计让我们可以轻松扩展,未来如果需要支持 UDP,只需要新增一个 UdpSocket 类继承并重写方法即可。

1. 基础定义与命名空间

  1. 命名空间 NS_SOCKET_MODULE:将所有 Socket 相关代码包裹其中,避免全局命名冲突。

  2. 日志模块引入 NS_LOG_MODULE:依赖之前实现的日志模块,统一处理日志输出。

  3. 全局常量 gbacklog:定义 TCP 服务器 listen() 函数的连接队列长度,这里设为 16。

  4. 错误码枚举:定义了创建、绑定、监听等关键步骤可能出现的错误码,方便程序错误处理。

2. 抽象基类 Socket:定义接口规范

1. 在我们的 Socket 基类中,我们可以看到无论是初始化的 Create、Bind、Listen,还是通信的Recv、Send、Accept,都被定义为 virtual 纯虚函数,原因就是为了实现多态,让基类统一规范接口,让子类实现不同协议的底层细节。

  1. 我们知道,网络通信不止有 TCP,还有 UDP。TCP 是面向连接的流式通信,UDP 是无连接的数据报通信,二者底层的系统调用和执行逻辑不同。如果我们不使用虚函数,就需要为 TCP、UDP 分别写两套完全独立的类,上层代码调用时,还要区分是 TCP 对象还是 UDP 对象,代码扩展性较差。

  2. 而虚函数的设计解决了这个问题,Socket 作为抽象基类,把所有套接字该有的行为(创建、绑定、收发、连接)全部定义为虚函数,这样 TCP 和 UDP 就分别可以作为子类重写这些虚函数,实现各自协议的专属逻辑;而上层的服务器、客户端代码,只需要和基类 Socket 打交道,不用关心底层是 TCP 还是 UDP,后续想扩展协议,只需要新增子类即可。这也是面向对象多态思想在网络编程中的核心落地。

  3. 再看一下权限划分:为什么前三个初始化函数(CreateSocketOrDie()、BindSocketOrDie(port)、ListenSocketOrDie())是 protected,后面的通信函数(Recv、Send、Accept)是 public 呢?首先,前三个函数在子类的重写实现中分别对应创建套接字,绑定,监听三个步骤,这三个步骤是 TCP 服务器套接字初始化的固定流水线,它们的顺序是固定的。如果我们把这三个函数设为public,上层代码就可以随意单独调用,一旦有人写错顺序,比如先绑定再创建,程序会直接崩溃。

  4. 因此我们将前三个函数的权限设为 protected,代表着只有子类 TcpSocket 可以访问,上层的服务器、客户端代码无法访问;并提供 public 模板方法,在基类中写一个 BuildTcpSocketMethod 公共函数,把这三步按固定顺序封装进去。上层代码不用关心内部三步怎么执行,只需要调用这一个模板方法,就能一键完成套接字初始化,既安全又简洁。

  5. 通信函数和初始化函数不同,Recv、Send、Accept、Close、Connect 这些函数,是套接字初始化完成后,运行时独立、按需调用的通信行为。服务器启动后,会循环调用 Accept 接受客户端连接;连接建立后,会反复调用 Recv、Send 收发数据;通信结束后,调用 Close 释放资源。这些函数没有固定的执行顺序。所以我们把它们设为 public ,直接对外暴露接口,让上层的服务器、客户端代码可以随时、灵活地调用,完成网络通信的核心逻辑。

3. 具体实现子类 TcpSocket

TcpSocket 子类继承 Socket 基类,并重写了所有纯虚函数,实现了 TCP 协议的所有细节。

首先我们先重写 Socket 基类的这三个 protected 纯虚函数,函数内部分别调用 socket()、bind()、listen() 系统调用完成重写。

现在我们看通信阶段的核心方法,这也是上层服务端/客户端代码真正会用到的接口:

封装了原生的 accept 系统调用,服务端会阻塞等待,直到有客户端发起连接;成功后返回一个新的文件描述符 sockfd,专门用于和这个客户端通信;clientaddr = addr 表示把客户端地址信息保存到传入的引用参数里,上层可以拿到客户端 IP 和端口,方便日志打印和调试。accept 拿到一个新的客户端 fd,把这个 fd 装进 TcpSocket 对象里,包成智能指针给上层,上层拿着这个智能指针,直接调用 Recv、Send、Close就行。

Accepter() 函数

为什么原生 accept 返回裸 fd,我们封装 Accepter() 函数后却返回智能指针?

首先明确核心前提:原生 Linux 的 accept 系统调用,返回的本质是一个纯 int 类型的文件描述符,这个数字就是服务端与单个客户端通信的唯一标识,后续所有和该客户端的收发、关闭操作,都依赖这个 fd。而我们封装的 TcpSocket 子类,核心作用就是把这个原生裸 fd 进行面向对象封装,让我们不再直接操作裸 fd,而是通过对象的成员方法完成通信,这是整个封装设计的核心初衷。Accepter 函数内部第一步,依然会调用原生的 accept 拿到一个全新的通信 fd;但它不会直接把这个 int 数字返回,而是立刻将这个新 fd 作为成员变量,创建一个全新的 TcpSocket 子类对象,再用 std::make_shared 把这个对象托管给智能指针,最终返回这个智能指针。

智能指针和对象到底是什么关系?

TcpSocket 对象是一个 "装着 fd 的工具箱",里面封装了收发、关闭等所有通信操作;而 std::shared_ptr 智能指针,就是这个工具箱的 "专属管理员"。std::make_shared 的作用,就是先在堆上创建出这个工具箱(Socket 对象),再生成一个管理员(智能指针),专门负责看管这个工具箱。我们拿到的智能指针,本质是一个变量,这个变量里存着工具箱的内存地址;我们通过智能指针,就能间接访问到工具箱里的 fd 和所有成员方法。简单说:智能指针是 "管对象的变量",对象是 "存 fd 的实体",二者是管理与被管理的关系,相辅相成。

为什么非要把新 fd 重新封装进 Socket 对象,再返回智能指针?

第一,统一操作接口。服务端的监听套接字、客户端的连接套接字、Accept 生成的通信套接字,本质都是 fd,都需要收发、关闭操作。把所有 fd 都封装进 Socket 类后,我们只需要调用统一的 Recv、Send、Close 方法,不用区分是监听 fd 还是通信 fd,代码风格完全统一;

第二,自动安全管理。原生裸 fd 用完必须手动 close,一旦忘记就会造成文件描述符泄露;而智能指针会在生命周期结束时,自动调用 Socket 对象的析构函数,内部自动关闭 fd,从根源上杜绝资源泄露;

第三,适配上层逻辑。我们下面写的 TcpServer 的 service 函数,参数接收的就是 std::shared_ptr<Socket> 类型。上层逻辑完全不需要关心底层 fd 是什么、怎么来的,只需要接收这个被封装好的对象,直接调用方法即可,完美实现底层细节与上层业务的解耦。
Sockfd() 获取底层文件描述符,返回当前套接字的原生文件描述符;上层代码如果需要直接操作 fd,可以通过这个方法获取;
Recv() 接收客户端数据,通过 *out += inbuffer 追加,这样设计的好处是,上层可以维护一个 inbuffer,多次 Recv 调用的数据会自动拼接起来,完美适配 TCP 的流式传输特性,后续我们的协议解包逻辑,就是基于这个拼接后的完整字符串处理的。
Send() 发送数据给客户端,这里我们使用了 send() 系统调用,它和 write() 本质上是一样的,只是参数多了一个标志位。
Close() 关闭套接字,调用 close(_sockfd) 释放文件描述符,再把 _sockfd 置为 -1,标记为无效;配合智能指针的 RAII 特性,对象销毁时自动调用 Close,实现资源自动释放,杜绝资源泄漏。
Connect() 客户端连接服务器,addr 是服务器的 IP 和端口,通过我们封装的 InetAddr 类传入;调用 connect() 系统调用,成功返回 0,失败返回 - 1。

三、Protocol.hpp 协议模块

这个文件有三个类:

  1. Request 类:定义客户端发给服务器的计算请求,做序列化 / 反序列化(结构体↔JSON)

  2. Response 类:定义服务器返回给客户端的计算结果,同样做序列化 / 反序列化

  3. Protocol 类:定义我们自定义的报文打包 / 解包规则,解决 TCP 粘包,串联解包→反序列化→业务计算→序列化→打包全流程

1. Request 请求报文类

这个 Request 类的作用是专门封装客户端要发给服务器的计算请求,只存 3 个核心数据:左数、右数、运算符。比如说用户输入了三个数据 : '10' '+' '20',就是创建 Request 对象 (10,20,'+'),在这个类中有序列化和反序列化函数,然后会将数据序列化为 JSON 字符串。

有两个构造函数,一个是无参构造 (默认初始化为 0,创建空请求对象),另一个是有参构造,直接给三个成员赋值,上层创建请求时一行代码搞定。
下来就是 Serialize() 序列化函数和 Deserialize() 反序列化函数。

Serialize() 序列化函数是把 C++ 结构体转换为 JSON 字符串,使用 Json::Value 创建一个 JSON 万能容器,把结构体的左数、右数、运算符,装进 JSON 的 key-value 里,再使用 writer() 把 JSON 对象序列化转成字符串,最后把生成的 JSON 字符串,写入传入的指针参数 out (输出型参数),返回 true表示序列化成功。举个例子:结构体是 x=10,y=20,oper='+',序列化后 JSON 字符串是 {"left":10,"right":20,"oper":"+"}。

Deserialize 反序列化函数把服务器收到的 JSON 字符串 → 还原成 C++ 结构体,parse 专门反序列化解析 JSON 字符串,把传入的 JSON 字符串 in,解析到 root 容器里,最后通过 asInit() 从 root 里取出对应的值,赋值给结构体成员。整体功能就是客户端发来 JSON 字符串,服务器收到后,用这个函数把 JSON 变回结构体,就能拿到要计算的三个数。
问题 1:为什么 Serialize() 的参数是 std::string* out(指针),而Deserialize参数是std::string& in(引用) ?

答案就是 *out 是输出型参数,因为我们要把序列化好的 JSON 字符串输出到函数外面的变量里。比如我们在调用这个函数时就得传地址 req.Serialize(&json),在函数内部通过解引用 *out = "生成的JSON字符串",从而就生成好的JSON字符串传给调用时得参数 json,从而我们就可以使用 json 了。用指针 * 是因为 C++ 我们默认函数要修改外部变量,必须传递变量的内存地址,指针正是传递地址的方式;这个参数是用来接收函数内部生成的结果。

反序列化函数是引用的原因是反序列化是用来接收外部已存在的 JSON 字符串,仅读取字符串内容,不修改原字符串,解析后将数据赋值给当前 Request/Response 对象自身的成员变量。
问题 2:为什么两个函数的返回值都是 bool 布尔类型?而不返回解析后的 Request 对象?

两个函数返回 bool,核心目的是标记函数执行的成功与失败。

反序列化修改的是当前已存在的对象自身,而非生成新对象。上层调用时会先提前定义好Request req 空对象,再调用 req.Deserialize(json_string),函数内部直接修改这个已定义对象的成员值。因此我们在调用这个函数时就已经将成员变量修改了,就没必要在返回该类型。返回 bool 值是刚好能判断序列化和反序列化是否正确。

2. Response 响应报文类

这个类的核心作用是专门封装服务器要返回给客户端的计算结果,存 2 个核心数据:计算结果、状态码。Request 是客户端发出去的请求,Response 是服务器发回来的响应。这个类里的序列化/反序列化逻辑和 Request 一样,只是存的数据不一样。状态码 _code = 0 表示计算成功,result 是正确结果。_code!=0 表示计算失败(比如除零、非法运算符),result 无意义。

3. Protocol 协议处理类

数据传输的第一步就是序列化为 JSON 字符串。我们之所以要把自定义的 Request、Response 对象转换成 JSON 字符串,核心目的就是适配 TCP 网络传输规则。因为TCP 网络底层只能识别和传输二进制字节流,而 JSON 字符串直接被编码为二进制字节流,完全契合网络传输的底层数据要求;所以序列化和反序列化解决了 "数据在网络中传输" 的问题,让原本无法直接在网络中传递的 C++ 对象,变成了网络可识别、可传输的字节流载体。

但即便完成了 JSON 序列化,得到了可传输的字符串,也不代表数据传输就万无一失,因为还可能会导致粘包、半包问题。这里我们很容易陷入一个认知误区:觉得客户端每发送一条序列化后的 JSON 字符串,服务器就会精准接收到一条完整数据,觉得发送和接收是一一对应的。但实际上并不是,因为 TCP 底层是流式传输,数据以字节流的形式连续传输,没有固定的报文边界,底层传输协议会根据网络拥堵情况、缓冲区大小、Nagle 算法等,自主对数据进行合并或拆分。这就会导致两种异常情况:要么多条 JSON 字符串被合并成一段数据传输,服务器无法区分单条报文,形成粘包;要么一条 JSON 字符串被拆分成多段传输,服务器只接收到部分数据,形成半包,最终都会导致 JSON 解析失败、程序运行异常。如下:

那为了解决 TCP 流式传输的报文边界问题,我们就需要引入封包与解包机制,就是给序列化后的 JSON 字符串添加统一的传输标识,让接收端能精准区分每一条完整报文。为了解决 TCP 粘包、半包问题,我们约定了一套极简且高效的自定义报文格式,一共分为三个部分,报文格式如下 :

1. 有效载荷长度:也就是 JSON 字符串内容的字节长度,用一个十进制数字表示;

  1. 特殊分隔符 \r\n :作为报文各部分的边界标识,用来拆分长度头与内容、标记报文结尾;

  2. 有效载荷内容:序列化后的纯净 JSON 字符串(数据内容)。
    我们举个例子 : 假如已经序列化后的 JSON 字符串如下:

cpp 复制代码
{"left":10,"right":20,"oper":"+"}

那封包后完整报文就如下:

其中 29 是 JSON 字符串的实际长度;第一个 \r\n 用来拆分长度头和 JSON 内容;第二个 \r\n 通过换行来表示整条报文已结束,方便调我们区分报文边界。

封包 Packet 与解包 Unpack

因此我们将封包和解包等核心功能封装在我们自定义的 Protocol 协议类。下面我们来看一下 Protocol 协议类中封包 Packet 与解包 Unpack 的操作:

首先我们在全局定义一个全局分隔符 "\r\n"。
这是 Packet 封包函数,用来将序列化后的 JSON 字符串封包程可传输的完整报文,按照 "长度头 + 分隔符 + JSON + 分隔符" 的完整网络报文。

std::to_string(json_string.size()) 用来获取 JSON 字符串的长度,并转为字符串形式作为长度头;然后拼接第一个 \r\n 分割长度头与 JSON 内容;再拼接原始 JSON 字符串;最后拼接第二个 \r\n 标记报文结尾,完成封包。返回一个完整的封包好的字符串。
这是 Unpack 解包函数,表示我们要将网络完整的报文解包为纯净的 JSON 字符串。第二个参数也是一个输出型参数,把解包后的 JSON 字符串通过指针带出来。

Unpack 就是 Packet 的反向操作,把接收端收到的报文 "脱掉协议外壳",还原出最开始的纯净 JSON 字符串,同时清理缓冲区,为下一次解析做准备。

前面我们已经搞懂了 Protocol 类中 Packet 封包、Unpack 解包这两个基础工具函数,解决了网络报文的格式封装与解析问题。但在实际项目中,封包、解包只是 "底层能力",我们还需要一个上层调度逻辑,这个逻辑就是把「解包→反序列化→业务处理的结果→序列化→封包」这一整条通信链路连接起来。因此我们就需要在类中再设计出了 ParseRequest 和 ParseResponse 这两个函数 :

ParseRequest() 服务器端解析函数

下面我们再对函数的各个参数变量和逻辑进行一下梳理:

首先对于参数和返回值来讲:

  1. inbuffer 是函数引用传递的参数,本质是服务器的 TCP 接收缓冲区,存储着经过客户端 Packet 封包后,从网络中发送过来的完整的网络报文。这里的内容不是纯净的 JSON 字符串,而是 长度头 + 分隔符 + JSON + 分隔符 的封包格式,并且因为 TCP 粘包问题,缓冲区中可能堆积 1 条、多条完整报文,或是末尾带有半包数据。

  2. result 是函数内部定义的返回值字符串,专门存储经过计算处理后生成的所有完整的报文,最终函数会将 result 里的内容一次性返回,由服务器统一发送给客户端。

  1. 函数首先以 while(true) 死循环开启处理,核心目的是一次性将 inbuffer 中所有完整的客户端请求报文处理完毕。因为 TCP 接收缓冲区中可能一次性堆积多条粘在一起的封包报文,如果不循环处理,仅能处理第一条报文,那剩余报文会永久滞留在缓冲区,导致逻辑出错;只有当解包识别出半包数据时,循环才会终止,等待下一次 TCP 接收补充数据。

  2. 循环内首先调用 Unpack(inbuffer, &json_string) 函数,对inbuffer中的网络报文进行解包操作。此时 inbuffer 中是封包后的完整报文,解包操作会精准剥离长度头、分隔符等协议外壳,提取出其中的纯净 JSON 字符串存入 json_string;同时同步修改 inbuffer,将已处理完毕的报文从缓冲区中删除,仅保留未处理的半包数据。若解包返回1:成功提取一条完整 JSON 字符串,继续向下执行业务处理;若解包返回0:缓冲区剩余数据不足一条完整报文(半包),直接终止循环,返回已处理完成的响应结果;若解包返回<0:参数非法,直接终止流程。

  3. 解包得到纯净的 json_string 后,我们定义空的 Request req 对象,调用 req.Deserialize (json_string) 完成反序列化操作。这里的 json_string 是客户端 Request 对象序列化后的产物,通过反序列化,会将 JSON 字符串中的左操作数、右操作数、运算符精准赋值到 req 对象的内部成员中,此时 req 对象中就完整存储了客户端的核心计算请求,为后续业务计算提供数据支撑;若反序列化失败,直接终止处理,丢弃脏数据。

  4. 我们定义空的 Response resp 对象,通过 _handler_request 回调函数,将存储着客户端请求的req 对象传入上层业务逻辑。回调函数会读取 req 中的运算数据,完成加减乘除等核心业务计算,最终将运算结果、状态码等数据赋值给 resp 对象。

  5. resp 对象中已经存储了完整的计算结果,但该对象无法直接在网络中传输,因此需要调用resp.Serialize(&resp_json_string) 进行序列化操作。该操作会将 resp 对象中的结果数据,转换为纯净的 JSON 字符串存入 resp_json_string,完成从业务对象到网络可传输字符串的转换,这一步与客户端 Request 对象的序列化逻辑完全对称。

  6. 序列化得到纯净的 resp_json_string 后,调用 Packet(resp_json_string) 进行封包操作,为响应 JSON 字符串拼接长度头、分隔符,封装成符合协议格式的完整网络报文;随后将该封包后的响应报文追加到result容器中,等待所有缓冲区中的请求处理完毕后,函数将result一次性返回,由服务器完成最终的网络发送。

高度总结:

从 TCP 接收缓冲区中读取到的数据,是客户端按照自定义协议封包完成后的报文,我们无法确定缓冲区中包含几条独立的请求报文,因此需要通过循环配合解包操作逐条提取出纯净的 JSON 字符串;之所以要对解包得到的 JSON 字符串进行反序列化,是因为 JSON 仅为便于网络传输的格式,必须将其还原为 Request 业务对象,才能提取其中的操作数与运算符并调用回调函数完成业务计算,计算完成后将结果存入 Response 响应对象中;而对 Response 对象进行序列化,是因为服务器需要将计算结果回传给客户端,网络传输只能识别 JSON 字符串格式,序列化能够将业务对象转换为可传输的纯净 JSON,序列化完成后还需进行封包处理,这是为了保持通信协议的对称性,客户端发来的是封包后的报文,服务器回传的响应也必须遵循相同的封包格式,无论服务器最终需要发送几条响应报文,封包与解包机制都能适配,保证收发双方都能稳定提取出完整独立的单条报文,规避 TCP 粘包、半包带来的数据解析异常问题。

返回值:

这个函数的返回值逻辑和我们平时遇到的函数的返回逻辑不一样,它并非在函数执行完毕后统一返回,而是遵循 "处理到半包即返回" 的核心原则。函数内部定义的 result 变量,用于持续累积所有已处理完成、可以正常回传给客户端的响应报文;循环处理过程中,一旦解包返回 n == 0,就意味着缓冲区剩余数据不足以构成一条完整报文,出现了半包状态,此时程序已无法继续解析新的完整请求,就会将 result 中累积的所有响应报文一次性返回;另一种特殊情况是解包返回 n < 0,代表出现参数非法等异常,整个处理流程无法正常开展,因此直接返回空字符串终止流程。

简单来说,函数只有在遇到无法继续处理的半包数据,或是出现异常错误时才会返回,半包时返回已处理完成的所有报文,异常时直接终止处理流程,这一设计既保证了完整报文能被及时处理和响应,又合理保留了半包数据,等待下一次接收数据后继续解析,完美适配 TCP 流式传输的特性。

粘包问题的补充:

我们要明确一个核心本质:无论是裸 JSON 字符串,还是我们加了长度头、分隔符封包后的完整报文,本质上都是一串连续的字符。TCP 传输的底层只认 "字节流",它不管这串字符是 "裸 JSON",还是 "长度头 + 分隔符 + JSON" 的协议格式。所以封包后的报文,依然 100% 会出现粘包,这是 TCP 协议本身的物理特性决定的,和报文里加了什么内容没有关系。

很多人会误以为 "我加了长度头,就不会粘包了",这是一个巨大的误区。真相是:加长度头,不是为了阻止粘包发生,而是为了「粘包发生后,能精准把它拆开」。客户端发出去的每一条封包报文,依然会被 TCP 底层、网络路由随意合并、拆分,粘包、半包该发生还是会发生。区别就在于:裸 JSON 粘在一起,服务器完全不知道从哪进行切割;而我们自定义的协议格式,通过 "长度头" 给每一条报文做了精准的 "身份证标记",哪怕 10 条报文粘成一大串,服务器也能通过 Unpack 解包函数,按照长度一条一条精准切开,完整还原出每一条独立的请求。

ParseResponse() 客户端解析函数

ParseResponse 是客户端的解析函数,用来处理服务器返回的响应报文。它的作用就是:把 TCP 接收缓冲区里的封包数据,循环拆包、解析,还原出服务器的响应结果,交给客户端的业务回调处理。和 ParseRequest 相比,它没有 "再生成响应发回去" 的步骤,因为客户端收到结果后,通信链路就完成了,所以流程更短。

逻辑总结:

ParseResponse 作为客户端侧专属的响应解析函数,它的参数 inbuffer 存储的正是服务器通过网络回传的、已完成业务计算并经过封包处理的报文,受 TCP 粘包特性影响,缓冲区中可能堆积多条完整响应报文。函数内部通过死循环持续调用解包函数,逐条剥离协议格式外壳,提取出纯净的响应 JSON 字符串;随后定义Response业务对象,通过反序列化将 JSON 中的运算结果、状态码等核心数据还原到对象中。此时解析工作完成,解析后的完整结果会通过 _handler_response 回调函数传递给客户端上层业务逻辑,上层代码进而在回调中打印计算结果、同样也实现了底层网络解析与上层业务处理解耦。

回调函数的理解:

客户端侧的 _handler_response 回调函数,本质就是将解析完成的结果交给上层业务逻辑处理,在我们这个计算器项目中,它的核心逻辑就是读取 Response 对象中存储的最终运算结果与状态码,将其打印输出给用户,完成一次完整的通信交互。这和服务端回调负责执行业务计算逻辑不同。

两个函数的不同:

因为这里是客户端,通信角色和服务器相反。服务器收到客户端请求后,算出结果、再序列化、封包、发回去,完成一次 "请求 - 响应" 的双向闭环;而客户端收到服务器返回的响应报文,拿到最终计算结果后,本次通信任务就彻底结束了,既不需要再执行业务计算,也不需要再序列化、封包、向服务器回传新数据。所以 ParseResponse 函数自然省去了序列化、封包、拼接响应报文的步骤,只需要完成解包、反序列化,最后通过回调把结果交给上层打印展示即可。

私有成员变量和回调逻辑

在 Protocol 类的私有成员中,_version 仅作为协议版本标识,用于版本兼容校验,并非核心功能;真正支撑整个网络通信业务解耦设计的,是下面的两个回调函数成员变量。首先通过using 关键字完成回调类型的别名定义,其中 HandlerRequest_t 被定义为一个 std::function函数容器,专门用于存放接收 Request 对象引用、返回 Response 对象的业务计算函数,对应服务器侧的回调逻辑,当服务器解析完客户端请求后,便会触发该回调,传入封装好请求数据的 Request 对象,执行业务计算逻辑并返回封装运算结果的 Response 对象;而HandlerResponse_t 则被定义为存放接收 Response 对象引用、无返回值的函数容器,对应客户端侧的回调逻辑,客户端解析完服务器响应报文后,触发该回调,传入存储运算结果的 Response 对象,完成结果打印展示的上层业务操作。这两个回调函数的具体实现逻辑,我们将在主函数中实现并注册到 Protocol 对象中,Protocol 类仅预留回调入口即可,不关心具体业务细节,通过这种回调机制,彻底实现了底层网络协议层与上层业务逻辑层的解耦,让网络通信框架具备极强的通用性和扩展性。

四、Calculator.hpp 计算器模块

在完成了Protocol协议类的底层通信框架搭建后,我们已经具备了报文解析、数据收发的完整能力,但整个通信链路还缺少最核心的业务处理环节。此前我们在 Protocol 类中预留了HandlerRequest_t 业务回调接口,专门用于对接上层计算逻辑,而现在要讲解的 Calculator 类,正是这个回调接口的具体的实现载体。Calculator 类完全脱离网络层,只负责计算逻辑。如下:

这个类中我们用了三个错误码来区分不同的异常场景:

  1. resp._code == 0(默认值):表示计算成功,resp._result 是有效结果;
  2. resp._code == 1:除零错误;
  3. resp._code == 2:取模运算除数为 0;
  4. resp._code == 3:非法运算符。

这样客户端收到 Response 后,通过判断 _code 字段,就能知道服务器计算是否成功,以及失败的具体原因,方便给用户友好的提示。
Execute() 函数的签名和 HandlerRequest_t 的定义完全匹配,所以 Calculator::Execute 可以直接被注册为 Protocol 类的业务回调函数。

这个 Calculator 类和 Protocol 类,通过回调机制形成了明确的分工协作:Protocol 负责网络数据接收 → 解包 → 反序列化为 Request → 调用注册的 HandlerRequest_t 回调;Calculator 负责:接收 Request → 计算 → 返回 Response;Protocol 再把返回的 Response 序列化 → 封包 → 发送给客户端。

这种设计实现了网络层与业务层的解耦:如果以后我们要把计算器改成聊天、文件传输等其他业务,只需要新增一个类似的业务类,实现相同签名的 Execute 方法,再注册给 Protocol 即可,协议层代码完全不用修改。

五、TcpServer.hpp 服务端网络通信模块

前面我们把协议解析、业务计算都讲了,但这些模块本质上都是 "空中楼阁",必须依托底层的网络通信才能真正跑起来。而 TcpServer.hpp 就是专门负责服务端网络通信的底层模块,它封装了 TCP 套接字创建、绑定、监听、数据收发等所有基础网络操作,把复杂的系统调用做了统一封装,只给上层暴露简洁的调用接口。接下来我们就详细讲解这个文件,搞清楚它是如何为上层协议模块和业务模块提供网络通信支撑的。

整个类的代码如上,下面我们逐个板块讲解:

1. 头文件

  • Socket.hpp:封装了系统调用 socket()、bind()、listen()、accept()、recv()、send() 等操作,让我们不用直接写系统调用。
  • InetAddr.hpp:封装了 IP 地址和端口号的管理,比如 sockaddr_in 结构体的创建、转换、打印。
  • Logger.hpp:日志工具,帮我们打印不同级别的日志。

2. 私有成员变量

_port:服务器绑定的端口号

_listensock:监听套接字,由 Socket.hpp 提供的封装类管理

_handler:收到数据后,要调用的上层处理回调(也就是 Protocol 层)
这是一个函数对象类型,它的签名是:

  • 参数:std::string& ------ 收到的原始网络数据(inbuffer)
  • 返回值:std::string ------ 处理后要发回给客户端的响应数据(outbuffer)

在我们项目里,这个 Handler_t 最终就是 protocol->ParseRequest(inbuffer),也就是协议层的解析函数。

3. 构造函数:创建监听套接字

这里依赖了 Socket.hpp 里的 TcpSocket 类:

BuildTcpSocketMethod(_port) 内部封装了 socket() → bind() → listen() 这三步

构造完成后,_listensock 就是一个已经绑定端口、处于监听状态的套接字

4. Loop ():服务器主循环

这是服务器的核心循环

5. service ():处理单个客户端的读写

1. service 函数的两个参数,本质上是我们对底层网络细节做了一层面向对象的封装。第一个参数是经过 Accept() 调用后创建的通信套接字对象,我们把原生 int 类型的文件描述符 fd 封装进了 Socket 类内部,并通过智能指针来管理它的生命周期,保证资源安全。第二个参数则是 InetAddr 封装对象,Accept 时就已经把客户端的 IP 与端口信息填充完成,用于标识当前连接的客户端身份。这两个参数,一个负责通信数据的收发,一个保存客户端的地址信息。

  1. Recv() 函数里的参数 inbuffer 是输出型参数。调用前 inbuffer 是空字符串调用 sockfd->Recv(&inbuffer) 后函数内部把从网络上读到的字节,全部填进了 inbuffer 里。outbuffer 是我们自己定义的空字符串,后面靠回调函数 _handler 往里面填数据。

  2. 下来的回调逻辑核心,我们通过 Recv 函数将网络数据读取至 inbuffer 后,并不会直接处理这些原始字节流,而是借助提前注册的回调函数,将 inbuffer 传递给协议层,也就是调用 Protocol 类的 ParseRequest 方法。在该方法内部,会依次完成对原始字节数据的解包、反序列化操作,解析出标准的 Request 请求对象,随后触发第二层业务回调,交由 Calculator 类完成核心计算逻辑,再将计算结果封装为 Response 对象,经过序列化、封包处理后,生成一段可直接发送的完整报文并返回。

  3. 而关于缓冲区的设计也尤为关键:outbuffer 在每次循环处理前都会被清空,避免上一次的响应数据残留导致报文错乱;它与 inbuffer 不同,inbuffer 是由 Recv 通过输出型参数填充而来,outbuffer 则是接收回调函数处理后返回的有效响应报文。同时得益于 while 循环的持续处理,每解析完一个完整的客户端请求报文,就会将对应的报文追加至 outbuffer 中,再统一发送给客户端。

  4. 当服务端完成所有业务处理与协议封装后,就来到了最终的数据发送环节。此时调用 Send 函数发送的 outbuffer,已经历经了解包、反序列化、业务计算、结果序列化、协议封包全流程处理,里面存放的是携带 4 字节长度头、搭配 JSON 字符串的完整合规报文,是可以直接传输的最终数据,无需再做任何额外处理。服务端通过 Send 将这份封装好的响应报文,经由 TCP 通信链路发送给对应的客户端后,客户端的网络模块会率先接收到这份原始报文数据,随即进入客户端的协议处理流程:会调用协议层的 ParseResponse 方法,先读取报文头部的 4 字节长度字段,精准拆分出有效的 JSON 数据部分,再将其反序列化为 Response 响应对象,拿到最终的计算结果与状态码。紧接着便会触发客户端专属的结果打印回调,将计算结果、错误提示等信息直观展示出来,完成一次从客户端发起请求到服务端响应反馈的完整通信闭环。

  5. 日志的打印:

六、NetCalServer.cc 服务端主文件

在完成了协议解析层 Protocol 与纯业务计算层 Calculator 的编写后,我们已经把底层通信能力与核心计算逻辑准备完毕,接下来就需要一个入口程序,将网络通信、协议解析、业务计算三者组装起来,形成一个能真正运行的服务器。这就是我们接下来要讲解的服务端 main.cc 文件,它负责搭建 TCP 监听、注册业务回调、循环接收并处理客户端数据,是整个服务端程序的启动入口与调度核心。

这是整个服务端程序的入口函数,它的核心作用只有一个:把网络层(TcpServer)、协议层(Protocol)、业务层(Calculator)三个模块组装起来,形成一个能跑起来的完整服务器。

第一步创建 Calculator 业务对象,它就是我们前面写的加减乘除取模的业务实现;用 std::unique_ptr 管理,自动释放内存,更安全;这个对象的 Execute 方法,就是我们后面要注册给 Protocol 的业务回调。
第二部创建 Protocol 协议对象,并注册业务回调。Protocol 构造函数需要传入一个 HandlerRequest_t 类型的回调;这里我们用一个lambda 表达式,把 cal->Execute(req) 包装成 Protocol 需要的回调:捕获 cal 智能指针;参数是 Request &req;返回值是 Response;这样 Protocol 在 ParseRequest 中,解析出 Request 对象后,就会自动调用这个 lambda,最终执行 cal->Execute(req),完成计算;这一步就是把 "协议层" 和 "业务层" 通过回调绑定起来了。
第三步就是创建 TcpServer 网络对象,并注册协议解析回调,TcpServer 构造函数需要传入两个东西:一个 "收到数据后的处理回调";服务器要绑定的端口号。这里的 lambda 回调,就是把 protocol->ParseRequest(inbuffer) 包装成 TcpServer 需要的类型:捕获 protocol 智能指针;参数是 std::string &inbuffer (TCP 接收缓冲区);返回值是 std::string (处理完后要发送的响应数据);这样 TcpServer 收到客户端数据后,就会自动调用这个 lambda,最终执行 protocol->ParseRequest (inbuffer),完成解包、解析、计算、封包响应的整个流程;这一步就是把 "网络层" 和 "协议层" 通过回调绑定起来了。
第五步启动服务器 tsvr->Loop(),启动 TCP 服务器的事件循环,开始监听端口、接收客户端连接、处理数据;服务器启动后,程序就停在这里,等待客户端请求,不会往下执行;只有当服务器退出时,才会执行 return 0;。

七、NetCalClient.cc 客户端主文件

前面我们已经把服务端从网络底层、协议解析、业务计算到入口组装的整套逻辑完整梳理完毕,服务端已经可以稳定接收连接、处理计算请求并返回响应。但整个 TCP 通信模型是双向的,光有服务端还不够,必须有客户端主动发起连接、发送请求、接收并展示结果,才能完成一次完整的交互闭环。接下来我们就来解析整个项目的最后一块核心代码 ------ 客户端的 .cc 文件,它是整个客户端程序的入口,负责主动连接服务端、封装用户输入的计算请求、发送报文、接收服务端响应并解析展示结果,和我们之前讲的服务端逻辑形成完美对称,共同构成完整的 TCP 计算器通信架构。

代码如上,下面我们进行分析:

Socket.hpp 同时封装了服务端监听套接字和客户端通信套接字
和服务端的用法提示逻辑一致,客户端启动时必须传入两个参数:服务端 IP + 服务端端口
这就是我们之前说的第三层回调。

  • 服务端回调:网络→协议→计算;
  • 客户端回调:协议→结果打印。

协议层解析完服务端返回的 Response 对象后,调用这个回调,把计算结果、错误码直接打印到控制台。
Usage 和 HandlerResponse 两个函数被修饰为static,核心目的是限制函数的作用域,使其仅在当前 client.cc 文件内可见、可调用,避免全局命名冲突,同时明确这两个函数是客户端专属的内部工具函数。C++ 里,普通全局函数,整个项目所有文件都能调用;加了 static 之后,这个函数就变成了文件级私有函数:

1. 创建客户端套接字并连接服务端

客户端套接字的创建采用了 C++ 智能指针的写法,使用 std::unique_ptr 作为管理对象,配合std::make_unique 完成内存创建。std::unique_ptr是独占式智能指针,保证套接字对象在整个客户端生命周期中仅被单一指针管理;而std::make_unique是 C++ 推荐的安全创建方式,会自动完成对象内存分配与智能指针封装,无需手动管理 new 和 delete,避免内存泄漏。

BuildTcpClientSockMethod() 内部只调用了 socket() 创建套接字,客户端不用绑定端口,操作系统会自动分配临时端口;InetAddr 把服务端的 IP 和端口封装成系统需要的地址结构;Connect() 和服务端建立连接;连接失败直接打印错误并退出。

2. 初始化协议层(绑定打印回调)

创建 Protocol 对象,把客户端的打印回调传进去;后面解析服务端数据时,协议层会自动调用 HanlderResponse 打印结果;inbuffer 专门用来存服务端发回来的、带长度头的完整报文。

3. 主循环:循环输入、发请求、收响应

我们先在循环外层定义了发送缓冲区 outbuffer,它专门用来存储待发送至服务端、已完成协议封包的完整报文。随后程序进入三次嵌套循环,实现连续输入三组计算请求的逻辑,每次输入都会先构建 Request 对象,依托我们此前定义的请求类,存储输入的操作数 x、y 与运算符。接着依次执行序列化操作,将结构化的 Request 对象转换为标准 JSON 字符串,再通过提前初始化好的协议对象,调用其 Packet 封包方法,为 JSON 字符串添加 4 字节长度头,完成报文边界封装,处理后的结果存入send_req_string,此时该变量中已是一条独立完整、可直接传输的封包报文。

因为要连续发送三组请求,我们便将每次生成的完整封包报文,逐次追加到 outbuffer 发送缓冲区中,最终让三条完整报文拼接在一起。这样的设计,本质是为了主动测试 TCP 粘包场景,验证服务端能否凭借报文头部的长度字段,正确拆分、解析粘连的多条数据,也能充分检验我们自定义协议处理粘包问题的有效性。

5. 发送、接收、解析(和服务端收发逻辑闭环)

时outbuffer 中已存储三条连续拼接、各自携带 4 字节长度头的完整封包报文,每条报文都是独立合规的请求数据,我们可以先将其打印输出,直观查看拼接后的报文形态。随后直接调用套接字的Send 方法,将 outbuffer 中拼接好的三条报文一次性发送给服务端。

接下来调用套接字的 Recv 方法接收服务端返回的数据,这里的 inbuffer 是标准输出型参数,和服务端接收数据的逻辑完全对应,Recv 函数会将服务端传回的响应数据直接填充至 inbuffer 中,此时 inbuffer 里同样是三条连续拼接的封包报文。但是这个报文已经是我们计算好的报文,数据接收完成后,通过协议对象调用 ParseResponse 方法,对 inbuffer 中的连续报文逐一分包、解包,再将 JSON 格式的数据反序列化为 Response 对象,过程中会自动调用我们提前注册的HandleResponse 静态打印回调,将计算结果、状态码直观输出到控制台。

循环结束后关闭套接字,断开和服务端的连接。

到现在为止我们所有的文件和模块都已经讲解完了,现在我们再梳理一下客户端和服务端的逻辑 :

八、运行结果:

客户端:

客户端运行现象验证了项目通信全链路:客户端连续输入三条计算请求,经序列化、添加长度头封包后批量发送,控制台打印出的长度头与 JSON 分行显示,是封包时 \r\n 换行符作用于终端的视觉效果;服务端接收报文后完成解包、计算、封包并回传响应,客户端解析后触发打印回调,输出带错误码的结果,精准区分正常计算与除零异常;协议层解析完成后打印日志标记单次通信结束,客户端循环等待新一轮输入。

\n 代表换行,使光标下移一行;\r 代表回车,使光标回到当前行的起始位置,二者组合是跨系统的标准换行格式。

服务端:

服务端日志完整还原通信全流程:先是建立客户端连接,生成专属通信套接字;接着读取客户端批量发来、发生粘包的三条完整封包报文;随后按长度头逐次解包,提取纯净 JSON 请求,剩余未解析报文逐步减少;全部解析完成后完成业务计算、响应封包,输出待发送的响应报文,整套流程验证了自定义协议成功处理 TCP 粘包问题。

九、深度剖析 OSI 七层模型

至此,我们已经完整实现并拆解了整套 TCP 计算器项目的全链路代码与运行逻辑。而当我们将项目架构与经典的OSI 七层网络参考模型对照时,会发现我们亲手编写的每一行业务代码,都精准对应并在用户态模拟实现了 OSI 上三层的核心功能。下面我们就结合代码,深度剖析项目与网络模型的映射关系,从底层原理上升华整个项目的设计思想。

OSI 七层模型从上至下分为:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。其中,下四层(传输层、网络层、数据链路层、物理层)由 Linux 操作系统内核协议栈全权实现,我们无法在用户态代码中直接模拟;而上三层(应用层、表示层、会话层)则完全暴露给用户,由我们的业务代码来实现和定义。我们服务端主函数中定义的三大核心对象,正是对这三层模型最精准的代码级复刻。

1. 应用层:Calculator 业务层(对应 OSI 第七层)

OSI 模型中,应用层的核心定义是 "针对特定应用的协议",它直接面向用户,定义具体的业务功能。在我们的项目中,第一行代码创建的 Calculator 计算器对象,正是应用层的直接体现。计算器的加减乘除逻辑,就是本次网络通信的 "特定应用",是整个 TCP 传输最终要服务的业务目的。数据穿过层层网络,最终就是要交给这一层来处理和计算。因此,Calculator业务层,完美承担了 OSI 应用层的职责。

2. 表示层:Protocol 协议层(对应 OSI 第六层)

OSI 模型中,表示层的核心定义是 "设备固有数据格式和网络标准数据格式的转换"。简单来说,就是负责数据的序列化、反序列化、加密、压缩,解决 "不同设备怎么看懂同一份数据" 的问题。我们项目中的Protocol协议层,正是表示层的代码实现。客户端要把内存中的 Request 对象(设备私有格式)序列化为 JSON 字符串,再封包为网络字节流(网络标准格式);服务端则要将网络字节流解包、反序列化为 Request 对象。这一套序列化 / 反序列化、封包 / 解包的逻辑,正是表示层最核心的 "格式转换" 工作。我们通过自定义协议,亲手定义了网络数据的传输格式,就是在实现表示层的功能。

3. 会话层:TcpServer 网络管理层(对应 OSI 第五层)

OSI 模型中,会话层的核心定义是 "通信管理,负责建立、断开和维护通信连接",它负责管理两个设备之间的逻辑通路。我们项目中的 TcpServer 网络服务对象,承担的正是会话层的职责。服务端通过它完成监听、Accept 获取新连接、管理客户端的通信生命周期、收发数据、断开连接。我们封装的 Socket 类,本质就是在管理 TCP 会话,控制着连接的建立与销毁。因此,TcpServer 网络管理层,精准对应了 OSI 会话层的通信管理功能。

4. 下四层:操作系统内核全权接管

而 OSI 模型中剩下的传输层(TCP)、网络层(IP)、数据链路层、物理层,全部由 Linux 操作系统内核协议栈帮我们完成。我们代码中调用的 socket、connect、send、recv 等系统调用,本质上就是我们的应用程序向操作系统内核 "下达指令",由内核去完成 TCP 握手、路由寻址、网卡驱动等底层复杂工作。这些底层细节被操作系统完全封装,我们无法、也无需在用户态代码中模拟。

5. 总结

我们整个 TCP 计算器项目,本质上就是一次OSI 上三层的完整落地实践:我们用Calculator实现了应用层的业务逻辑,用Protocol实现了表示层的数据格式转换,用TcpServer实现了会话层的连接管理。通过分层回调将这三层解耦,各司其职,完美复刻了网络模型的设计思想。这也是我们为什么要分层、为什么要解耦的根本原因 ------代码分层,就是在模拟网络分层。理解了这一点,才算真正吃透了网络编程的本质。

十、代码展示

Socket.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include "Logger.hpp"
#include "InetAddr.hpp"


namespace NS_SOCKET_MODULE
{
    using namespace NS_LOG_MODULE;

    static const int gbacklog = 16;

    enum
    {
        OK = 0,
        SOCKET_ERR,
        BIND_ERR,
        LISTEN_ERR
    };

    // 模版方法模式!
    class Socket
    {
    public:
        ~Socket()
        {}

    protected:
        virtual void CreateSocketOrDie() = 0;
        virtual void BindSocketOrDie(uint16_t port) = 0;
        virtual void ListenSocketOrDie() = 0;

    public:
        virtual std::shared_ptr<Socket> Accepter(InetAddr &addr) = 0;
        virtual int Sockfd() = 0;
        virtual int Recv(std::string *out) = 0;
        virtual int Send(const std::string &in) = 0;
        virtual void Close() = 0;
        virtual bool Connect(InetAddr &addr) = 0;
    public:
        void BuildTcpSocketMethod(uint16_t port) // 模版方法
        {
            CreateSocketOrDie();
            BindSocketOrDie(port);
            ListenSocketOrDie();
        }
        void BuildTcpClientSockMethod()
        {
            CreateSocketOrDie();
        }
    };

    class TcpSocket : public Socket
    {
    public:
        TcpSocket() : _sockfd(0)
        {
        }
        TcpSocket(int sockfd): _sockfd(sockfd)
        {}
        void CreateSocketOrDie() override
        {
            _sockfd = socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
            {
                LOG(LogLevel::FATAL) << "create socket error";
                exit(SOCKET_ERR);
            }
        }
        void BindSocketOrDie(uint16_t port) override
        {
            InetAddr addr(port);
            if (bind(_sockfd, addr.NetAddress(), addr.Len()) != 0)
            {
                LOG(LogLevel::FATAL) << "bind socket error";
                exit(BIND_ERR);
            }
        }
        void ListenSocketOrDie() override
        {
            if (listen(_sockfd, gbacklog) != 0)
            {
                LOG(LogLevel::FATAL) << "listen socket error";
                exit(LISTEN_ERR);
            }
        }
        std::shared_ptr<Socket> Accepter(InetAddr &clientaddr) override
        {
            struct sockaddr_in addr;
            socklen_t len = sizeof(addr);
            int sockfd = accept(_sockfd, CONV(&addr), &len);
            if(sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept error";
                return nullptr;
            }
            clientaddr = addr;
            return std::make_shared<TcpSocket>(sockfd);
        }
        int Sockfd() override
        {
            return _sockfd;
        }

        int Recv(std::string *out) override
        {
            char inbuffer[1024];
            ssize_t n = recv(_sockfd, inbuffer, sizeof(inbuffer)-1, 0);
            if(n > 0)
            {
                inbuffer[n] = 0;
                *out += inbuffer; // 追加写入的
            }
            return n;
        }
        
        int Send(const std::string &in) override
        {
            return send(_sockfd, in.c_str(), in.size(), 0);
        }
        void Close() override
        {
            if(_sockfd>=0)
            {
                close(_sockfd);
                _sockfd = -1;
            }
        }
        bool Connect(InetAddr &addr) override
        {
            int n = connect(_sockfd, addr.NetAddress(), addr.Len());
            if(n < 0)
                return false;
            else
                return true;
        }

        ~TcpSocket() {}

    private:
        int _sockfd;
    };

    // class UdpSocket: public Socket
    // {

    // };

} 

Protocol.hpp

cpp 复制代码
#pragma once

// 自定义协议部分
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
#include <functional>
#include "Logger.hpp"

using namespace NS_LOG_MODULE;

// 请求报文
class Request
{
public:
    Request() : _data_x(0), _data_y(0), _oper(0)
    {
    }
    Request(int x, int y, char oper) : _data_x(x), _data_y(y), _oper(oper)
    {
    }
    bool Serialize(std::string *out)
    {
        // 结构化 -> "_data_x _oper _data_y"
        Json::Value root;
        root["left"] = _data_x;
        root["right"] = _data_y;
        root["oper"] = _oper;

        Json::FastWriter writer;
        *out = writer.write(root);
        return true;
    }
    bool Deserialize(std::string &in) // "_data_x _oper _data_y"
    {
        // "_data_x _oper _data_y" -> 结构化
        Json::Value root;
        Json::Reader reader;
        bool parsesuccess = reader.parse(in, root);
        if (!parsesuccess)
            return false;

        _data_x = root["left"].asInt();
        _data_y = root["right"].asInt();
        _oper = root["oper"].asInt();
        return true;
    }
    ~Request()
    {
    }
    // get set

public:
    // 10 20 '-' -> 10 - 20 = ?
    // _data_x _oper _data_y
    int _data_x;
    int _data_y;
    char _oper; // '+' '-' '/' '*' '%'
};

// 应答报文
class Response
{
public:
    Response() : _result(0), _code(0)
    {
    }
    Response(int result, int code) : _result(result), _code(code)
    {
    }
    bool Serialize(std::string *out)
    {
        Json::Value root;
        root["result"] = _result;
        root["code"] = _code;

        Json::FastWriter writer;
        *out = writer.write(root);
        return true;
    }
    bool Deserialize(std::string &in)
    {
        Json::Value root;
        Json::Reader reader;
        bool parsesuccess = reader.parse(in, root);
        if (!parsesuccess)
            return false;

        _result = root["result"].asInt();
        _code = root["code"].asInt();
        return true;
    }
    ~Response()
    {
    }

public:
    int _result; // 结果
    int _code;   // 状态码
};

const std::string gsep = "\r\n";

using HandlerRequest_t = std::function<Response(Request &)>;
using HandlerResponse_t = std::function<void (Response &)>;

class Protocol
{
public:
    Protocol(HandlerRequest_t handler) : _version("1.0"), _handler_request(handler)
    {
    }
    Protocol(HandlerResponse_t handler_response):_version("1.0"), _handler_response(handler_response)
    {
    }
    // {"left": 10, "right": 20, oper: '+'}
    // len\r\n{"left": 10, "right": 20, oper: '+'}\r\n
    std::string Packet(const std::string &json_string)
    {
        return std::to_string(json_string.size()) + gsep + json_string + gsep;
    }
    // len\r\n{"left": 10, "right": 20, oper: '+'}\r\n
    // len\r\n{"left": 10, "right": 20, oper: '+'}\r\nlen\r\n{"left": 10, "right": 20, oper: '+'}\r\n
    // len\r\n{"left": 10, "right": 20, oper: '+'}\r\nlen\r\n{"left": 10,
    // len\r\n{"left": 10, "right": 20, o
    // le
    // ret > 0: no error, json_string != NULL
    // ret == 0: no error, json_string == NULL
    // ret < 0 : error.
    int Unpack(std::string &packet, std::string *json_string)
    {
        if (packet.empty())
            return 0;
        if (json_string == nullptr)
            return -1;

        // 分析报文
        auto pos = packet.find(gsep);
        if (pos == std::string::npos)
            return 0;
        std::string lenstr = packet.substr(0, pos);

        // lenstr 合法性判断,lenstr -> 123 345

        int len = std::stoi(lenstr);
        int total = lenstr.size() + len + 2 * gsep.size();
        if (packet.size() < total)
            return 0;
        // 提取报文
        *json_string = packet.substr(pos + gsep.size(), len);
        packet.erase(0, total);
        return 1;
    }
    // 如果读到半个报文,什么都不做
    // 如果读到一个报文+,循环处理,把所有合法的报文都进行统一处理
    std::string ParseRequest(std::string &inbuffer)
    {
        std::string result;
        while (true)
        {
            std::string json_string;
            // 1. 解包
            int n = Unpack(inbuffer, &json_string);
            if (n < 0)
            {
                LOG(LogLevel::DEBUG) << "no way !!";
                return std::string();
            }
            if (n == 0)
            {
                LOG(LogLevel::INFO) << inbuffer << " parse done";
                return result;
            }
            LOG(LogLevel::DEBUG) << "json_string:\n" << json_string;
            LOG(LogLevel::DEBUG) << "unpack done, inbuffer:\n" << inbuffer;
            // 2. 反序列化
            // 得到一个完整的报文jsonstring
            Request req;
            if (!req.Deserialize(json_string))
                return std::string();

            // 3. 业务计算
            Response resp;
            if (_handler_request)
                resp = _handler_request(req);

            // 4. 应答序列化
            std::string resp_json_string;
            resp.Serialize(&resp_json_string);

            // 5. 添加报头
            result += Packet(resp_json_string);
        }
    }
    std::string ParseResponse(std::string &inbuffer)
    {
        while (true)
        {
            std::string json_string;
            // 1. 解包
            int n = Unpack(inbuffer, &json_string);
            if (n < 0)
            {
                LOG(LogLevel::DEBUG) << "no way !!";
                return std::string();
            }
            if (n == 0)
            {
                LOG(LogLevel::INFO) << inbuffer << " parse done";
                return std::string();
            }
            // 2. 反序列化
            // 得到一个完整的报文jsonstring
            Response resp;
            if (!resp.Deserialize(json_string))
                return std::string();
            
            // 3. 回调处理
            if (_handler_response)
                _handler_response(resp);
        }
    }

    ~Protocol()
    {
    }

private:
    std::string _version;
    HandlerRequest_t _handler_request;
    HandlerResponse_t _handler_response;
};

Calculator.hpp

cpp 复制代码
#pragma once

#include "Protocol.hpp"
#include <iostream>
#include <string>

class Calculator
{
public:
    Response Execute(const Request &req)
    {
        Response resp;
        switch (req._oper)
        {
        case '+':
            resp._result = req._data_x + req._data_y;
            break;
        case '-':
            resp._result = req._data_x - req._data_y;
            break;
        case '*':
            resp._result = req._data_x * req._data_y;
            break;
        case '/':
        {
            if (req._data_y == 0)
                resp._code = 1; // div error
            else
                resp._result = req._data_x / req._data_y;
        }
        break;
        case '%':
        {
            if (req._data_y == 0)
                resp._code = 2; // mod error
            else
                resp._result = req._data_x % req._data_y;
        }
        break;
        default:
            resp._code = 3; // 非法操作
            break;
        }
        return resp;
    }
};

TcpServer.hpp

cpp 复制代码
#include "Logger.hpp"
#include "InetAddr.hpp"
#include "Socket.hpp"
#include <memory>
#include <unistd.h>
#include <signal.h>
#include <functional>

static uint16_t gport = 8080;

using namespace NS_SOCKET_MODULE;

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

class TcpServer
{
public:
    TcpServer(Handler_t handler, uint16_t port = gport) 
    : _port(port),
     _listensock(std::make_unique<TcpSocket>()),
     _handler(handler)
    {
        _listensock->BuildTcpSocketMethod(_port); // 使用创建listensockfd的模版方法!
        LOG(LogLevel::INFO) << "create listen socket success: " << _listensock->Sockfd();
    }
    void Loop()
    {
        signal(SIGCHLD, SIG_IGN);
        while (true)
        {
            InetAddr clientaddr;
            auto sockfd = _listensock->Accepter(clientaddr);
            if (!sockfd)
                continue;
            LOG(LogLevel::DEBUG) << "get a new link, socket address: " << clientaddr.ToString() << " sockfd: " << sockfd->Sockfd();

            if (fork() == 0)
            {
                // child
                service(sockfd, clientaddr);
                sockfd->Close();
                exit(0);
            }
        }
    }
    ~TcpServer()
    {
    }

private:
    void service(std::shared_ptr<Socket> sockfd, InetAddr &clientaddr)
    {
        std::string inbuffer, outbuffer;
        while (true)
        {
            outbuffer.clear();
            
            int n = sockfd->Recv(&inbuffer);
            if(n <= 0)
            {
                LOG(LogLevel::WARNING) << "recv: client quit, " << clientaddr.ToString();
                break;
            }

            LOG(LogLevel::DEBUG) << "inbuffer:\n" << inbuffer;

            if(_handler)
                outbuffer += _handler(inbuffer);

            if(outbuffer.empty())
                continue;

            LOG(LogLevel::DEBUG) << "outbuffer:\n" << outbuffer;
            n = sockfd->Send(outbuffer);
            if(n < 0)
            {
                LOG(LogLevel::WARNING) << "send: client quit, " << clientaddr.ToString();
                break;
            }
        }
    }

private:
    uint16_t _port;
    std::unique_ptr<Socket> _listensock;
    Handler_t _handler;
};

NetCalServer.cc

cpp 复制代码
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "Calculator.hpp"
#include <memory>


static void Usage(const std::string &proc)
{
    std::cout << "Usage:\n\t" << proc << " port" << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }
    uint16_t port = std::stoi(argv[1]);
    // 1. 定义计算机
    std::unique_ptr<Calculator> cal = std::make_unique<Calculator>();

    // 2. 定义协议对象
    std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>(
        [&cal](Request &req)->Response{
            return cal->Execute(req);
        }
    );

    // 3. 定义网络对象
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(
        [&protocol](std::string &inbuffer)->std::string{
            return protocol->ParseRequest(inbuffer);
        }, port
    );
    
    // 4. 启动
    tsvr->Loop();

    return 0;
}

NetCalClient.cc

cpp 复制代码
#include <iostream>
#include <string>
#include <memory>
#include "InetAddr.hpp"
#include "Socket.hpp"
#include "Protocol.hpp"

using namespace NS_SOCKET_MODULE;

static void Usage(const std::string &proc)
{
    std::cout << "Usage:\n\t" << proc << " server_ip server_port" << std::endl;
}

static void HanlderResponse(Response &resp)
{
    std::cout << "result: " << resp._result << "[" << resp._code << "]" << std::endl;
}

// ./netcal_client 目标IP 目标主机端口
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

    std::unique_ptr<Socket> socket = std::make_unique<TcpSocket>();
    socket->BuildTcpClientSockMethod();

    InetAddr serveraddress(server_port, server_ip);
    bool n = socket->Connect(serveraddress);
    if (!n)
    {
        std::cerr << "connect error: " << serveraddress.ToString() << std::endl;
        exit(2);
    }
    Protocol procotol(HanlderResponse);
    std::string inbuffer;
    while (true)
    {
        int cnt = 3;
        std::string outbuffer;
        while (cnt--)
        {
            // 0. 获取数据
            int x, y;
            char oper;
            std::cout << "Enter Your x: ";
            std::cin >> x;
            std::cout << "Enter Your y: ";
            std::cin >> y;
            std::cout << "Enter Your oper: ";
            std::cin >> oper;

            // 1. 定义请求对象
            Request req(x, y, oper);

            // 2. 序列化
            std::string req_json;
            req.Serialize(&req_json);

            // 3. 封装报头
            std::string send_req_string = procotol.Packet(req_json);
            outbuffer += send_req_string;
        }
        std::cout << "\n" << outbuffer << std::endl;

        // 4. 发送
        socket->Send(outbuffer);

        // 5. 接收
        socket->Recv(&inbuffer);

        // 6. 解析
        procotol.ParseResponse(inbuffer);
    }
    socket->Close();

    return 0;
}

Makefile

cpp 复制代码
.PHONY:all
all:netcal_server netcal_client

netcal_server:NetCalServer.cc
	g++ -o $@ $^ -std=c++17 -ljsoncpp

netcal_client:NetCalClient.cc
	g++ -o $@ $^ -std=c++17 -ljsoncpp

.PHONY:clean
clean:
	rm -f netcal_server netcal_client

InetAddr.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define CONV(address) ((struct sockaddr *)address)

// 对客户端进行先描述
class InetAddr
{
public:
    InetAddr()
    {
    }
    InetAddr(const struct sockaddr_in &address) : _address(address), _len(sizeof(address))
    {
        // _ip = inet_ntoa(_address.sin_addr);
        char ipstr[32];
        inet_ntop(AF_INET, &(_address.sin_addr), ipstr, sizeof(ipstr));
        _ip = ipstr;
        _port = ntohs(_address.sin_port);
    }
    InetAddr(uint16_t port, const std::string &ip = "0.0.0.0") : _ip(ip), _port(port)
    {
        bzero(&_address, sizeof(_address));
        _address.sin_family = AF_INET;
        _address.sin_port = htons(_port); // h->n
        //_address.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. 字符串ip->4字节IP 2. hton
        inet_pton(AF_INET, ip.c_str(), &(_address.sin_addr));
        _len = sizeof(_address);
    }
    bool operator==(const InetAddr &addr)
    {
        return (this->_ip == addr._ip) && (this->_port == addr._port);
    }
    void operator=(struct sockaddr_in &addr)
    {
        _address = addr;
        char ipstr[32];
        inet_ntop(AF_INET, &(_address.sin_addr), ipstr, sizeof(ipstr));
        _ip = ipstr;
        _port = ntohs(_address.sin_port);
    }
    std::string ToString()
    {
        return "[" + _ip + ":" + std::to_string(_port) + "]";
    }

    struct sockaddr *NetAddress()
    {
        return CONV(&_address);
    }
    socklen_t Len()
    {
        return _len;
    }
    ~InetAddr()
    {
    }

private:
    // net address
    struct sockaddr_in _address;
    socklen_t _len;
    // host address
    std::string _ip;
    uint16_t _port;
};

Logger.hpp

cpp 复制代码
#ifndef __LOGGER_HPP
#define __LOGGER_HPP

#include <iostream>
#include <cstdio>
#include <string>
#include <memory>
#include <sstream>
#include <ctime>
#include <sys/time.h>
#include <unistd.h>
#include <filesystem> // C++17
#include <fstream>
#include "Mutex.hpp"

namespace NS_LOG_MODULE
{
    enum class LogLevel
    {
        INFO,
        WARNING,
        ERROR,
        FATAL,
        DEBUG
    };
    std::string LogLevel2Message(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::INFO:
            return "INFO";
        case LogLevel::WARNING:
            return "WARNING";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        case LogLevel::DEBUG:
            return "DEBUG";
        default:
            return "UNKNOWN";
        }
    }

    // 1. 时间戳 2. 日期+时间
    std::string GetCurrentTime()
    {
        struct timeval current_time;
        int n = gettimeofday(&current_time, nullptr);
        (void)n;

        // current_time.tv_sec; current_time.tv_usec;
        struct tm struct_time;
        localtime_r(&(current_time.tv_sec), &struct_time); // r: 可重入函数
        char timestr[128];
        snprintf(timestr, sizeof(timestr), "%04d-%02d-%02d %02d:%02d:%02d.%ld",
                 struct_time.tm_year + 1900,
                 struct_time.tm_mon + 1,
                 struct_time.tm_mday,
                 struct_time.tm_hour,
                 struct_time.tm_min,
                 struct_time.tm_sec,
                 current_time.tv_usec);
        return timestr;
    }

    // 输出角度 -- 刷新策略
    // 1. 显示器打印
    // 2. 文件写入
    // 策略模式,策略接口
    class LogStrategy
    {
    public:
        virtual ~LogStrategy() = default;
        virtual void SyncLog(const std::string &message) = 0;
    };
    // 控制台日志刷新策略, 日志将来要向显示器打印
    class ConsoleStrategy : public LogStrategy
    {
    public:
        void SyncLog(const std::string &message) override
        {
            LockGuard lockguard(_mutex);
            std::cerr << message << std::endl; // ??
        }
        ~ConsoleStrategy()
        {
        }

    private:
        Mutex _mutex;
    };

    const std::string defaultpath = "./log";
    const std::string defaultfilename = "log.txt";


    // 文件策略
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const std::string &path = defaultpath, const std::string &name = defaultfilename)
            : _logpath(path),
              _logfilename(name)
        {
            LockGuard lockguard(_mutex);
            if (std::filesystem::exists(_logpath))
                return;
            try
            {
                std::filesystem::create_directories(_logpath);
            }
            catch (const std::filesystem::filesystem_error &e)
            {
                std::cerr << e.what() << '\n';
            }
        }

        void SyncLog(const std::string &message) override
        {
            {
                LockGuard lockguard(_mutex);
                if (!_logpath.empty() && _logpath.back() != '/')
                {
                    _logpath += "/";
                }
                std::string targetlog = _logpath + _logfilename; // "./log/log.txt"
                std::ofstream out(targetlog, std::ios::app);     // 追加方式写入
                if (!out.is_open())
                {
                    std::cerr << "open " << targetlog << "failed" << std::endl;
                    return;
                }
                out << message << "\n";
                out.close();
            }
        }

        ~FileLogStrategy()
        {
        }

    private:
        std::string _logpath;
        std::string _logfilename;
        Mutex _mutex;
    };

    // 日志类:
    // 1. 日志的生成
    // 2. 根据不同的策略,进行刷新
    class Logger
    {
        // 日志的生成:
        // 构建日志字符串
    public:
        Logger()
        {
            UseConsoleStrategy();
        }
        void UseConsoleStrategy()
        {
            _strategy = std::make_unique<ConsoleStrategy>();
        }
        void UseFileStrategy()
        {
            _strategy = std::make_unique<FileLogStrategy>();
        }
        // 内部类, 标识一条完整的日志信息
        //  一条完整的日志信息 = 做半部分固定部分 + 右半部分不固定部分
        //  LogMessage RAII风格的方式,进行刷新
        class LogMessage
        {
        public:
            LogMessage(LogLevel level, std::string &filename, int line, Logger &logger)
                : _level(level),
                  _curr_time(GetCurrentTime()),
                  _pid(getpid()),
                  _filename(filename),
                  _line(line),
                  _logger(logger)
            {
                // 先构建出来左半部分
                std::stringstream ss;
                ss << "[" << _curr_time << "] "
                   << "[" << LogLevel2Message(_level) << "] "
                   << "[" << _pid << "] "
                   << "[" << _filename << "] "
                   << "[" << _line << "] "
                   << " - ";

                _loginfo = ss.str();
            }
            template <typename T>
            LogMessage &operator<<(const T &info)
            {
                std::stringstream ss;
                ss << info;
                _loginfo += ss.str();
                return *this; // 返回当前LogMessage对象,方便下次继续进行<<
            }

            ~LogMessage()
            {
                if (_logger._strategy)
                {
                    _logger._strategy->SyncLog(_loginfo);
                }
            }

        private:
            LogLevel _level;
            std::string _curr_time;
            pid_t _pid;
            std::string _filename;
            int _line;
            std::string _loginfo; // 一条完整的日志信息

            // 一个引用,引用外部的Logger类对象
            Logger &_logger; // 方便我们后续进行策略式刷新
        };

        // 这里已经不是内部类了
        // 故意采用拷贝LogMessage
        LogMessage operator()(LogLevel level, std::string filename, int line)
        {
            return LogMessage(level, filename, line, *this);
        }

        ~Logger()
        {
        }

    private:
        std::unique_ptr<LogStrategy> _strategy; // 刷新策略
    };

    // 日志对象,全局使用
    Logger logger;

#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy();
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy();

#define LOG(level) logger(level, __FILE__, __LINE__)

}

#endif

Mutex.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <pthread.h>

class Mutex
{
public:
    Mutex()
    {
        pthread_mutex_init(&_lock, nullptr);
    }
    void Lock()
    {
        pthread_mutex_lock(&_lock);
    }
    pthread_mutex_t *Ptr()
    {
        return &_lock;
    }
    void Unlock()
    {
        pthread_mutex_unlock(&_lock);
    }
    ~Mutex()
    {
        pthread_mutex_destroy(&_lock);
    }
private:
    pthread_mutex_t _lock;
};

class LockGuard // RAII风格代码
{
public:
    LockGuard(Mutex &lock):_lockref(lock)
    {
        _lockref.Lock();
    }
    ~LockGuard()
    {
        _lockref.Unlock();
    }
private:
    Mutex &_lockref;
};

十一、总结

本文详细介绍了如何实现一个基于TCP协议的网络计算器系统。该系统采用分层架构设计,包含网络通信层(TcpServer)、协议解析层(Protocol)和业务计算层(Calculator),分别对应OSI模型中的会话层、表示层和应用层。通过自定义应用层协议解决TCP粘包问题,使用JSON进行序列化和反序列化,实现了客户端请求的发送和服务端结果的返回。系统采用回调机制实现模块解耦,支持多进程并发处理客户端请求。文章从设计框架到具体实现,详细讲解了Socket封装、协议处理、业务逻辑等核心模块,并展示了运行效果和与OSI七层模型的对应关系。

谢谢大家的观看!

相关推荐
Misnice8 小时前
DevOps 介绍
运维·devops
liann1198 小时前
3.3_tasklist和netstat命令详解
运维·windows·计算机网络·安全·信息与通信
mounter6258 小时前
Linux Kernel Design Patterns (Part 2):从经典链表到现代 XArray,拆解内核复杂数据结构的设计哲学
linux·数据结构·链表·设计模式·内存管理·kernel
aodunsoft8 小时前
安全月报 | 傲盾DDoS攻击防御2026年4月简报
网络·安全·ddos
Lucis__8 小时前
HTTP协议深度解析—从HTTP原理到手写实现服务器
服务器·网络协议·http
虚幻如影8 小时前
web端安全测试报告模板
linux·服务器·安全
拜托啦!狮子8 小时前
本地连接服务器并运行jupyter
服务器·jupyter·github
郝学胜-神的一滴8 小时前
epoll 反应堆模型深度拆解:从红黑树到回调闭环,手写高性能回射服务器
linux·运维·服务器·开发语言·c++·unix
sdszoe49228 小时前
Windows server服务器AD+DC网络服务器运维管理方式
运维·服务器·windows·ad+dc·集中式管理·域控制器dc