前面的博文咱们讨论了编解码, 想必你已经知道编解码过程就是字节数组到应用消息的转换, 接下来想跟你聊聊编解码场景需要考虑哪些问题, 以及常用的处理方式。就编解码中的信息而言, 一部分肯定是应用处理需要的信息, 另一部分则是与具体传输协议有关的信息。因此, 本篇分业务编解码和传输编解码来讨论。
业务编解码解决业务message到byte的转换, 在Java中就是序列化和反序列化。一般从编解码的效率, 资源占用和阅读友好性考虑。
项目 | 说明 | 场景特点 |
---|---|---|
编码效率 | 编码前后的空间消耗比 | 窄带宽传输更多数据, 比如蓝牙应用 |
解码效率 | 收到数据后,解码时间消耗 | 接收端解码能力弱, 比如移动应用 |
资源占用 | 编解码过程中CPU和内存占用 | 接收或发送端CPU和内存较小, 比如嵌入式应用 |
升级兼容性 | 协议升级时向前兼容的难易程度 | 与协议变化频度有关 |
阅读友好性 | 方便排查问题 | 面向开发者,一般不直接考虑, 通过开发配套工具解决 |
想必你也知道TCP是面向数据流的传输, 数据流本身没有类似分段或者局部结束的概念, 所以在具体实现上以segment的承载能力为界限, 分割应用层数据。在MTU=1500个字节的情况下, TCP segment中一次至多有1460个字节可以填充应用消息。显然, 这种分割标准与应用层完全不同, 标准不同自然就会带来潜在的问题。
如果一条应用层消息超过1460 byte则需要放在2个segment, 此时我们说TCP协议对message做了分包处理。个人认为, 分包的本质是整个网络链路上各跳的处理能力不同, 处理能力最差的节点可能仅能够缓存有限个MTU。
如果一条应用层消息只有10 byte, 那么理论上1460 byte可以填充146个, 然后通过一个TCP segment发出, 此时我们说TCP协议对message做了粘包处理。协议这么实现是为了充分利用每一个segment的承载能力。
无论分包还是粘包, 应用层的核心问题是因为不知道消息本身的分割方式, 所以无法处理收到segment。解决方法有三种, 定长消息、变长消息(将消息长度编码在消息中)和固定分隔符。
想必你已经知道UDP是面向数据报的, 所有的数据以数据包为单位传输, 在协议层面数据包之间没有顺序关系。如果要实现类似TCP协议中的可靠性, 则需要在应用层解决如下问题:
在UDP协议层面, 数据包可以顺序发送, 但由于IP层会对路由过程做动态优化, 可能存在后发先至的情况。为解决该问题, 一方面应用需要在消息中增加顺序字段(想想TCP中的seq), 另一方面应用接收端需要设定接收窗口而后在窗口内按照顺序字段做重排。
有了顺序字段, 通信双方就可以做收发的的确认。对于超时未确认的数据报, 应用也可以自定义重传策略。比如流媒体领域通常允许一定的丢包率, 特定场景下不做重传也是可以的。
整个通信链路是动态波动的, 带宽紧张时发送更多的数据包毫无意义, 同样带宽充裕时任然无所作为实在浪费资源, 而想要有所作为就得能感知波动这个是带宽探测。带宽探测包括链路带宽探测和对端的带宽探测(其实是处理能力探测)。有了探测结果, 应用就可以做比较, 而后调整收发速度。该过程可以参考TCP实现, 并结合应用场景做定制。
笔者对UDP的传输做的比较少, 只能提供一些思路, 还请读者见谅。
编解码方式 | 编码效率 | 解码效率 | 资源占用 | 升级兼容性 | 阅读友好性 | 备注 |
---|---|---|---|---|---|---|
JSON/XML | 1 | 1 | 5 | 5 | 5 | 调试,小message场景 |
Protobuf | 5 | 4 | 3 | 5 | 0 | 关注编码效率 |
JDK原生序列化 | 2 | 2 | 3 | 5 | 0 | 参考对象, 几乎不用 |
Flatbuffer | 4 | 5 | 3 | 5 | 0 | 关注解码效率 |
Netty中通过实现MessageToByteEncoder和MessageToByeDecoder可以实现自定义编解码器, 其中考虑到解码是整个pipeline的第一环, 而编码则是最后一环, 所以在配置Pipeline应用自定义编解码器器时, 应格外注意顺序问题。
本文从业务编解码和传输编解码两个方面讨论网络应用中的编解码问题。其中应用编解码层面本文讨论了评价方式和场景特点, 在传输编解码层面讨论了典型问题和解决方案。最后, 本文分享了Netty中对编解码的支持和以及部分个人使用经验。