Netty系列文章


目录


1. TCP粘包、拆包

1.1 TCP粘包/拆包

TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分。所以业务上,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送。

  • 问题说明
    在这里插入图片描述
    还有第5种可能:如果此时服务端TCP接收滑窗非常小,而数据包大,可能会发生多次拆包。

  • 发生的原因

    • 应用程序write写入的字节大小大于套接口发送缓冲区大小
    • 进行MSS大小的TCP分段
    • 以太网帧的payload大于MTU进行IP分片
      在这里插入图片描述 - 解决策略 - 由于底层的TCP无法理解上层的业务数据,所以底层是无法保证数据包不被拆分和重组的,只能通过上层的应用协议栈设计来解决:
      • 消息定长,如果不够,空位补空格;
      • 在包尾增加回车换行符进行分割,比如FTP协议;
      • 将消息分为消息头和消息体,消息头中包含表示消息总长度的字段;
      • 更复杂的应用层协议。

1.2 利用LineBasedFrameDecoder解决TCP粘包问题

  • 为了解决TCP粘包/拆包导致的半包读写问题,Netty默认提供了多种编解码器用于处理半包。
  • 在TimeServer和TimeClient的例子中:通过使用LineBasedFrameDecoderStringDecoder成功解决了TCP粘包导致的读半包问题。只要将支持半包解码的Handler添加到ChannelPipeline中即可。
  • LineBasedFrameDecoder
    • 是以换行符为结束标志的解码器。
    • 工作原理:它依次遍历ByteBuf中的可读字节,判断看是否有"\n"或者"\r\n",如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。
    • 支持携带结束符或不携带结束符两种解码方式;支持配置单行最大长度。
  • StringDecoder
    • 将接收到的对象转换成字符串,然后继续调用后面的Handler。
  • LineBasedFrameDecoder+StringDecoder组合就是按行切换的文本解码器。它被设计用来支持TCP的粘包和拆包。

2. 分隔符和定长解码器的应用

TCP以流的方式进行数据传输,上层的应用协议为了对消息区分,往往采用以下四种方式(第4章有提到):

  • 消息定长
  • 将回车换行符作为消息结束符
  • 将特殊的分隔符作为消息的结束标志
  • 消息头中定义消息总长度的字段
    Netty对这4种应用做了统一的抽象,提供了4种解码器来解决对应问题。

2.1 DelimiterBasedFrameDecoder应用开发

使用DelimiterBasedFrameDecoder,可以自动完成以分隔符做结束标志的消息的解码。以Echo服务为例:

  • EchoServer服务端
    • EchoServer
      在这里插入图片描述
      • 首先创建分隔符缓冲对象ByteBuf,本例使用"$_ "作为分隔符。
      • 创建DelimiterBasedFrameDecoder对象并将其加入到ChannelPipeline中。DelimiterBasedFrameDecoder有多个构造方法,这里传递两个参数:
        1. 1024表示单条消息的最大长度
        2. 分隔符缓冲对象
    • EchoServerHandler
      • 由于DelimiterBasedFrameDecoder自动对请求消息进行了解码,后续的ChannelHandler接收到的msg对象就是个完整的信息包;
      • 第二个ChannelHandlerStringDecoder,它将ByteBuf解码成字符串对象;
      • 第三个EchoServerHandler接收到的msg消息就是解码后的字符串对象。
        在这里插入图片描述
  • EchoClient客户端
    与服务端类似,分别将DelimiterBasedFrameDecoderStringDecoder添加到客户ChannelPipeline中,最后添加客户端I/O事件处理类EchoClientHandler

2.2 FixedLengthFrameDecoder应用开发

  • 是固定长度解码器,它能够按照指定长度对消息进行自动解码,开发者不需要考虑TCP粘包/拆包问题。
  • 在Echo服务端ChannelPipeline中新增FixedLengthFrameDecoder,长度设置为20,然后再依次增加字符串解码器和EchoServerHandler。
    在这里插入图片描述
  • 无论一次接收到多少数据报,它都会按照构造函数中设置的固定长度进行解码,如果是半包消息,它会缓存半包消息并等待下个包到达后进行拼包,直到读取到一个完整的包。

2.3 总结

应用DelimiterBasedFrameDecoderFixedLengthFrameDecoder进行开发非常简单,绝大多数情况下,只要DelimiterBasedFrameDecoderFixedLengthFrameDecoder添加到对应ChannelPipeline的起始位即可。


3. 编解码技术

  • 基于Java提供的对象输入/输出流ObjectInputStreamObjectOutputStream可以直接把Java对象作为可存储的字节数组写入文件,也可以传输到网络上。基于JDK默认的序列化机制可以避免操作底层的字节数组,提高开发效率。
  • Java序列化的目的主要有两个:
    • 网络传输
    • 对象持久化
  • Java对象编解码技术:当进行远程跨进程服务调用时,需要把被传输的Java对象编码为字节数组或ByteBuffer对象。而当远程服务读取到ByteBuffer对象或者字节数组时,需要将其解码为发送时的Java对象。
  • Java序列化:仅仅是Java编解码技术的一种,由于它的种种缺陷,衍生出了多种编解码技术和框架。

3.1 Java序列化的缺点

Java序列化不需要添加额外的类库,只需实现java.io.Serializable并生称序列ID即可,但在远程服务调用(RPC)时,很少直接使用Java序列化进行消息的编解码和传输。它的缺点如下:

  1. 无法跨语言
    • 对于跨进程的服务调用,服务提供者可能会使用C++或者其他语言开发,需要交互时Java序列化会难以胜任。对于Java序列化后的字节数组,别的语言无法进行反序列化。
  2. 序列化后的码流太大
    • 经过测试,采用JDK序列化机制编码后的二进制数组大小是二进制编码的5.29倍。编码后的字节数组越大,存储时越占空间,存储的硬件成本越高,网络传输时更占带宽,系统吞吐量降低。
  3. 序列化性能太低
    • 只有二进制编码的6.17%左右。
  • 不使用JDK提供的默认序列化框架,业界有其他很多优秀的编解码框架,它们在克服了这些缺点的基础上还增加了很多亮点。

3.2 业界主流的编解码框架

3.2.1 Google的Protobuf

  • 全称Google Protocol Buffers,它由谷歌开源而来。它用.proto文件描述数据结构,通过代码生成工具可以生成对应数据结构的POJO对象和Protobuf相关的方法和属性。
  • 特点:
    • 结构化数据存储格式(XML,JSON等);
    • 高效的编解码性能;
    • 语言无关、平台无关、扩展性好;
    • 官方支持Java、C++和Python三种语言。
  • 为什么不用XML?——XML解析时间开销和为可读性而牺牲的空间开销都很大,不适合做高性能的通信协议。
  • Protobuf编解码性能远远高于其他几种序列化框架的序列化和反序列化。

3.2.2 Facebook的Thrift

  • Facebook当时创造它是为了解决其各系统间大数据量的传输通信、以及系统之间语言环境不同需要跨平台的特性。因此Thrift可以支持多种程序语言(如C++, C#, Cocoa, Erlang, Haskell, Java, Ocami, Perl, PHP, Python, Ruby和Smalltalk)。
  • Thrift可以作为高性能的通信中间件使用,适用于静态的数据交换,适用于搭建大型数据交换及存储的通用工具。
  • Thrift支持三种比较典型的编解码方式:
    • 通用的二进制编解码
    • 压缩二进制编解码
    • 优化的可选字段压缩编解码

3.2.3 JBoss Marshalling

是一个Java对象的序列化API包,修正了JDK自带的序列化包的很多问题,但又保持跟java.io.Serializable接口的兼容。相比于前面两种,它更多是在JBoss内部使用,应用范围有限。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐