TCP粘包与拆包基本介绍
- TCP是面向连接的,面向流的,提供高可靠 性服务。收发两端 (客户端 和服务端)都要有一一对比的socket,因此,发送端为了就多个发给服务端的包,更有效的发给对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合成 一个大的数据块,然后进行封包。这样做虽然高效,但接收端就难与分辨出完整的数据包了,因为面向流的通信是无消息保护边界的
- 由于TCP无消息保护边界,需要在接收端处理消息边界问题,也就是我们所说的粘包,拆包问题
- TCP粘包,拆包图解
假设 客户端分别发送了2个数据包D1和D2给服务端,由于服务端一次读到的字节数是不定的,故可能出现以下四种情况:- 服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包
- 服务端一次接受到了两个数据包,D1和D2粘合在一起,称之为TCP粘包
- 服务端分两次读取到了数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这称之为TCP拆包
- 服务端分两次读取到了数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余部分内容D1_2和完整的D2包。
下面我们来编写一个程序,如果没有做处理,就会发生粘包和拆包问题
客户端程序:
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new NettyClientInitialize());
System.out.println("客户端启动。。。");
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6666).sync();
channelFuture.channel().closeFuture().sync();
} finally {
eventLoopGroup.shutdownGracefully();
}
}
}
/
public class NettyClientInitialize extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new NettyClientHandler());
}
}
/
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
/**
* 通道就绪就会触发此方法, 发送10条数据给服务器
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 0; i < 10; i++) {
String msg = "Hello,服务器,我是消息" + i;
ctx.writeAndFlush(Unpooled.copiedBuffer(msg, CharsetUtil.UTF_8));
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("收到服务器发来消息:" + byteBuf.toString(CharsetUtil.UTF_8));
}
}
服务端程序
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
// 初始化2个线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 引导程序
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new NettyServerInitialize());
System.out.println("服务器启动。。。");
ChannelFuture channelFuture = serverBootstrap.bind(6666).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
//
public class NettyServerInitialize extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new NettyServerHandler());
}
}
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
private int count;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("服务器ChannelRead方法被调用"+ (++count) + "次数");
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("收到客户端发来消息:"+ byteBuf.toString(CharsetUtil.UTF_8));
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// 发送消息给客户端
String msg = "Hello,我是服务器 ";
ctx.writeAndFlush(Unpooled.copiedBuffer(msg, CharsetUtil.UTF_8));
}
}
我没来运行2个客户端,来看看测试结果
服务器启动。。。
服务器ChannelRead方法被调用1次数
收到客户端发来消息:Hello,服务器,我是消息0Hello,服务器,我是消息1Hello,服务器,我是消息2Hello,服务器,我是消息3Hello,服务器,我是消息4Hello,服务器,我是消息5Hello,服务器,我是消息6Hello,服务器,我是消息7Hello,服务器,我是消息8Hello,服务器,我是消息9
服务器ChannelRead方法被调用1次数
收到客户端发来消息:Hello,服务器,我是消息0
服务器ChannelRead方法被调用2次数
收到客户端发来消息:Hello,服务器,我是消息1
服务器ChannelRead方法被调用3次数
收到客户端发来消息:Hello,服务器,我是消息2Hello,服务器,我是消息3Hello,服务器,我是消息4
服务器ChannelRead方法被调用4次数
收到客户端发来消息:Hello,服务器,我是消息5Hello,服务器,我是消息6
服务器ChannelRead方法被调用5次数
收到客户端发来消息:Hello,服务器,我是消息7
服务器ChannelRead方法被调用6次数
收到客户端发来消息:Hello,服务器,我是消息8Hello,服务器,我是消息9
客户端启动。。。
收到服务器发来消息:Hello,我是服务器 Hello,我是服务器 Hello,我是服务器 Hello,我是服务器 Hello,我是服务器 Hello,我是服务器
从运行结果 我们可以分析得出:
- 当我们从客户端连续发送10次消息到服务器,服务器这边接收不一定分10次接收,
- 服务器接收客户端发来的消息可以使一次性接收所有消息,也有可能是2次,3次或者9次等。这就是TCP的粘包和拆包问题了
- 服务器无法分辨出客户端发送消息的边界,因此我们需要来解决TCP粘包和拆包问题
TCP粘包和拆包解决 方案
- 使用自定义协议 + 编解码器来解决
- 关键就是要解决服务器端每次读取数据长度的问题,这个问题解决,就不会出现服务器多读或者少读 的问题,从而避免 了TCP粘包,拆包。
代码示例(自定义协议 + 编解码器):
- 要求客户端发送 5 个 Message 对象, 客户端每次发送一个 Message 对象
- 服务器端每次接收一个Message, 分5次进行解码, 每读取到 一个Message , 会回复一个Message 对象 给客户端.
客户端程序
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new NettyClientInitialize());
System.out.println("客户端启动。。。");
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6666).sync();
channelFuture.channel().closeFuture().sync();
} finally {
eventLoopGroup.shutdownGracefully();
}
}
}
/public class NettyClientInitialize extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
// 加入编码器
pipeline.addLast(new MessageEncoder());
// 加入解码器
pipeline.addLast(new MessageDecoder());
pipeline.addLast(new NettyClientHandler());
}
}
客户端处理器
public class NettyClientHandler extends SimpleChannelInboundHandler<MessageProtocol> {
/**
* 通道就绪就会触发此方法, 发送10条数据给服务器
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 0; i < 5; i++) {
String msg = "Hello,服务器,我是消息" + i;
byte[] content = msg.getBytes(CharsetUtil.UTF_8);
int length = msg.getBytes(CharsetUtil.UTF_8).length;
// 封装消息
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLength(length);
messageProtocol.setContent(content);
ctx.writeAndFlush(messageProtocol);
}
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, MessageProtocol messageProtocol) throws Exception {
System.out.println("收到服务器发来消息:" + new String(messageProtocol.getContent()));
}
}
协议类(关键)
public class MessageProtocol {
// 表示发送数据的长度
private int length;
// 发送数据的内容
private byte[] content;
}
解码器和编码器(关键)
/**
* 编码器
*/
public class MessageEncoder extends MessageToByteEncoder<MessageProtocol> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, MessageProtocol messageProtocol, ByteBuf byteBuf) throws Exception {
System.out.println("编码器被调用");
byteBuf.writeInt(messageProtocol.getLength());
byteBuf.writeBytes(messageProtocol.getContent());
}
}
/**
* 解码器
*/
public class MessageDecoder extends ReplayingDecoder<Void> {
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
System.out.println("解码器被调用");
// 得到消息长度和内容
int length = byteBuf.readInt();
byte[] content = new byte[length];
byteBuf.readBytes(content);
// 将消息封装成MessagePool,放入 list, 传递下一个handler业务处理
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLength(length);
messageProtocol.setContent(content);
list.add(messageProtocol);
}
}
服务端程序
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
// 初始化2个线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 引导程序
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new NettyServerInitialize());
System.out.println("服务器启动。。。");
ChannelFuture channelFuture = serverBootstrap.bind(6666).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class NettyServerInitialize extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
// 加入编解码器
pipeline.addLast(new MessageEncoder());
pipeline.addLast(new MessageDecoder());
pipeline.addLast(new NettyServerHandler());
}
}
服务端处理器
public class NettyServerHandler extends SimpleChannelInboundHandler<MessageProtocol> {
private int count;
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, MessageProtocol messageProtocol) throws Exception {
System.out.println("服务器channelRead0方法被调用"+ (++count) + "次数");
byte[] content = messageProtocol.getContent();
System.out.println("收到客户端发来消息:"+ new String(content));
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// 发送消息给客户端
String msg = "Hello,我是服务器 ";
byte[] content = msg.getBytes(CharsetUtil.UTF_8);
int length = msg.getBytes(CharsetUtil.UTF_8).length;
// 封装对象
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLength(length);
messageProtocol.setContent(content);
ctx.writeAndFlush(messageProtocol);
}
}
运行结果(启动多个客户多):
服务器启动。。。
解码器被调用
服务器channelRead0方法被调用1次数
收到客户端发来消息:Hello,服务器,我是消息0
解码器被调用
服务器channelRead0方法被调用2次数
收到客户端发来消息:Hello,服务器,我是消息1
解码器被调用
服务器channelRead0方法被调用3次数
收到客户端发来消息:Hello,服务器,我是消息2
解码器被调用
服务器channelRead0方法被调用4次数
收到客户端发来消息:Hello,服务器,我是消息3
解码器被调用
服务器channelRead0方法被调用5次数
收到客户端发来消息:Hello,服务器,我是消息4
编码器被调用
解码器被调用
服务器channelRead0方法被调用1次数
收到客户端发来消息:Hello,服务器,我是消息0
编码器被调用
解码器被调用
服务器channelRead0方法被调用2次数
收到客户端发来消息:Hello,服务器,我是消息1
编码器被调用
解码器被调用
服务器channelRead0方法被调用3次数
收到客户端发来消息:Hello,服务器,我是消息2
解码器被调用
服务器channelRead0方法被调用4次数
收到客户端发来消息:Hello,服务器,我是消息3
编码器被调用
解码器被调用
服务器channelRead0方法被调用5次数
收到客户端发来消息:Hello,服务器,我是消息4
从结果我们可得知,客户端发送5次信息,客户端根据协议接收5次消息,启用多个客户端测试结果还是一样,这样就完美结果了TCP粘包和拆包问题,
解决TCP粘包和拆包的关键在于定义发送消息的协议和编解码器,让消息根据协议和编解码器发送,从而结果了TCP拆包和粘问题。