一 什么是TCP 粘包拆包
TCP 协议是流数据,流数据的特点就是没有分界线;TCP 会将数据流 缓冲进 缓冲池,缓冲池对数据流进行推送;
缓冲池对数据发送有可能完整的2个包回黏在一起发送,称为粘包
缓冲池中有可能会对数据流进行拆包 发送数据,有可能数据包1中包含数据包2, 数据包2中包含数据包1;
二 粘包拆包产生的原因
- 发送的数据大于TCP发送缓冲区剩余空间大小,TCP会发生拆包。
- 发送数据大于MSS(最大报文长度),TCP会在传输前进行拆包。
- 发送数据远小于TCP缓冲区的大小,TCP将多次写入缓冲区的数据一次发送,将会发生粘包。
- 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
三 粘包拆包的解决方案
- 在报文末尾增加换行符表明一条完整的消息,接收端可以根据换行符来判断消息是否完整。
- 将消息分为消息头、消息体。可以在消息头中声明消息的长度,根据这个长度来获取报文(比如 808 协议)。
- 规定好报文长度,不足的空位补齐,接收端取的时候按照长度截取
四 模拟粘包
我们在netty 入门应用中 改造 客户端的激活方法, 加入100个循环;
/* *
* @Author lsc
* <p>触发回调 </p>
* @Param [ctx]
* @Return void
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i=0;i<100;i++){
byte[] bytes = "关注公众号知识追寻者回复netty获取本教程源码".getBytes();
// 创建节字缓冲区
ByteBuf message = Unpooled.buffer(bytes.length);
// 将数据写入缓冲区
message.writeBytes(bytes);
// 写入数据
ctx.writeAndFlush(message);
}
}
然后我们服务端就接收到的消息如下,发现发送的数据都连接在了一起,即发生了数据包的粘包情况;
五 解决拆包粘包
LineBasedFrameDecoder 解码器
netty 中默认提高了多种节码器,和编码器;我们可以利用不同的编码,节码器进行解决粘包拆包的问题;
比如 利用 LineBasedFrameDecoder 进行粘包问题;
我们在服务端的通道初始化的时候加上编码器即可
/**
* @Author lsc
* <p>通道初始化 </p>
* @Param
* @Return
*/
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 管道(Pipeline)持有某个通道的全部处理器
ChannelPipeline pipeline = socketChannel.pipeline();
// 解决粘包问题
pipeline.addLast(new LineBasedFrameDecoder(1024));
pipeline.addLast(new StringDecoder());
// 添加处理器
pipeline.addLast(new NettyServerHandler());
}
}
当然我们在 客户端发送消息的时候就需要加上分割符号(知识追寻者这边以换行符号作为分割),要不然服务端没法接收消息
/* *
* @Author lsc
* <p>触发回调 </p>
* @Param [ctx]
* @Return void
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i=0;i<100;i++){
byte[] bytes = "关注公众号知识追寻者回复netty获取本教程源码
".getBytes();
// 创建节字缓冲区
ByteBuf message = Unpooled.buffer(bytes.length);
// 将数据写入缓冲区
message.writeBytes(bytes);
// 写入数据
ctx.writeAndFlush(message);
}
}
最终打印效果如下
如果想在客户端也解决此类问题,加上解码器即可;
注:LineBasedFrameDecoder 支持
或者
进行解码;
DelimiterBasedFrameDecoder 解码器
DelimiterBasedFrameDecoder 解码器的应用和 LineBasedFrameDecoder 相似,不过 优点是我们可以自定义分割符号;
比如我们修改服务端的解码器为 DelimiterBasedFrameDecoder ,指定分割符合为 %
/**
* @Author lsc
* <p>通道初始化 </p>
* @Param
* @Return
*/
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 管道(Pipeline)持有某个通道的全部处理器
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new DelimiterBasedFrameDecoder(10240, Unpooled.copiedBuffer("%".getBytes())));
pipeline.addLast(new StringDecoder());
// 添加处理器
pipeline.addLast(new NettyServerHandler());
}
}
我们在客户端发送数据的时候以 %
为结尾;
/* *
* @Author lsc
* <p>触发回调 </p>
* @Param [ctx]
* @Return void
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i=0;i<100;i++){
byte[] bytes = "关注公众号知识追寻者回复netty获取本教程源码%".getBytes();
// 创建节字缓冲区
ByteBuf message = Unpooled.buffer(bytes.length);
// 将数据写入缓冲区
message.writeBytes(bytes);
// 写入数据
ctx.writeAndFlush(message);
}
}
FixedLengthFrameDecoder 解码器
FixedLengthFrameDecoder是按固定的数据长度来进行解码,我们就不用关系分割符号的问题,所以这种节码器也非常实用;
我们 需要精确的计算客户端发送的数据长度,然后在服务端解码器中配置,否则会出现乱码等情况;
/**
* @Author lsc
* <p>通道初始化 </p>
* @Param
* @Return
*/
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 管道(Pipeline)持有某个通道的全部处理器
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new FixedLengthFrameDecoder(63));
pipeline.addLast(new StringDecoder());
// 添加处理器
pipeline.addLast(new NettyServerHandler());
}
}
效果如下