一. 粘包半包现象分析
服务端代码:
public static void main(String[] args) {
// 创建线程组
// bossGroup 处理链接请求
// workerGroup 处理客户端业务逻辑
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try{
// 创建服务端启动对象
new ServerBootstrap()
.group(bossGroup, workerGroup) // 设置两个线程组
.channel(NioServerSocketChannel.class) //设置tcpSocket通道 使用nio作为服务器通道
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("收到消息");
super.channelRead(ctx, msg);
}
});
}
}).bind(8000).sync();
} catch (Exception e){
e.printStackTrace();
}
}
客户端代码:
public static void main(String[] args) {
EventLoopGroup eventExecutors = new NioEventLoopGroup();
try {
// 创建客户端启动对象
ChannelFuture channelFuture = new Bootstrap()
.group(eventExecutors) // 设置线程组
.channel(NioSocketChannel.class) //设置 客户端通道的实现类
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new StringEncoder()); // 解码器
}
})
.connect("localhost", 8000);
channelFuture.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) {
Channel channel = future.channel();
for (int i = 0; i < 20; i++) {
channel.writeAndFlush("加入了连接!");
}
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
问题: 客户端连接成功后,分20次发送一串字符串,结果服务端只收到一次,包含了这20次发送的所有消息
分析原因: 以上问题就是由于tcp粘包导致的
客户端发送的报文(字节数据)会缓冲在服务端的滑动窗口
(缓冲区)中,当滑动窗口中缓冲了多个报文就会粘包,如果超过报文大小超过滑动窗口大小就会发生半包现象
注意:UDP是基于报文发送的,UDP报文的首部会有16bit来表现UDP数据的长度,所以不同的报文之间是可以区别隔离出来的,所以应用层接收传输层的报文时,不会存在拆包和粘包的问题
二. 解决:
1. 服务端设置固定报文大小
服务端设置缓冲区大小20个字节
ch.pipeline().addLast(new FixedLengthFrameDecoder(20));
客户端发送的字节小于20的话就补充到20
channel.writeAndFlush("加入了连接!__"); // __ 补充两个字节凑成20字节
2. 行解码器
服务端加入,默认以 \n 或 \r\n 作为分隔符,如果超出指定长度仍未出现分隔符,则抛出异常
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
客户端
channel.writeAndFlush("加入了连接!\n");
3. LTC解码器
服务端
- 第一个:最大长度 报文的总长度
- 第二个:长度字段偏移量 比如:设置成10 就是从报文的第11个字节开始就是代表长度
- 第三个:长度占用字节 因为这个解码器类似于协议,报文开头需要指定报文的字节数 如:
20加入了连接!__
,20就是报文的长度,后面的内容就是报文真实数据,如果这个20是int类型那就是4个字节,long类型就是8个字节 - 第四个:长度字节后的偏移量 比如:设置成10,就是从去掉长度后的报文第11个字节开始才是真实数据
- 第五个:剥离字节数 比如:设置成4 就是从报文的第5个字节开始就是报文的真实数据(ps:因为前四个字节是长度,所以建议剥离)
// 最大长度,长度偏移,长度占用字节,长度调整,剥离字节数
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4));
客户端:
public static void main(String[] args) {
EventLoopGroup eventExecutors = new NioEventLoopGroup();
try {
// 创建客户端启动对象
ChannelFuture channelFuture = new Bootstrap()
.group(eventExecutors) // 设置线程组
.channel(NioSocketChannel.class) //设置 客户端通道的实现类
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new StringEncoder()); // 解码器
}
})
.connect("localhost", 8000);
channelFuture.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) {
Channel channel = future.channel();
for (int i = 0; i < 20; i++) {
byte[] bytes = "加入了连接!".getBytes();
ByteBuf buffer = channel.alloc().buffer();
buffer.writeInt(bytes.length);
buffer.writeBytes(bytes);
// 18加入了连接!
channel.writeAndFlush(buffer);
}
}
});
} catch (Exception e) {
e.printStackTrace();
}
}