Java 独守闺房 夜探代理 鸡鸣灯灭
Netty 解决粘包拆分, 你必须要知道的知识 !!!
第一次接吻, 我记得是荔枝味的
那是一个阳光明媚的午后, 我和我大一岁的学姐恋爱了, 因为我没谈过, 根本没什么经验
于是她看看我看看自己, 突然脸红了起来, 对我道
“生命那么长, 我们最后可能不会在一起, 但我总要让你记住我点什么”
说着, 她从包里拿出了三颗带包装的糖, 我定睛一看, 有苹果味, 橘子味, 还有荔枝味
“你喜欢哪一种 ?” 她看向我, 脸更红了
“我…我喜欢吃荔枝口味…” 我一脸疑惑, 但还是结结巴巴的回答道
待我还没来得及说完, 她一把剥开糖衣将糖吃紧嘴里, 然后怔怔看向我, 我一时不知所措
愣神的功夫, 猝不及防, 她带着嘴里的糖亲了上来…
很多年过去了, 时间久远到让我忘记了她叫什么名字, 但是我还记得我的初吻, 那是荔枝味的 !
现在距离我们分手过去那么久, 我还是会偶尔吃荔枝味的糖, 不为什么, 就是会回想那个午后
…
其实铺垫那么久, 我想说的是, 我第一次吃人下面吃的事情
那也是一个阳光明媚的午后, 在她家的卧室里面, 她看着满头大汗的我说
“生命那么长, 我们最后可能不会在一起, 但我总要让你记住我点什么”
于是她果断去了厨房, 拿出了三罐调料, 有酱油, 醋, 和辣椒酱
你们也知道, 那个年纪的我总会在最爱的人面前露出自己最真实的一面, 当然, 我不是说我是个江西人会吃辣就有优越感什么的
然后, 耿直的我指了指那瓶装小米椒酱的玻璃瓶
…
现在的我还是很喜欢吃辣, 不要问问什么 ! 也不要问Why !!!
1. 最TM烦人的理论环节
拆包和粘包怎么出现的?
-
发送端为了将多个发给接收端的数据包,更有效地发送到接收端,会使用Nagle算法。Nagle算法会将多次时间间隔较小且数据量小的数据合并成一个大的数据块进行发送。虽然这样的确提高了效率,但是因为面向流通信,数据是无消息保护边界的,就会导致接收端难以分辨出完整的数据包了, 这就是所谓的粘包。
-
而当发送数据包过大, 为了网络传输的效率, 总会在数据缓冲区满的时候发送, 因此, TCP又会将数据拆分出一个一个相对大小的块发送, 因此按接收数据角度来讲, 就会出现拆包的问题
所谓的粘包和拆包问题,就是因为TCP消息无保护边界导致的。
2. 图解理论
3. 整出问题咋办, 解决方案啊
-
固定包大小(只要我的很大, 那你肯定装的下!), 比如指定一个包长度 1024, 不满化, 空格补齐就行了
-
分割符指定包边界(没有边界, 我就指定边界!), 按分隔符将包分割成一个一个包就可以顺利读取了, 就算他长度不一样, 那我可以读取出中间的包
-
数据拆分为head和body, head固定字节存放身体长度, 只需要读取到head信息, 则就可以根据读取出来的长度读取剩下的body
4. 不逼逼, 上代码 !
前置工作
Netty 的代码太模板化了, 我这里只提供一个 Server和Client中的文件信息
服务端代码
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.string.StringDecoder;
public class PackServer {
public static void main(String[] args) {
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
try {
bootstrap.group(bossGroup, workGroup).channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 中间通过 ch.pipeline().addLast() 添加处理器即可
ch.pipeline().addLast(new MyServerHandler());
}
});
ChannelFuture sync = bootstrap.bind(8000).sync();
sync.channel().closeFuture().sync();
} catch (Exception ex) {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
}
MyServerHandler 这里接收消息, 然后打印, 最后发送五条消息, 查看是否会发生拆包现象
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class MyServerHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println("接收到客户端消息: " + msg.trim());
for (int i = 0; i < 5; i++) {
ctx.writeAndFlush("嘿, hello client!"));
System.out.println("发送消息给服务端: " + "hello client!");
}
}
}
客户端代码
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.string.StringDecoder;
public class PackClient {
public static void main(String[] args) {
NioEventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.group(group).channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 中间通过 ch.pipeline().addLast() 添加处理器即可
ch.pipeline().addLast(new MyClientHandler());
}
});
ChannelFuture sync = bootstrap.connect("127.0.0.1", 8000).sync();
sync.channel().closeFuture().sync();
} catch (Exception ex) {
group.shutdownGracefully();
}
}
}
客户端处理器, 当建立连接时发送长度很长的消息给服务端, 测试是否拆包
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import java.nio.charset.StandardCharsets;
public class MyClientHandler extends SimpleChannelInboundHandler<String> {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("发送消息给服务端: 宝贝, 我今天晚上要去欧洲啦啦啦啦啦啦啦啦啦");
ctx.writeAndFlush("发送消息给服务端: 宝贝, 我今天晚上要去欧洲啦啦啦啦啦啦啦啦啦");
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println("收到来自服务端信息: " + msg.trim());
}
}
至此, 准备工作差不多了
方案一: 固定包大小
服务端添加代码
添加服务端处理器链 (放在 ch.pipeline().addLast(new MyServerHandler()); 之前, 顺序很重要)
// 这里将FixedLengthFrameDecoder添加到pipeline中,指定长度为20
ch.pipeline().addLast(new FixedLengthFrameDecoder(1024));
// 将前一步解码得到的数据转码为字符串
ch.pipeline().addLast(new StringDecoder());
// 这里需要自己实现一个编码器
ch.pipeline().addLast(new FixedLengthFrameEncoder(1024));
需要自己实现一个 固定长度的编码器, 主要做的是将消息不足长度用空格补的逻辑 FixedLengthFrameEncoder()
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
public class FixedLengthFrameEncoder extends MessageToByteEncoder<String> {
private int len;
public FixedLengthFrameEncoder(int len) {
this.len = len;
}
@Override
protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) throws Exception {
StringBuilder msgBuilder = new StringBuilder(msg);
if (msg.length() < len) {
System.out.println("msg.length()=" + msg.length() + " len=" + len);
// 这里不能用 StringBuilder 的length()去获取, 因为每次添加 ' ' 后这个值会变化, 导致添加个数不是制定的 len
for (int i = 0; i < len - msg.length(); i++) {
msgBuilder.append(" ");
}
msg = msgBuilder.toString();
}
out.writeBytes(Unpooled.wrappedBuffer(msg.getBytes()));
}
}
客户端添加代码
// 固定长度方法: 这里将FixedLengthFrameDecoder添加到pipeline中,指定长度为20
ch.pipeline().addLast(new FixedLengthFrameDecoder(1024));
// 将前一步解码得到的数据转码为字符串
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new FixedLengthFrameEncoder(1024));
查看结果:
不要惊讶, 基操务六, FixedLengthFrameDecoder 其底层就是读取字符串长度来做的, 会吧不满足制定长度的包丢弃, 这就是我们要去重写一个 FixedLengthFrameEncoder 的原因(不足指定长度的数据补空处理)
方案二: 指定包分割符
服务端添加代码
String delimiter = "_$";
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer(delimiter, StandardCharsets.UTF_8)));
// 自定义一个编码器发送数据按编码格式发送即可
ch.pipeline().addLast(new DelimiterBasedFrameEncoder(delimiter));
// 将分隔符分开的数据包转换成String
ch.pipeline().addLast(new StringDecoder());
新增一个 DelimiterBasedFrameEncoder 编码器, 用于发送消息是编码加上分隔符的逻辑
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import java.nio.charset.StandardCharsets;
public class DelimiterBasedFrameEncoder extends MessageToByteEncoder<String> {
private String delimiter;
public DelimiterBasedFrameEncoder(String delimiter) {
this.delimiter = delimiter;
}
@Override
protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) throws Exception {
// 将数据加上分隔符返回回去, 这里要注意下, 因为下一个编码器是没配置, 因此需要转为 ByteBuf
ByteBuf byteBuf = Unpooled.copiedBuffer(msg + delimiter, StandardCharsets.UTF_8);
ctx.writeAndFlush(byteBuf);
}
}
客户端添加代码
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer(delimiter, StandardCharsets.UTF_8)));
// 自定义一个编码器发送数据按编码格式发送即可
ch.pipeline().addLast(new DelimiterBasedFrameEncoder(delimiter));
// 将分隔符分开的数据包转换成String
ch.pipeline().addLast(new StringDecoder());
运行结果
可以看到这里也是没毛病的, 要注意, 里面的参数填1024, 指的是包最大只能这个大小, 如果操过了, 就会报错, 导致解析不了数据, 其实除了自己加分隔符外, 还可以用netty自带的换行符解码器, 这个玩意也是OK的
扩展实现(可以自己去尝试下 LineBasedFrameDecoder )
需要注意的是也需要指定最大长帧长度
方案三: 通过在消息头中定义长度字段来标识消息的总长度
服务端添加代码
/**
* lengthFieldLength – 预置长度字段的长度。只允许 1、2、3、4 和 8。
* lengthAdjustment – 要添加到长度字段值的补偿值
* lengthIncludesLengthFieldLength – 如果 true,则将预置长度字段的长度添加到预置长度字段的值中。
*/
ch.pipeline().addLast(new LengthFieldPrepender(2, 0, false));
// 自定义一个编码器发送数据按编码格式发送即可
/**
* maxFrameLength – 框架的最大长度。如果帧的长度大于此值, TooLongFrameException 则将被抛出。
* lengthFieldOffset – 长度字段的偏移量
* lengthFieldLength – 长度字段的长度
* lengthAdjustment – 要添加到长度字段值的补偿值
* initialBytesToStrip – 要从解码帧中剥离出的第一个字节数
*/
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 2, 0, 2));
// 将byteBuf 转化为String, 以及发送数据为String, 然后转byteBuf
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new StringEncoder());
服务端添加代码
/**
* lengthFieldLength – 预置长度字段的长度。只允许 1、2、3、4 和 8。
* lengthAdjustment – 要添加到长度字段值的补偿值
* lengthIncludesLengthFieldLength – 如果 true,则将预置长度字段的长度添加到预置长度字段的值中。
*/
ch.pipeline().addLast(new LengthFieldPrepender(2, 0, false));
// 自定义一个编码器发送数据按编码格式发送即可
/**
* maxFrameLength – 框架的最大长度。如果帧的长度大于此值, TooLongFrameException 则将被抛出。
* lengthFieldOffset – 长度字段的偏移量
* lengthFieldLength – 长度字段的长度
* lengthAdjustment – 要添加到长度字段值的补偿值
* initialBytesToStrip – 要从解码帧中剥离出的第一个字节数
*/
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 2, 0, 2));
// 将byteBuf 转化为String, 以及发送数据为String, 然后转byteBuf
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new StringEncoder());
其实吧客户端和服务端都是一样的, 不需要修改啥的
运行结果
5. 总结
Netty自带的解决拆包和粘包的方案总分三种
-
固定数据包的长度
解码处理器: FixedLengthFrameDecoder(指定数据帧最大长度)
编码处理器: 自己实现一个 FixedLengthFrameEncoder(指定数据帧最大长度), 处理发送数据时的长度补齐 -
指定包之间的分隔符
方案一
解码处理器: DelimiterBasedFrameDecoder(指定数据帧最大长度, 分隔符))
编码处理器: 自己实现一个 DelimiterBasedFrameEncoder(指定数据帧最大长度, 分隔符), 处理发送数据时的分割符拼接, 注意这里的 数据帧长度, 自己实现的时候大于这个就可以抛异常了
方案二
解码处理器: LineBasedFrameDecoder(指定数据帧最大长度))
编码处理器: 自己实现一个 LineBasedFrameEncoder(指定数据帧最大长度), 处理发送数据时的分割符拼接, 注意这里的 数据帧长度, 自己实现的时候大于这个就可以抛异常了
这里发现个好玩的事情, LineBasedFrameEncoder 既可以继承 MessageToByteEncoder, 也可以继承 MessageToMessageEncoder, 不同的是他们将数据传递到下一个Handler的方式不一样
- 通过在消息头中定义长度字段来标识消息的总长度
解码处理器: LengthFieldPrepender(预置长度字段的长度, 添加到长度字段值的补偿值, 如果 true,则将预置长度字段的长度添加到预置长度字段的值中)
编码处理器: LengthFieldBasedFrameDecoder(指定数据帧最大长度, 长度字段的偏移量, 长度字段的长度, 要添加到长度字段值的补偿值, 要从解码帧中剥离出的跳过的第几个字节数)
具体实现其实就是将数据包分为, head, body, 然后加上一些字段偏移信息保证能读取到准确的数据来实现的
到这里就可以完结撒花了, 对了, 提一嘴, 我觉得还是江西的小米辣好吃 !!!