什么是粘包拆包?
假设客户端分别发送两个数据包 D1
和 D2
给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况:
- 服务端分两次读取到了两个独立的数据包,分别是
D1
和D2
,没有粘包和拆包。
- 服务端一次接收到了两个数据包,
D1
和D2
粘合在一起,称为TCP粘包。 - 服务端第一次读到完整的
D1
包和D2
包的部分,第二次读到D2
剩余的部分。称为TCP拆包。 - 服务端第一次读取到
D1
包的部分内容,第二次读取到D1
包的剩余部分和D2
的全部。
产生的原因
- 应用程序
write
写入的字节大小大于套接口发送缓冲区大小。 - 进行mss(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包
- 以太网帧的payload(净荷)大于MTU(1500字节)进行ip分片。
解决方法
1. 短连接
发一个包建立一次连接,这样连接建立到连接断开之间就是消息的边界,缺点效率太低。
测试步骤:
1. 导入依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
2. 服务端测试代码
public class TestServer {
static final Logger log = LoggerFactory.getLogger(TestServer.class);
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
log.debug("conneted...");
// 最大长度,长度偏移,长度占用字节,长度调整,剥离字节数
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 1, 0, 1));
//设置固定分隔符
// ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
//固定长度
// ch.pipeline().addLast(new FixedLengthFrameDecoder(8));
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(msg);
ctx.fireChannelRead(msg);
System.out.println(msg);
}
});
}
})
.bind(new InetSocketAddress("localhost",8200));
}
}
LengthFieldBasedFrameDecoder
:参数含义:
maxFrameLength
:最大帧长度。也就是可以接收的数据的最大长度。如果超过,此次数据会被丢弃。
lengthFieldOffset
:长度域偏移。就是说数据开始的几个字节可能不是表示数据长度,需要后移几个字节才是长度域。
lengthFieldLength
:长度域字节数。用几个字节来表示数据长度。
lengthAdjustment
:数据长度修正。因为长度域指定的长度可以使header+body的整个长度,也可以只是body的长度。如果表示header+body的整个长度,那么我们需要修正数据长度。
initialBytesToStrip
:跳过的字节数。如果你需要接收header+body的所有数据,此值就是0,如果你只想接收body数据,那么需要跳过header所占用的字节数。
3. 客户端测试代码:
public class ShortConnect {
/**
* 短连接
*/
static final Logger log = LoggerFactory.getLogger(ShortConnect.class);
public static void main(String[] args){
for (int i = 0; i < 10; i++) {
send();
}
}
public static void send(){
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(worker)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
log.debug("conneted...");
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
log.debug("sending...");
ByteBuf buffer = ctx.alloc().buffer();
buffer.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
ctx.writeAndFlush(buffer);
// 发完即关
ctx.close();
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect("localhost",8200).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e){
log.error("client error");
} finally {
worker.shutdownGracefully();
}
}
}
2. 固定长度
每一条消息采用固定长度。
缺点是,数据包的大小不好把握
- 长度定的太大,浪费
- 长度定的太小,对某些数据包又显得不够
修改代码
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.debug("sending...");
// 发送内容随机的数据包
Random r = new Random();
char c = 'a';
ByteBuf buffer = ctx.alloc().buffer();
for (int i = 0; i < 10; i++) {
byte[] bytes = new byte[8];
for (int j = 0; j < r.nextInt(8); j++) {
bytes[j] = (byte) c;
}
c++;
//每次发送固定长度的数据包
buffer.writeBytes(bytes);
}
ctx.writeAndFlush(buffer);
}
3. 固定分隔符
处理字符数据比较合适,但如果内容本身包含了分隔符(字节数据常常会有此情况),那么就会解析错误
修改代码
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.debug("sending...");
// 发送内容随机的数据包
Random r = new Random();
char c = 'a';
ByteBuf buffer = ctx.alloc().buffer();
for (int i = 0; i < 10; i++) {
byte[] bytes = new byte[8];
for (int j = 0; j < r.nextInt(8); j++) {
bytes[j] = (byte) c;
}
c++;
buffer.writeBytes(bytes);
}
ctx.writeAndFlush(buffer);
}
4. 预设长度
在发送消息前,先约定用定长字节表示接下来数据的长度。
服务端 pipeline中 添加
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 1, 0, 1));
LengthFieldBasedFrameDecoder
:参数含义:
maxFrameLength
:最大帧长度。也就是可以接收的数据的最大长度。如果超过,此次数据会被丢弃。
lengthFieldOffset
:长度域偏移。就是说数据开始的几个字节可能不是表示数据长度,需要后移几个字节才是长度域。
lengthFieldLength
:长度域字节数。用几个字节来表示数据长度。
lengthAdjustment
:数据长度修正。因为长度域指定的长度可以是header+body的整个长度,也可以只是body的长度。如果表示header+body的整个长度,那么我们需要修正数据长度。
initialBytesToStrip
:跳过的字节数。如果你需要接收header+body的所有数据,此值就是0,如果你只想接收body数据,那么需要跳过header所占用的字节数。
客户端代码:
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.debug("sending...");
// 发送内容随机的数据包
Random r = new Random();
char c = 'a';
ByteBuf buffer = ctx.alloc().buffer();
for (int i = 0; i < 10; i++) {
int length = r.nextInt(16);
//先写入长度
buffer.writeByte(length);
//再写入数据
for (int j = 0; j < length; j++) {
buffer.writeByte((byte) c);
}
c++;
}
ctx.writeAndFlush(buffer);
}