什么是TCP粘包/拆包
TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP是基于字节流传输的,就像河流一样,数据“流”式传输,数据中间没有分界。
TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以一个完整的数据包可能会拆分成多个包传输,或者多个数据包可能会合并成一个包传输,这就是所谓的TCP的粘包/拆包问题。如下图所示:
正常业务逻辑上,Client需要向Server发送D1和D2两个数据包(如上图第一种情况);但实际在TCP传输的过程中,可能会将D1和D2合并为一个数据包发送(如上图第二种情况);也有可能D2被拆分成两个数据包,其中一个跟D1合并成了一个数据包(如上图第三种情况);又或者D1被拆分成两个数据包,其中一个跟D2合并成了一个数据包(如上图第四种情况)。
还有一种可能,当服务端接收滑窗非常小,而D1、D2数据又很大的时候,可能会分多次才能将D1、D2两个数据接收完全。
TCP粘包/拆包发送的原因
TCP发送粘包/拆包的原因主要有以下几个:
(1)应用程序write写入的字节大小大于套接口发送缓冲区大小。
(2)进行MSS(最大报文段长度)大小的TCP分段。当TCP报文段的长度大于MSS时,要进行分段传输。
(3)以太网帧的payload大于MTU(最大传输单元)进行IP分片。一个IP数据报在以太网中 传输,如果它的长度大于该MTU值,就要进行分片传输,使得每片数据报的长度小于MTU。
使用Netty未考虑TCP粘包的示例
仍然还是用之前的客户端向服务端请求时间的案例来改造。之前的示例没有出现粘包/拆包问题是因为客户端与服务器之间只是一问一答,并没有太大的流量压力。这次为了呈现问题,我们将服务器端增加一个计数,每收到一个TCP数据包计数加一;在客户端上,循环发送100条请求,每接收到一条服务端的回复也计数加一。
服务端NettyServerTest类:
package com.test.netty;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class NettyServerTest {
public void bind(int port) throws InterruptedException {
// NioEventLoopGroup 是用来处理I/O操作的多线程事件循环器
// 第一个经常被叫做‘boss’,用来接收进来的连接
EventLoopGroup bossGroup = new NioEventLoopGroup();
// 第二个经常被叫做‘worker’,用来处理已经被接收的连接,一旦‘boss’接收到连接,就会把连接信息注册到‘worker’上
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// ServerBootstrap 是一个启动 NIO 服务的辅助启动类
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
// 设置 socket 的参数选项
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new childChannelHandler());
// 绑定端口,同步等待成功
ChannelFuture f = b.bind(port).sync();
// 等待服务端监听端口关闭
f.channel().closeFuture().sync();
}finally {
// 优雅退出,释放线程资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
/**
* ChannelInitializer 是一个特殊的处理类,他的目的是帮助使用者配置一个新的 Channel
* @author
*
*/
private class childChannelHandler extends ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel arg0) throws Exception {
arg0.pipeline().addLast(new ServerTestHandler());
}
}
public static void main(String[] args) throws InterruptedException {
int port = 8888;
new NettyServerTest().bind(port);
}
}
服务端ServerTestHandler类:
package com.test.netty;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class ServerTestHandler extends ChannelInboundHandlerAdapter {
private int counter;
/**
* 每当从客户端收到新的数据时,channelRead()方法会在收到消息时被调用
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "UTF-8").substring(0, req.length - System.getProperty("line.separator").length());
System.out.println("The time server receive order : " + body + " ; the counter is : " + ++counter);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date(
System.currentTimeMillis()).toString() : "BAD ORDER";
currentTime = currentTime + System.getProperty("line.separator");
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.write(resp);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// 将消息发送发送队列中的消息写入到SocketChannel中发送给对方
ctx.flush();
}
/**
* exceptionCaught() 事件处理方法是当出现 Throwable 对象才会被调用,
* 即当 Netty 由于 IO 错误或者处理器在处理事件时抛出的异常时
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
System.out.println(cause.getMessage());
ctx.close();
}
}
客户端NettyClientTest:
package com.test.netty;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
public class NettyClientTest {
public void connect(int port, String host) throws Exception {
// 配置客户端NIO线程组
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// TODO Auto-generated method stub
ch.pipeline().addLast(new ClientTestHandler());
}
});
// 发起异步连接操作
ChannelFuture f = b.connect(host, port).sync();
// 等待客户端链路关闭
f.channel().closeFuture().sync();
} finally {
// 优雅退出,释放NIO线程组
group.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
int port = 8888;
new NettyClientTest().connect(port, "127.0.0.1");
}
}
客户端ClientTestHandler类:
package com.test.netty;
import java.util.logging.Logger;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class ClientTestHandler extends ChannelInboundHandlerAdapter{
private