【Netty】粘包/拆包 详解
在本篇博文中,本人要来讲解一个十分重要的问题 —— 粘包/拆包
首先,本人来讲解下 什么是 粘包
和 拆包
:
定义:
TCP
是一个 流协议
,就是 没有界限 的一长串 二进制数据
TCP
作为 传输层协议,并不了解上层业务数据的具体含义,
它会根据 TCP缓冲区 的 实际情况 进行 数据包的划分
拆包:
在 业务 上认为是一个 完整的包,可能会被 TCP 拆分成 多个包 进行发送
粘包:
有可能把 多个小的包 封装成 一个大的数据包 发送,
这就是所谓的 TCP 粘包
和 拆包
问题
面向 “流” 的通信是 无消息保护边界 的
那么,本人现在来给出一张图,来 展示下 粘包
和 拆包
:
图示:
如下图所示,client发了 两个数据包 —— D1 和 D2
但是 server端 可能会收到 如下几种情况 的 数据:
可以看到:
- 第一行 是
正常发送
情况- 第二行 是
粘包
情况- 第三行 是
拆包
情况- 第四行 是
拆包
情况
现在,本人来展示下 使用Netty发送多条数据,出现 粘包拆包
现象:
情况展示:
服务端:
package edu.youzg.demo.stick;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
/**
* @Author: Youzg
* @CreateTime: 2021-05-06 16:37
* @Description: 带你深究Java的本质!
*/
public class NettyServerDemo {
public static void main(String[] args) {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new StringDecoder());
pipeline.addLast(new ServerHandler());
}
});
System.out.println("Netty Server start...");
ChannelFuture channelFuture = bootstrap.bind(9000).sync();
channelFuture.channel().closeFuture();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
}
}
static class ServerHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, String msg) throws Exception {
System.out.println("====服务端接收到消息如下====");
System.out.println("长度=" + msg.length());
System.out.println("内容=" + msg);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
}
客户端:
package edu.youzg.demo.stick;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
/**
* @Author: Youzg
* @CreateTime: 2021-05-06 16:37
* @Description: 带你深究Java的本质!
*/
public class NettyClientDemo {
public static void main(String[] args) {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new StringEncoder());
pipeline.addLast(new ClientHandler());
}
});
System.out.println("Netty Client start...");
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 9000).sync();
channelFuture.channel().closeFuture();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
static class ClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 0; i < 100; i++) {
ctx.writeAndFlush("Youzg 是一个 good man!");
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
}
那么,本人来运行下,我们来看看 运行结果:
运行结果:
我们可以看到:
发生了
粘包拆包
现象
那么,我们该 如何解决 粘包拆包情况 呢?
解决方案:
自定义协议格式
消息固定长度
:
传输的数据大小 固定长度
(例如:每段的长度固定为100字节,如果 不够空位补空格)特殊分隔符
:
在 数据包尾部 添加特殊分隔符
:
(例如:下划线,中划线等,这种方法简单易行,但选择分隔符的时候一定要注意 每条数据的内部 一定不能出现 分隔符)发送 内容+长度
:
发送每条 数据内容 的时候,将 数据的长度 一并发送
(例如:可以选择 每条数据的前4位 是 数据的长度,应用层处理时可以 根据长度 来判断 每条数据 的 开始和结束)
Netty
也提供了多个 解码器,可以进行 分包 的操作:
Netty解码器:
解码器 | 功能 |
---|---|
LineBasedFrameDecoder |
回车换行 分包 |
DelimiterBasedFrameDecoder |
特殊分隔符 分包 |
FixedLengthFrameDecoder |
固定长度报文 分包 |
有关 Netty
提供的解码器,同学们可以在网上查找具体的使用方法
本人在这里来展示下 自定义协议格式 的 处理实现:
自定义协议格式 处理实现:
那么,本人现在来展示下 自定义协议格式 使用:
自定义协议:
package edu.youzg.demo.splite;
/**
* 自定义协议 类型
* @Author: Youzg
* @CreateTime: 2021-05-06 11:08
* @Description: 带你深究Java的本质!
*/
public class MessageProtocol {
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;
}
}
接下来,是 服务器:
服务器:
package edu.youzg.demo.splite;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.util.CharsetUtil;
import java.util.List;
/**
* @Author: Youzg
* @CreateTime: 2021-05-06 10:07
* @Description: 带你深究Java的本质!
*/
public class NettyServerDemo {
public static void main(String[] args) {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new MessageDecoder());
pipeline.addLast(new ServerHandler());
}
});
System.out.println("Netty Server start...");
ChannelFuture channelFuture = bootstrap.bind(9000).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
static class ServerHandler extends SimpleChannelInboundHandler<MessageProtocol> {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, MessageProtocol messageProtocol) throws Exception {
System.out.println("====服务端接收到消息如下====");
System.out.println("长度=" + messageProtocol.getLength());
System.out.println("内容=" + new String(messageProtocol.getContent(), CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
static class MessageDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
int length = 0;
if (byteBuf.readableBytes() >= 4) { // 当前缓冲区 包含 整个 数据长度信息
length = byteBuf.readInt();
}
if (byteBuf.readableBytes() < length) { //当前缓冲区 未拥有 全部内容,继续等待
System.out.println("waitting...");
return;
}
byte[] content = new byte[length];
if (byteBuf.readableBytes() >= length) {
byteBuf.readBytes(content);
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLength(length);
messageProtocol.setContent(content);
list.add(messageProtocol);
}
}
}
}
接下来,是 客户端:
客户端:
package edu.youzg.demo.splite;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.MessageToByteEncoder;
import io.netty.util.CharsetUtil;
/**
* @Author: Youzg
* @CreateTime: 2021-05-06 10:30
* @Description: 带你深究Java的本质!
*/
public class NettyClientDemo {
public static void main(String[] args) {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new MessageEncoder());
pipeline.addLast(new ClientHandler());
}
});
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 9000).sync();
channelFuture.channel().closeFuture();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
static class ClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 0; i < 100; i++) {
String msg = "Youzg 是一个 good man!";
//创建 协议包对象
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLength(msg.getBytes(CharsetUtil.UTF_8).length);
messageProtocol.setContent(msg.getBytes(CharsetUtil.UTF_8));
ctx.writeAndFlush(messageProtocol);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
static class MessageEncoder extends MessageToByteEncoder<MessageProtocol> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, MessageProtocol messageProtocol, ByteBuf byteBuf) throws Exception {
byteBuf.writeInt(messageProtocol.getLength());
byteBuf.writeBytes(messageProtocol.getContent());
}
}
}
运行结果:
可以看到:
没有发生
粘包拆包
情况!