TCP粘包/分包问题的由来
因为TCP是以流的方式来处理数据,一个完整的包可能会被TCP拆分成多个包进行发送,也可能把小的封装成一个大的数据包发送。
这样说可能比较抽象,下面举例来说明TCP拆包/粘包问题!
- 图解:如果客户端分别发送两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,可能会出现四种情况。
(1)服务端分别读取到D1和D2,没有产生粘包和拆包的情况,如下图:
(2)服务端一次接收到二个数据包,D1和D2粘合在一起,被成为TCP粘包。如下图:
(3)服务端分二次读取到了二个数据包,第一次读取到了完整的D1包和D2包的一部分,第二次读取到了D2包的剩余部分,这被成为TCP拆包(D2拆包),如下图:
(4)服务器还是分二次读取到了二个数据包,但第一次是读取到了D1包的部分内容 ,第二次读取到了D1包剩余部分和完整的D2包,这被成为TCP拆包(D1拆包),如下图:
- 代码示例:
服务端代码
public class Server4 {
public static void main(String[] args) throws SigarException {
//boss线程监听端口,worker线程负责数据读写
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
try{
//辅助启动类
ServerBootstrap bootstrap = new ServerBootstrap();
//设置线程池
bootstrap.group(boss,worker);
//设置socket工厂
bootstrap.channel(NioServerSocketChannel.class);
//设置管道工厂
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//获取管道
ChannelPipeline pipeline = socketChannel.pipeline();
//处理类
pipeline.addLast(new ServerHandler4());
}
});
//绑定端口
ChannelFuture future = bootstrap.bind(8866).sync();
System.out.println("server start ...... ");
//等待服务端监听端口关闭
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//优雅退出,释放线程池资源
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
class ServerHandler4 extends SimpleChannelInboundHandler {
//用于记录次数
private int count = 0;
//读取客户端发送的数据
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf)msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String c = new String(req,"UTF-8").substring(0, req.length - System.getProperty("line.separator").length());
count++;
System.out.println("RESPONSE--------"+c+";"+" @ "+count);
}
//新客户端接入
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("channelActive");
}
//客户端断开
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("channelInactive");
}
//异常
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//关闭通道
ctx.channel().close();
//打印异常
cause.printStackTrace();
}
}
客户端代码:
public class Client4 {
public static void main(String[] args) {
//worker负责读写数据
EventLoopGroup worker = new NioEventLoopGroup();
try {
//辅助启动类
Bootstrap bootstrap = new Bootstrap();
//设置线程池
bootstrap.group(worker);
//设置socket工厂
bootstrap.channel(NioSocketChannel.class);
//设置管道
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//获取管道
ChannelPipeline pipeline = socketChannel.pipeline();
//处理类
pipeline.addLast(new ClientHandler4());
}
});
//发起异步连接操作
ChannelFuture futrue = bootstrap.connect(new InetSocketAddress("127.0.0.1",8866)).sync();
//等待客户端链路关闭
futrue.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//优雅的退出,释放NIO线程组
worker.shutdownGracefully();
}
}
}
class ClientHandler4 extends SimpleChannelInboundHandler<String> {
//接受服务端发来的消息
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println("server response : "+msg);
}
//与服务器建立连接
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//给服务器发消息
ByteBuf message = null;
byte[] req = " I am client ".getBytes();
//发送50次消息
for (int i = 0; i < 50; i++) {
message = Unpooled.buffer(req.length);
message.writeBytes(req);
ctx.channel().writeAndFlush(message);
}
}
//与服务器断开连接
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("channelInactive");
}
//异常
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//关闭管道
ctx.channel().close();
//打印异常信息
cause.printStackTrace();
}
}
服务端运行结果:
分析:通过代码可知,客户端向服务端发送了50条消息,正常结果是服务端应该接收了50条消息,但服务端的运行结果显示只收到了两条客户端的消息,由图知,第一条消息包含37 个I am client,而第二条消息包含13个I am client。这明显是出现了TCP粘包问题。
出现TCP粘包/分包的原因
1.应用程序写入的字节大小大于套接字发送缓冲区的大小,会发生拆包现象,而应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包现象;
2.进行mss(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包
3.以太网帧的payload(净荷)大于MTU(1500字节)进行ip分片。
图解:
TCP粘包/分包的解决方法
1.消息定长
例如:每个报文的大小固定为200个字节,如果不够,空位补空格
对应Netty中的定长类 :FixedLengthFrameDecoder
2.在包尾都增加特殊字符进行分割
例如:加回车、加换行、FTP协议等
对应Netty中的类
- 自定义分隔符类 :DelimiterBasedFrameDecoder
- 行分隔符类:LineBasedFrameDecoder
3.将消息分为消息头和消息体
例:在消息头中包含表示消息总长度的字段,然后进行业务逻辑的处理。
对应Netty中的基于消息头指定消息长度类:LengthFieldBasedFrameDecoder
4.更复杂的应用层协议
解决TCP粘包/分包问题的实例请阅读我的下一篇博文:解决TCP粘包/分包的实例
转载自https://blog.csdn.net/baiye_xing/article/details/73188847