TCP协议是面向流的协议,是流式的,没有业务上的分段,只会根据当前套接字缓冲区的状况进行拆包或者粘包:java
发送端的字节流都会先传入缓冲区,再经过网络传入到接收端的缓冲区中,最终由接收端获取。缓存
由于TCP会根据缓冲区的实际状况进行包的划分,在业务上认为,有的包被拆分红多个包进行发送,也可能多个晓小的包封装成一个大的包发送,这就是TCP的粘包或者拆包。服务器
假设客户端分别发送了两个数据包D1和D2给服务端,因为服务端一次读取到字节数是不肯定的,故可能存在如下几种状况:网络
当TCP缓存再小一点的话,会把D1和D2分别拆成多个包发送。socket
由于TCP只负责数据发送,并不处理业务上的数据,因此只能在上层应用协议栈解决,目前的解决方案概括:ide
Netty提供了多种默认的编码器解决粘包和拆包:oop
基于回车换行符的解码器,当遇到"n"或者 "rn"结束符时,分为一组。支持携带结束符或者不带结束符两种编码方式,也支持配置单行的最大长度。
LineBasedFrameDecoder与StringDecoder搭配时,至关于按行切换的文本解析器,用来支持TCP的粘包和拆包。
使用例子:编码
private void start() throws Exception { //建立 EventLoopGroup NioEventLoopGroup group = new NioEventLoopGroup(); NioEventLoopGroup work = new NioEventLoopGroup(); try { //建立 ServerBootstrap ServerBootstrap b = new ServerBootstrap(); b.group(group, work) //指定使用 NIO 的传输 Channel .channel(NioServerSocketChannel.class) //设置 socket 地址使用所选的端口 .localAddress(new InetSocketAddress(port)) //添加 EchoServerHandler 到 Channel 的 ChannelPipeline .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) { ChannelPipeline p = ch.pipeline(); p.addLast(new LineBasedFrameDecoder(1024)); p.addLast(new StringDecoder()); p.addLast(new StringEncoder()); p.addLast(new EchoServerHandler()); } }); //绑定的服务器;sync 等待服务器关闭 ChannelFuture f = b.bind().sync(); System.out.println(EchoServer.class.getName() + " started and listen on " + f.channel().localAddress()); //关闭 channel 和 块,直到它被关闭 f.channel().closeFuture().sync(); } finally { //关机的 EventLoopGroup,释放全部资源。 group.shutdownGracefully().sync(); } }
注意ChannelPipeline 中ChannelHandler的顺序,spa
分隔符解码器,能够指定消息结束的分隔符,它能够自动完成以分隔符做为码流结束标识的消息的解码。回车换行解码器其实是一种特殊的DelimiterBasedFrameDecoder解码器。
使用例子(后面的代码只贴ChannelPipeline部分):.net
ChannelPipeline p = ch.pipeline(); p.addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer("制定的分隔符".getBytes()))); p.addLast(new StringDecoder()); p.addLast(new StringEncoder()); p.addLast(new EchoServerHandler());
固定长度解码器,它可以按照指定的长度对消息进行自动解码,当制定的长度过大,消息太短时会有资源浪费,可是使用起来简单。
ChannelPipeline p = ch.pipeline(); p.addLast(new FixedLengthFrameDecoder(1 << 5)); p.addLast(new StringDecoder()); p.addLast(new StringEncoder()); p.addLast(new EchoServerHandler());
通用解码器,通常协议头中带有长度字段,经过使用LengthFieldBasedFrameDecoder传入特定的参数,来解决拆包粘包。
io.netty.handler.codec.LengthFieldBasedFrameDecoder的实例化:
/** * Creates a new instance. * * @param maxFrameLength 最大帧长度。也就是能够接收的数据的最大长度。若是超过,这次数据会被丢弃。 * @param lengthFieldOffset 长度域偏移。就是说数据开始的几个字节可能不是表示数据长度,须要后移几个字节才是长度域。 * @param lengthFieldLength 长度域字节数。用几个字节来表示数据长度。 * @param lengthAdjustment 数据长度修正。由于长度域指定的长度能够是header+body的整个长度,也能够只是body的长度。若是表示header+body的整个长度,那么咱们须要修正数据长度。 * @param initialBytesToStrip 跳过的字节数。若是你须要接收header+body的全部数据,此值就是0,若是你只想接收body数据,那么须要跳过header所占用的字节数。 * @param failFast 若是为true,则在解码器注意到帧的长度将超过maxFrameLength时当即抛出TooLongFrameException,而不论是否已读取整个帧。 * 若是为false,则在读取了超过maxFrameLength的整个帧以后引起TooLongFrameException。 */ public LengthFieldBasedFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip, boolean failFast) { //略 }
最大帧长度。也就是能够接收的数据的最大长度。若是超过,这次数据会被丢弃。
长度域偏移。就是说数据开始的几个字节可能不是表示数据长度,须要后移几个字节才是长度域。
长度域字节数。用几个字节来表示数据长度。
数据长度修正。由于长度域指定的长度能够是header+body的整个长度,也能够只是body的长度。若是表示header+body的整个长度,那么咱们须要修正数据长度。
跳过的字节数。若是你须要接收header+body的全部数据,此值就是0,若是你只想接收body数据,那么须要跳过header所占用的字节数。
若是为true,则在解码器注意到帧的长度将超过maxFrameLength时当即抛出TooLongFrameException,而不论是否已读取整个帧。
若是为false,则在读取了超过maxFrameLength的整个帧以后引起TooLongFrameException。
下面经过Netty源码中LengthFieldBasedFrameDecoder的注释几个例子看一下参数的使用:
本例中的length字段的值是12 (0x0C),它表示“HELLO, WORLD”的长度。默认状况下,解码器假定长度字段表示长度字段后面的字节数。
由于咱们能够经过调用readableBytes()来得到内容的长度,因此可能但愿经过指定initialbystrip来删除长度字段。在本例中,咱们指定2(与length字段的长度相同)来去掉前两个字节。
在大多数状况下,length字段仅表示消息体的长度,如前面的示例所示。可是,在一些协议中,长度字段表示整个消息的长度,包括消息头。在这种状况下,咱们指定一个非零长度调整。由于这个示例消息中的长度值老是比主体长度大2,因此咱们指定-2做为补偿的长度调整。
下面的消息是第一个示例的简单变体。一个额外的头值被预先写入消息中。长度调整再次为零,由于译码器在计算帧长时老是考虑到预写数据的长度。
这是一个高级示例,展现了在长度字段和消息正文之间有一个额外头的状况。您必须指定一个正的长度调整,以便解码器将额外的标头计数到帧长度计算中。
这是上述全部示例的组合。在长度字段以前有预写的header,在长度字段以后有额外的header。预先设置的header会影响lengthFieldOffset,而额外的leader会影响lengthAdjustment。咱们还指定了一个非零initialBytesToStrip来从帧中去除长度字段和预约的header。若是不想去掉预写的header,能够为initialBytesToSkip指定0。
让咱们对前面的示例进行另外一个修改。与前一个示例的唯一区别是,length字段表示整个消息的长度,而不是消息正文的长度,就像第三个示例同样。咱们必须把HDR1的长度和长度计算进长度调整里。请注意,咱们不须要考虑HDR2的长度,由于length字段已经包含了整个头的长度。