ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

【C++标准项目】发布订阅式消息队列(篇三):C++ 第三方库实战:基于 muduo 库函数实现 protobuf 协议的通信

【C++标准项目】发布订阅式消息队列(篇三):C++ 第三方库实战:基于 muduo 库函数实现 protobuf 协议的通信 大致流程编译阶段通过 protoc 工具编译 proto 文件自动生成 C 序列化代码request.pb.h与request.pb.cc业务代码引入头文件即可使用消息结构体。客户端构造 Protobuf 消息对象序列化后使用 ProtobufCodec 完成封帧通过 muduo TcpClient 发送二进制报文。网络传输报文通过 TCP 传输到服务端。服务端muduo 触发 onMessage 回调ProtobufCodec 完成拆包反序列化得到消息对象ProtobufDispatcher 根据消息类型路由到对应的业务回调函数 onTranslate/onAdd。业务处理执行翻译或者加法计算构造响应消息同样使用 Codec 封帧后调用 conn-send 发回客户端。客户端收到响应报文拆包反序列化打印最终业务结果一. 先定义具体的业务请求类型request.proto ├── syntax proto3; - 语法版本必须在第一行 ├── package bit; - 命名空间 └── 四个 message ├── TranslateRequest { string msg 1; } 1 字段 ├── TranslateResponse { string msg 1; } 1 字段 ├── AddRequest { int32 num1 1; 2 字段 │ int32 num2 2; } └── AddResponse { int32 result 1; } 1 字段syntax proto3;声明使用 protobuf3 版本语法proto3 默认值规则简化不需要required/optional修饰。package bit;命名空间 bitprotoc 编译生成 C 代码后所有消息类都放在 bit:: 命名空间下防止类名冲突。C 里使用bit::TranslateRequest req;message TranslateRequest客户端发给服务端的翻译请求消息string msg 1;字符串字段msg1 是字段编号不是变量值protobuf 二进制序列化靠编号识别字段编号一旦上线不能随便修改。message TranslateResponse服务端返回给客户端的翻译响应消息同样携带字符串 msg。message AddRequest加法请求客户端传递两个整数int32 num1 1; 第一个整数int32 num2 2; 第二个整数message AddResponse加法响应服务端返回相加结果int32 result 1; 相加后的结果整体作用这个 proto 文件定义两组 RPC 风格消息对文本翻译TranslateRequest(请求) -- TranslateResponse(响应)整数加法AddRequest(请求) -- AddResponse(响应)客户端把请求结构体序列化二进制通过 muduo 网络库发给服务端服务端收到二进制反序列化成对应消息对象处理业务后把响应消息序列化发回客户端。protoc --cpp_out. request.proto执行后会生成request.pb.h 头文件request.pb.cc 源文件在 muduo 代码里#include request.pb.h就可以使用bit::下面所有消息类.二. 实现服务端提供的服务这是Muduo Protobuf 服务端代码采用多 Reactor 模型。 作用监听 8085 端口接收客户端发来的 Protobuf 请求报文依靠ProtobufCodec做拆包 / 反序列化ProtobufDispatcher根据消息类型路由到对应的业务回调函数处理完成后构造 Response再通过 Codec 封帧回发给客户端。1. 成员变量_baseloop主 Reactor 事件循环负责监听套接字接收新连接分发连接到 sub reactor。_serverMuduo 的 TcpServer封装 TCP 服务监听0.0.0.0:8085开启端口复用。_dispatcherProtobuf 消息分发器。注册 Request 请求回调收到解码后的 pb 消息根据消息类型自动路由到对应业务函数。_codecProtobuf 编解码器。核心两件事接收方向TCP 二进制流 → 拆包 → 反序列化为 protobuf 消息对象。发送方向protobuf 对象 → 序列化 → 封装[长度][类型名][pb数据]帧头解决 TCP 粘包。2. 构造函数初始化 TcpServer监听 8085 端口。向 Dispatcher 注册两个业务回调TranslateRequest→onTranslate翻译业务AddRequest→onAdd加法业务设置messageCallback网络收到数据交给ProtobufCodec::onMessage做解码。设置connectionCallback连接建立 / 断开打印日志。3. start()启动 TcpServer开启监听然后启动主事件循环_baseloop.loop()主线程阻塞在这里持续处理 IO 事件。4. 业务回调函数 onTranslate /onAdd执行具体业务逻辑构造响应 Protobuf 对象调用_codec.send(conn, resp)把响应封帧发回客户端。重点发送响应同样走 Codec 封帧和我们之前 Mermaid 流程图里 “回程 Codec 封帧” 完全对应。5. onUnknownMessage兜底回调收到协议里没有注册的消息类型打印日志并关闭连接。用于非法报文容错。6. onConnection连接回调客户端建立连接 / 断开连接时打印对端 IP 端口方便调试。7. main 函数创建 Server 实例监听 8085调用 start 启动服务。8.服务端完整运行流程服务端启动创建EventLoop与TcpServer监听 8085 端口进入事件循环等待客户端连接。客户端发起 TCP 连接三次握手成功触发onConnection打印新连接日志。客户端发送经过 Codec 封帧的二进制请求报文。Muduo 收到 TCP 数据触发messageCallback交给ProtobufCodec按长度字段拆包取出完整报文根据类型名字段反序列化为对应的 Protobuf Request 对象。ProtobufDispatcher根据消息类型路由到注册好的业务函数如果是TranslateRequest→onTranslate如果是AddRequest→onAdd。执行业务逻辑字典翻译 / 加法计算构造 Response 响应对象。调用_codec.send(conn, resp)序列化 Codec 封帧通过 TCP 发回客户端。客户端收到报文Codec 拆包反序列化Dispatcher 路由到客户端回调打印结果。9.注意事项Dispatcher 两端注册消息相反服务端注册Request 请求消息回调处理客户端请求客户端注册Response 响应消息回调处理服务端返回结果Codec 封帧是双向的客户端发请求要封帧服务端返回响应也要封帧这是 TCP 流协议必须处理粘包问题。初始化成员顺序代码里成员定义顺序必须和初始化列表顺序一致否则会出现未定义行为。0.0.0.0监听本机所有网卡地址支持虚拟机 / 本机回环、局域网其他机器访问。三.实现客户端提供的服务这是基于 Muduo Protobuf 的客户端代码和刚才的服务端配套。 客户端使用TcpClient发起 TCP 连接同样复用ProtobufCodec编解码、封帧拆包和ProtobufDispatcher消息分发。 客户端负责构造请求消息、发送给服务端收到服务端返回的 Response 消息后根据消息类型分发到对应的回调函数打印结果。1. 成员变量EventLoopThread _loopthread单独开一个 IO 线程客户端网络事件循环跑在这个线程不和主线程冲突。TcpClient _clientMuduo 封装好的 TCP 客户端。CountDownLatch _latch同步阻塞工具。主线程调用_latch.wait()卡住当 TCP 连接成功触发onConnection回调调用_latch.countDown()主线程才会继续往下执行。保证连接建立成功之后才发送请求避免请求发送失败。ProtobufCodec _codec和服务端完全一致负责序列化 / 反序列化、封帧拆包帧格式[长度][类型名][pb数据]自动处理 TCP 粘包。ProtobufDispatcher _dispatcher消息分发器。客户端这里注册的是 Response 响应消息回调和服务端反过来服务端注册 Request 回调客户端注册 Response 回调。2. 构造函数初始化 TcpClient指定服务端 IP 端口。给 Dispatcher 注册回调收到TranslateResponse就调用onTranslate收到AddResponse调用onAdd。设置网络回调TCP 收到数据交给ProtobufCodec::onMessage做解码。3. connect () 连接函数void connect() { _client.connect(); _latch.wait(); }发起 TCP 连接主线程阻塞等待直到连接成功回调触发_latch.countDown()解锁。4. 对外接口 Translate / Add对外简单 API业务层直接调用。内部构造对应的TranslateRequest/AddRequestproto 消息对象调用私有send()发送。5. send () 发送函数bool send(const google::protobuf::Message *message) { if (_conn-connected()) { _codec.send(_conn, *message); return true; } return false; }_codec.send()自动三件套Protobuf 对象序列化为二进制数据 → 封装[长度][类型名][pb数据]帧头 → 写入 TCP 发送缓冲区。和服务端发送响应的逻辑完全对称。6. 消息回调 onTranslate /onAdd客户端收到服务端返回的响应报文经过 Codec 拆包反序列化Dispatcher 查表路由到这两个回调直接打印结果。7. onConnection 连接回调连接成功保存_conn连接句柄_latch.countDown()唤醒主线程。 连接断开清空连接句柄。8. main 函数创建客户端实例连接127.0.0.1:8085。等待连接建立完成。发送 2 条请求翻译 hello、加法 1122。sleep (1)等待服务端响应返回防止主线程直接退出IO 线程来不及接收响应。9.客户端完整运行流程程序启动创建Client对象内部启动独立 IO 线程。client.connect()发起 TCP 连接主线程阻塞等待。TCP 三次握手成功触发onConnection_latch.countDown()主线程解除阻塞。client.Translate(hello)构造TranslateRequest对象 →send()→_codec.send()序列化 封帧 → 通过 muduo TcpClient 发送二进制报文。client.Add(11,22)构造AddRequest同样封帧发送。报文经过 TCP 网络传输到达服务端就是前面的服务端代码。服务端处理完成构造 Response 响应消息_codec.send()封帧发回客户端。客户端收到二进制数据muduo 触发 onMessage交给 ProtobufCodec 拆包、反序列化为 Response 对象。Dispatcher 根据消息类型路由如果是TranslateResponse→ 调用onTranslate打印翻译结果。如果是AddResponse→ 调用onAdd打印加法结果。sleep (1) 等待接收完毕程序退出。10.注意事项CountDownLatch 作用不使用 Latch 的话主线程直接往下执行发送请求此时 TCP 连接还没建立完成消息发送失败。客户端和服务端 Dispatcher 注册消息类型相反服务端注册Request 请求消息回调处理客户端发来请求。客户端注册Response 响应消息回调处理服务端返回结果。sleep (1) 的作用主线程退出会直接杀死整个进程IO 线程来不及接收服务端回包。正式项目不能用 sleep这里是简易 demo 写法。客户端和服务端必须使用同一套 ProtobufCodec 帧协议否则两端无法正确拆包解析。11.整体通信完整链路汇总编译 proto 文件protoc --cpp_out. request.proto生成.pb.h/.pb.cc客户端构造 Request 对象 → Codec 序列化 封帧 → muduo TcpClient 发送 → TCP 网络传输服务端收到二进制 → Codec 拆包反序列化 → Dispatcher 路由到业务函数 → 业务处理 → 构造 Response → Codec 序列化 封帧 → TCP 发回客户端收到响应Codec 拆包反序列化 → Dispatcher 路由到客户端回调函数打印结果四.编译运行文件清单├── request.proto # protobuf协议定义 ├── request.pb.h # protoc生成 ├── request.pb.cc # protoc生成 ├── codec.cc # muduo protobuf编解码封装 ├── protobuf_server.cpp # 服务端代码 ├── protobuf_client.cpp # 客户端代码 └── Makefile # 编译脚本MUDUO : /home/hjq/mq/third/muduo-master定义变量MUDUO保存 muduo 源码根目录。后面$(MUDUO)会替换成这个路径方便后续修改路径。all: client server默认目标执行make不带参数时会编译生成client和server两个可执行程序。client: protobuf_client.cpp request.pb.cc codec.cc冒号前面目标client最终客户端程序冒号后面依赖文件列表要编译客户端依赖这 3 个源码文件protobuf_client.cpp我们写的客户端主代码request.pb.ccprotoc 编译 proto 文件自动生成的 protobuf 源码codec.ccmuduo 封装的 protobuf 编解码代码ProtobufCodecMake 规则只要依赖文件有改动就会执行下面的 g 编译命令重新生成 client。客户端 g 编译命令g -stdc11 protobuf_client.cpp request.pb.cc codec.cc -o client -I$(MUDUO) -I../include -I. -L../lib -lmuduo_net -lmuduo_base -lprotobuf -pthread -lz参数拆解-stdc11启用 C11 标准muduo 大量依赖 C11 特性-o client输出的可执行文件名字叫 client-Ixxx头文件搜索路径-I$(MUDUO)去 muduo 源码目录找 muduo 头文件-I../include上层 include 目录-I.当前目录找头文件request.pb.h-L../lib库文件搜索路径去上层 lib 文件夹找编译好的 muduo 静态库-lxxx链接库-lmuduo_netmuduo 网络库-lmuduo_basemuduo 基础库日志、线程、CountDownLatch 等-lprotobufprotobuf 库序列化反序列化-pthread链接线程库muduo 多线程必须-lzzlib 压缩库muduo 依赖server: protobuf_server.cpp request.pb.cc codec.cc和客户端逻辑完全一致依赖文件换成protobuf_server.cpp输出可执行文件server编译参数一模一样。clean:清理规则执行make clean删除client、server两个可执行文件。.PHONY: all clean声明all和clean是伪目标。 意思当前目录如果有文件名叫all或者clean也不会影响 make 执行这两个规则。
返回列表