TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。即面向流的通信是无消息保护边界的。
图解TCP的粘包和拆包
假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下四种情况:
1.服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包
2.服务端一次接受到了两个数据包,D1和D2粘合在一起,称之为TCP粘包
3.服务端分两次读取到了数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这称之为TCP拆包
4.服务端分两次读取到了数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余部分内容D1_2和完整的D2包。
特别要注意的是,如果TCP的接受滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种情况,即服务端分多次才能将D1和D2包完全接受,期间发生多次拆包。
粘包、拆包问题的解决方案:定义通信协议
目前业界主流的协议(protocol)方案可以归纳如下:
1 定长协议:假设我们规定每3个字节,表示一个有效报文。
2.特殊字符分隔符协议:在包尾部增加回车或者空格符等特殊字符进行分割 。
3.长度编码:将消息分为消息头和消息体,消息头中用一个int型数据(4字节),表示消息体长度的字段。在解析时,先读取内容长度Length,其值为实际消息体内容(Content)占用的字节数,之后必须读取到这么多字节的内容,才认为是一个完整的数据报文。
下面我用长度编码方式解决TCP的粘包、拆包问题
首先看一下不使用长度编码协议的时候会发生什么问题?
Server端测试代码
public class MyServer { public static void main(String[] args) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class). childHandler(new MyServerInitializer()); ChannelFuture channelFuture = serverBootstrap.bind(8899).sync(); channelFuture.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } } public class MyServerHandler extends SimpleChannelInboundHandler<ByteBuf> { private int count; @Override protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { byte[] buffer = new byte[msg.readableBytes()]; msg.readBytes(buffer); String message = new String(buffer, Charset.forName("utf-8")); System.out.println("服务端接收到的消息内容: " + message); System.out.println("服务端接收到的消息数量: " + (++this.count)); ByteBuf responseByteBuf = Unpooled.copiedBuffer(UUID.randomUUID().toString(), Charset.forName("utf-8")); ctx.writeAndFlush(responseByteBuf); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } } public class MyServerInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { System.out.println(this); ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new MyServerHandler()); } }
Client端测试代码
public class MyClient { public static void main(String[] args) throws Exception{ EventLoopGroup eventLoopGroup = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class). handler(new MyClientInitializer()); ChannelFuture channelFuture = bootstrap.connect("localhost", 8899).sync(); channelFuture.channel().closeFuture().sync(); } finally { eventLoopGroup.shutdownGracefully(); } } } public class MyClientHandler extends SimpleChannelInboundHandler<ByteBuf> { private int count; @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { for (int i = 0; i < 10; ++i) { ByteBuf buffer = Unpooled.copiedBuffer("sent from client ", Charset.forName("utf-8")); ctx.writeAndFlush(buffer); } } @Override protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { byte[] buffer = new byte[msg.readableBytes()]; msg.readBytes(buffer); String message = new String(buffer, Charset.forName("utf-8")); System.out.println("客户端接收到的消息内容: " + message); System.out.println("客户端接收到的消息数量: " + (++this.count)); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } } public class MyClientInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new MyClientHandler()); } }
启动服务端,然后
第一次启动客户端,服务端日志:
第二次启动客户端,服务端日志:
第三次启动客户端,服务端日志:
从结果中可以看出发生了粘包问题。
用长度编码方式解决TCP的粘包和拆包问题
自定义协议CustomProtocol,解码器MyDecoder,编码器MyEncoder
public class CustomProtocol { private int length; private byte[] content; public int getLength() { return length; } public void setLength(int length) { this.length = length; } public byte[] getContent() { return content; } public void setContent(byte[] content) { this.content = content; } } public class MyDecoder extends ReplayingDecoder<Void> { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { int length = in.readInt(); byte[] content = new byte[length]; in.readBytes(content); CustomProtocol personProtocol = new CustomProtocol(); personProtocol.setLength(length); personProtocol.setContent(content); out.add(personProtocol); } } public class MyEncoder extends MessageToByteEncoder<CustomProtocol> { @Override protected void encode(ChannelHandlerContext ctx, CustomProtocol msg, ByteBuf out) throws Exception { out.writeInt(msg.getLength()); out.writeBytes(msg.getContent()); } }
Server端测试代码
public class MyServer { public static void main(String[] args) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class). childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new MyDecoder()); pipeline.addLast(new MyEncoder()); pipeline.addLast(new MyClientHandler()); } }); ChannelFuture channelFuture = serverBootstrap.bind(8899).sync(); channelFuture.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } } public class MyServerHandler extends SimpleChannelInboundHandler<CustomProtocol> { private int count; @Override protected void channelRead0(ChannelHandlerContext ctx, CustomProtocol msg) throws Exception { int length = msg.getLength(); byte[] content = msg.getContent(); System.out.println("服务端接收到的数据:"); System.out.println("长度: " + length); System.out.println("内容:" + new String(content, Charset.forName("utf-8"))); System.out.println("服务端接收到的消息数量:" + (++this.count)); String responseMessage = UUID.randomUUID().toString(); int responseLength = responseMessage.getBytes("utf-8").length; byte[] responseContent = responseMessage.getBytes("utf-8"); CustomProtocol personProtocol = new CustomProtocol(); personProtocol.setLength(responseLength); personProtocol.setContent(responseContent); ctx.writeAndFlush(personProtocol); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } }
Client端测试代码
public class MyClient { public static void main(String[] args) throws Exception { EventLoopGroup eventLoopGroup = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class). handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new MyDecoder()); pipeline.addLast(new MyEncoder()); pipeline.addLast(new MyClientHandler()); } }); ChannelFuture channelFuture = bootstrap.connect("localhost", 8899).sync(); channelFuture.channel().closeFuture().sync(); } finally { eventLoopGroup.shutdownGracefully(); } } } public class MyClientHandler extends SimpleChannelInboundHandler<CustomProtocol> { private int count; @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { for (int i = 0; i < 10; ++i) { String messageToBeSent = "sent from client "; byte[] content = messageToBeSent.getBytes(Charset.forName("utf-8")); int length = messageToBeSent.getBytes(Charset.forName("utf-8")).length; CustomProtocol personProtocol = new CustomProtocol(); personProtocol.setLength(length); personProtocol.setContent(content); ctx.writeAndFlush(personProtocol); } } @Override protected void channelRead0(ChannelHandlerContext ctx, CustomProtocol msg) throws Exception { int length = msg.getLength(); byte[] content = msg.getContent(); System.out.println("客户端接收到的消息: "); System.out.println("长度: " + length); System.out.println("内容:" + new String(content, Charset.forName("utf-8"))); System.out.println("客户端接受到的消息数量:" + (++this.count)); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } }
测试结果:
结果分析:没有发生Tcp的粘包和拆包。