TCP是个“流”协议,所谓流,就是没有界限的一串数据。可以想想河里的流水,是连成一片的,其间并没有分界线。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。
有关TCP的详细讲解,可以点击关于三次握手与四次挥手你要知道这些和快速了解TCP的流量控制与拥塞控制
TCP粘包或拆包的原因
拆包和粘包的形式
第一种情况:接收端正常收到两个数据包,即没有发生拆包和粘包的现象,此种情况不在本文的讨论范围内。
发生拆包
第二种情况:接收端只收到一个数据包,由于TCP是不会出现丢包的,所以这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。
发生粘包
第三种情况:这种情况有两种表现形式,如下图。接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。这两种情况如果不加特殊处理,对于接收端同样是不好处理的。
发生拆包和粘包
发生拆包和粘包
粘包和拆包的解决办法
Netty中的代码示例
Netty封装了JDK的NIO,是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能服务器和客户端。一般开发中并不会用JDK原生NIO,原因如下:
所以,本文选择演示Netty的编解码代码。
在Netty中,我们定义MessageToByteEncoder<T>的继承类,重写其encode函数,来自定义编码器。
public class SocketEncoder extends MessageToByteEncoder<Packet> { @Override protected void encode(ChannelHandlerContext channelHandlerContext, NetPacket msg, ByteBuf byteBuf) throws Exception { byte body[] = msg.getBody(); int packetLen = body.length; // 先设置包长度,然后写入二进制数据 byteBuf.writeInt(packetLen); byteBuf.writeBytes(body); } }
在Netty中,我们定义ByteToMessageDecoder的继承类,重写其decode函数,用来自定义解码器。
public class SocketDecoder extends ByteToMessageDecoder { @Override void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception { int bufLen = byteBuf.readableBytes(); // 解决粘包问题(不够一个包头的长度) // 4字节是报文中使用了一个int表示了报文长度 if (bufLen < 4) { return; } // 标记一下当前的readIndex的位置 byteBuf.markReaderIndex(); int packetLength = byteBuf.readInt(); // 读到的消息体长度如果小于我们传送过来的消息长度,则resetReaderIndex。重置读索引,继续接收 if (byteBuf.readableBytes() < packetLength) { // 配合markReaderIndex使用的。把readIndex重置到mark的地方 byteBuf.resetReaderIndex(); return; } NetPacket netPacket = new NetPacket(); netPacket.setPacketLen(packetLength); // 传送过来数据的长度,满足我们的要求了 byte body[] = new byte[packetLength]; byteBuf.readBytes(body); netPacket.setBody(body); list.add(netPacket); } }
更多内容,欢迎关注微信公众号:全菜工程师小辉~