TCP粘包/半包问题
TCP是以流的方式来处理数据,一个完整的包可能会被TCP拆分成多个包进行发送,也可能把小的封装成一个大的数据包发送
TCP粘包/分包的原因
- 应用程序写入的字节大小大于套接字发送缓冲区的大小,会发生拆包现象* 应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包现象
进行MSS大小的TCP分段,当TCP报文长度-TCP头部长度 > MSS的时候将发生拆包
以太网帧的payload(净荷)大于MTU(1500字节)进行ip分片
TCP粘包/分包解决方法
主要有四种方法:
- 消息定长:
FixedLengthFrameDecoder
类 - 行分隔符类:
LineBasedFrameDecoder
类 - 定义分隔符类:
DelimiterBasedFrameDecoder
类 - 将消息分为消息头和消息体:
LengthFieldBasedFrameDecoder
类。分为有头部的拆包与粘包、长度字段在前且有头部的拆包与粘包、多扩展头部的拆包与粘包
底层Socket通信经常遇到这种基础打解包的需求,我写了一个针对该场景的通用信息交换平台,可以实现打解包的配置化,放在Github上
GitHub 地址:https://github.com/dragonflysun2019/GXPMaster
编解码器框架
什么是编解码器
每个网络应用程序都必须定义如何解析在两个节点之间来回传输的原始字节,以及如何将其和目标应用程序的数据格式做相互转换。这种转换逻辑由Codec
处理,Codec
由Encoder
和Decoder
组成,它们每种都可以将字节流从一种格式转换为另一种格式。那么它们的区别是什么呢?
如果将消息看作是对于特定的应用程序具有具体含义的结构化的字节序列—它的数据。那么Encoder
是将消息转换为适合于传输的格式(最有可能的就是字节流);而对应的Decoder
则是将网络字节流转换回应用程序的消息格式。因此,Encoder
操作出站数据,而Decoder
处理入站数据。我们前面所学的解决粘包半包的其实也是编解码器框架的一部分。
解码器
- 将字节解码为消息:
ByteToMessageDecoder
- 将一种消息类型解码为另一种:
MessageToMessageDecoder
因为解码器是负责将入站数据从一种格式转换到另一种格式的,所以
Netty 的解码器实现了
ChannelInboundHandler
什么时候会用到解码器呢?
很简单:每当需要为ChannelPipeline
中的下一个ChannelInboundHandler
转换入站数据时会用到。此外,得益于ChannelPipeline 的设计
,可以将多个解码器链接在一起,以实现任意复杂的转换逻辑
将字节解码为消息ByteToMessageDecoder
抽象类ByteToMessageDecoder
将字节解码为消息(或者另一个字节序列)是一项如此常见的任务,以至于Netty 为它提供了一个抽象的基类:ByteToMessageDecoder
。由于你不可能知道远程节点是否会一次性地发送一个完整的消息,所以这个类会对入站数据进行缓冲,直到它准备好处理
它最重要方法
decode(ChannelHandlerContext ctx,ByteBuf in,List
这是你必须实现的唯一抽象方法。decode()
方法被调用时将会传入一个包含了传入数据的ByteBuf,以及一个用来添加解码消息的List。对这个方法的调用将会重复进行,直到确定没有新的元素被添加到该List,或者该ByteBuf 中没有更多可读取的字节时为止。然后,如果该List 不为空,那么它的内容将会被传递给ChannelPipeline
中的下一个ChannelInboundHandler
将一种消息类型解码为另一种MessageToMessageDecoder
在两个消息格式之间进行转换(例如,从String->Integer)
decode(ChannelHandlerContext ctx,I msg,List
对于每个需要被解码为另一种格式的入站消息来说,该方法都将会被调用。解码消息随后会被传递给ChannelPipeline
中的下一个ChannelInboundHandler
MessageToMessageDecoder
TooLongFrameException
由于Netty 是一个异步框架,所以需要在字节可以解码之前在内存中缓冲它们(没有读取完之前是无法解码的)。因此,不能让解码器缓冲大量的数据以至于耗尽可用的内存。为了解除这个常见的顾虑,Netty 提供了TooLongFrameException
类,其将由解码器在帧超出指定的大小限制时抛出
为了避免这种情况,你可以设置一个最大字节数的阈值,如果超出该阈值,则会导致抛出一个TooLongFrameException
(随后会被ChannelHandler.exceptionCaught()
方法捕获)。然后,如何处理该异常则完全取决于该解码器的用户。某些协议(如HTTP)可能允许你返回一个特殊的响应。而在其他的情况下,唯一的选择可能就是关闭对应的连接
编码器
和解码器的功能正好相反。Netty 提供了一组类,用于帮助你编写具有以下功能的编码器:
- 将消息编码为字节:
MessageToByteEncoder
- 将消息编码为消息:
MessageToMessageEncoder
,T代表源数据的类型
将消息编码为字节MessageToByteEncoder
encode(ChannelHandlerContext ctx,I msg,ByteBuf out)
encode()
方法是你需要实现的唯一抽象方法。它被调用时将会传入要被该类编码为ByteBuf 的(类型为I 的)出站消息。该ByteBuf 随后将会被转发给ChannelPipeline
中的下一个ChannelOutboundHandler
将消息编码为消息MessageToMessageEncoder
encode(ChannelHandlerContext ctx,I msg,List
这是你需要实现的唯一方法。每个通过write()
方法写入的消息都将会被传递给encode()方法,以编码为一个或者多个出站消息。随后,这些出站消息将会被转发给ChannelPipeline
中的下一个ChannelOutboundHandler
编解码器类
我们一直将解码器和编码器作为单独的实体讨论,但是你有时将会发现在同一个类中管理入站和出站数据和消息的转换是很有用的。Netty 的抽象编解码器类正好用于这个目的,因为它们每个都将捆绑一个解码器/编码器对,以处理我们一直在学习的这两种类型的操作。这些类同时实现了ChannelInboundHandler 和ChannelOutboundHandler
接口。
为什么我们并没有一直优先于单独的解码器和编码器使用这些复合类呢?因为通过尽可能地将这两种功能分开,最大化了代码的可重用性和可扩展性,这是Netty 设计的一个基本原则。
相关的类:
- 抽象类ByteToMessageCodec
- 抽象类MessageToMessageCodec
- HttpServerCodec
- HttpClientCodec