TCP粘包/拆包问题
- 什么是粘包/拆包问题?
客户端发送了两笔请求,服务端确只收到了包含两笔请求的一笔请求,举例:
请求
1
2
收到
1 2
或
请求
1 2
收到
1
2
TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分
多个小的包被封装成一个大的数据包进行发送(粘包问题)
一个数据包被拆分成多个包进行发送(拆包问题)
- 业界主流的解决方案
1、消息定长,不够用空格补齐
2、包尾增加回车换行符分割
3、将消息拆分成消息头和消息体,消息头包含消息的总长度(或消息体的长度)。
4、其它更复杂的应用层协议
- 粘包问题产生演示
Server
package org.zln.netty.five.timer; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; import io.netty.util.ReferenceCountUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.UnsupportedEncodingException; import java.text.SimpleDateFormat; import java.util.Date; /** * Handler主要用于对网络事件进行读写操作,是真正的业务类 * 通常只需要关注 channelRead 和 exceptionCaught 方法 * Created by sherry on 16/11/5. */ public class TimerServerHandler extends ChannelHandlerAdapter { /** * 日志 */ private Logger logger = LoggerFactory.getLogger(TimerServerHandler.class); private static int count = 0; @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //ByteBuf,类似于NIO中的ByteBuffer,但是更强大 ByteBuf reqBuf = (ByteBuf) msg; //获取请求字符串 String req = getReq(reqBuf); logger.debug("From:" + ctx.channel().remoteAddress()); logger.debug("服务端收到:" + req); String timeNow = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS").format(new Date()); String resStr = "这是当前收到的第 " + (count++) + " 笔请求.当前时间:" + timeNow; //获取发送给客户端的数据 ByteBuf resBuf = getRes(resStr); logger.debug("服务端应答数据:\n" + resStr); ctx.write(resBuf); // //丢弃 // logger.debug("丢弃"); // ReferenceCountUtil.release(msg); } /** * 字符串转化为缓冲区数据 * * @param resStr * @return */ private ByteBuf getRes(String resStr) { try { ByteBuf byteBuf = Unpooled.copiedBuffer(resStr.getBytes("UTF-8")); return byteBuf; } catch (UnsupportedEncodingException e) { logger.error(e.getMessage(), e); return null; } } /** * 缓冲区数据转化为字符串 * * @param buf * @return */ private String getReq(ByteBuf buf) { byte[] con = new byte[buf.readableBytes()]; //将ByteByf信息写出到字节数组 buf.readBytes(con); try { return new String(con, "UTF-8"); } catch (UnsupportedEncodingException e) { logger.error(e.getMessage(), e); return null; } } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { //将消息发送队列中的消息写入到SocketChannel中发送给对方 logger.debug("channelReadComplete"); ctx.flush(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { //发生异常时,关闭 ChannelHandlerContext,释放ChannelHandlerContext 相关的句柄等资源 logger.error("exceptionCaught"); ctx.close(); } }
Client
package org.zln.netty.five.timer; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.UnsupportedEncodingException; /** * Created by sherry on 16/11/5. */ public class TimerClientHandler extends ChannelHandlerAdapter { /** * 日志 */ private Logger logger = LoggerFactory.getLogger(TimerClientHandler.class); private static int count = 0; @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { logger.debug("客户端连接上了服务端"); //发送请求 ByteBuf reqBuf = null; for (int i = 0; i < 100; i++) { reqBuf = getReq("GET TIME"); ctx.writeAndFlush(reqBuf); } } /** * 将字符串包装成ByteBuf * @param s * @return */ private ByteBuf getReq(String s) throws UnsupportedEncodingException { byte[] data = s.getBytes("UTF-8"); ByteBuf reqBuf = Unpooled.buffer(data.length); reqBuf.writeBytes(data); return reqBuf; } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf byteBuf = (ByteBuf) msg; String resStr = getRes(byteBuf); logger.debug("客户端收到:"+resStr); logger.debug("这是收到的第 "+(count++)+" 笔响应"); } private String getRes(ByteBuf buf) { byte[] con = new byte[buf.readableBytes()]; buf.readBytes(con); try { return new String(con, "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); return null; } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } }
日志结果
服务端
20:10:01.591 [nioEventLoopGroup-1-0] DEBUG org.zln.netty.five.timer.TimerServerHandler - From:/127.0.0.1:58358 20:10:01.591 [nioEventLoopGroup-1-0] DEBUG org.zln.netty.five.timer.TimerServerHandler - 服务端收到:GET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIMEGET TIME 20:10:01.600 [nioEventLoopGroup-1-0] DEBUG org.zln.netty.five.timer.TimerServerHandler - 服务端应答数据: 这是当前收到的第 0 笔请求.当前时间:2016-11-06 20:10:01 593 20:10:01.602 [nioEventLoopGroup-1-0] DEBUG org.zln.netty.five.timer.TimerServerHandler - channelReadComplete
客户端
20:10:01.617 [nioEventLoopGroup-0-1] DEBUG org.zln.netty.five.timer.TimerClientHandler - 客户端收到:这是当前收到的第 0 笔请求.当前时间:2016-11-06 20:10:01 593 20:10:01.618 [nioEventLoopGroup-0-1] DEBUG org.zln.netty.five.timer.TimerClientHandler - 这是收到的第 0 笔响应
从中我们可以看到,客户端明明发送了100次,而服务端只收到了一次,但是这一次中包含了客户端100次发送的数据。这就产生了粘包的问题
由此也不难推断,如果客户端一次性发送的数据很多,就会产生拆包的问题