在开发分布式程序时,我们需要定义传输用的消息格式。无论是使用json还是protobuf,我们都需要对消息的收发做一些必要的处理工作。这篇文章就来谈一下这些工作。
编解码器Codec
Codec 是 encoder 和 decoder 的缩写,这是一个到软硬件都在使用的术语,这里我借指“把网络数据和业务消息之间互相转换”的代码。
在最简单的网络编程中,没有消息 message 只有字节流数据,这时候实际上是用不到 codec 的。比如我们前面介绍过的echo server,它只需要把收到的数据原封不动地发送回去,它不必关心消息的边界(也没有“消息”的概念),收多少就发多少,这种情况下它干脆直接使用 Buffer,取到数据再交给 TcpConnection 发送回去,见下图。
而一般的网络服务程序通常会以消息为单位来通信,每条消息有明确的长度与界限。程序每次收到一个完整的消息的时候才开始处理,发送的时候也是把一个完整的消息交给网络库。
codec 的基本功能之一是做 TCP 分包:确定每条消息的长度,为消息划分界限。在 non-blocking 网络编程中,codec 几乎是必不可少的。如果只收到了半条消息,那么不会触发消息回调,数据会停留在 Buffer 里(数据已经读到 Buffer 中了),等待收到一个完整的消息再通知处理函数。
对于长连接的 TCP 服务,分包一般有四种方法:
- 消息长度固定,比如采用固定的 16 字节消息;
- 在每条消息的头部加一个长度字段,比如Boost.Asio的例子 asio chat,它的一条聊天记录就是一条消息,它设计一个简单的消息格式,即在聊天记录前面加上 4 字节的 length header;
- 使用特殊的字符或字符串作为消息的边界,例如 HTTP 协议的 headers 以 “/r/n” 为字段的分隔符;
- 利用消息本身的格式来分包,例如 XML 格式的消息中 … 的配对,或者 JSON 格式中的 { … } 的配对。解析这种消息格式通常会用到状态机。
codec 是一层间接性,它位于 TcpConnection 和 ChatServer 之间,拦截处理收到的数据,在收到完整的消息之后再调用 ChatServer 对应的处理函数;在发送数据时,则将消息进行编码后再发送。这正是“编解码器”名字的由来。
Protobuf 的处理与此非常类似,只不过消息类型从 std::string 变成了 protobuf::Message。对于只接收处理 Query 消息的 QueryServer 来说,用 ProtobufCodec 非常方便,收到 protobuf::Message 之后 down cast 成 Query 来用就行。但如果要接收处理不止一种消息,ProtobufCodec 恐怕还不能单独完成工作。
分发器Dispatcher
前面提到,在使用 TCP 长连接,且在一个连接上传递不止一种 protobuf 消息(比方同时发 Heartbeat 和Request/Response)的情况下,客户代码需要对收到的消息按类型做分发。
比方说,收到 Logon 消息就交给 QueryServer::onLogon() 去处理,收到 Query 消息就交给 QueryServer::onQuery() 去处理。这个消息分派机制可以做得稍微有点通用性,让所有 muduo+protobuf 程序收益,而且不增加复杂性。
换句话说,又是一层间接性,ProtobufCodec 拦截了 TcpConnection 的数据,把它转换为 Message,ProtobufDispatcher 拦截了 ProtobufCodec 的 callback,按消息具体类型把它分派给多个 callbacks。
总结
ProtobufCodec 和 ProtobufDispatcher 把每个直接收发 protobuf Message 的网络程序都会用到的功能提炼出来做成了公用的 utility,这样以后新写 protobuf 网络程序就不必为打包分包和消息分发劳神了。
它俩以库的形式存在,是两个可以拿来就当 data member 用的 class,它们没有基类,也没有用到虚函数或者别的什么面向对象特征,不侵入用户代码。如果不这么做,那将来每个 protobuf 网络程序都要自己重新实现类似的功能,徒增负担。
附录:一种可能的实现方式