Netty——TCP粘包和拆包
一、概述
对于TCP,发送端为了更高效地发送多个发给接收端的包,使用了优化方法——Nagle算法,将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界的。在接收端处理消息边界问题,就是粘包、拆包问题。
粘包拆包情况:
二、TCP粘包演示
- MyServerHandler
public class MyServerHandler extends SimpleChannelInboundHandler<ByteBuf> {
private int count = 0;
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
byte[] bytes = new byte[msg.readableBytes()];
msg.readBytes(bytes);
// 将 buffer 转成字符串
String message = new String(bytes, CharsetUtil.UTF_8);
System.out.println("服务器接收到数据 " + message);
System.out.println("服务器接收到消息量 = " + (++this.count));
// 服务器回送数据到客户端,回送一个随机 Id
ByteBuf response = Unpooled.copiedBuffer(UUID.randomUUID().toString() + "--", CharsetUtil.UTF_8);
ctx.writeAndFlush(response);
}
}
- MyClientHandler
public class MyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
private int count = 0;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 使用客户端发送 10 条数据,hello,server
for (int i = 0; i < 10; i++) {
String msg = "server" + i + " ";
System.out.println("发送消息 " + msg);
ByteBuf byteBuf = Unpooled.copiedBuffer(msg, CharsetUtil.UTF_8);
ctx.writeAndFlush(byteBuf);
}
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
byte[] bytes = new byte[msg.readableBytes()];
msg.readBytes(bytes);
// 将 buffer 转成字符串
String message = new String(bytes, CharsetUtil.UTF_8);
System.out.println("客户端接收到数据 " + message);
System.out.println("客户端接收到消息量 = " + (++this.count));
}
}
- 结果
出现粘包情况
三、解决方案——自定义协议
发送端每发送一次消息,就携带消息的长度,这样,接收方每次先接收消息的长度,再根据长度去读取该消息剩余的内容。
- 自定义MessageProtocol对象
public class MessageProtocol {
// 关键
private int len;
private byte[] content;
}
- 将ByteBuf转换成MessageProtocol的解码器
public class MessageDecoder extends ReplayingDecoder<Void> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
System.out.println("MessageDecoder 被调用");
// 需要将获取到的二进制字节码转换成 MessageProtocol
int length = in.readInt();
byte[] content = new byte[length];
in.readBytes(content);
// 封装成 MessageProtocol 对象,放入 out,传递到下一个 Handler
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLen(length);
messageProtocol.setContent(content);
out.add(messageProtocol);
}
}
- 将MessageProtocol转换为ByteBuf的编码器
public class MessageEncoder extends MessageToByteEncoder<MessageProtocol> {
@Override
protected void encode(ChannelHandlerContext ctx, MessageProtocol msg, ByteBuf out) throws Exception {
System.out.println("MessageEncoder 方法被调用");
out.writeInt(msg.getLen());
out.writeBytes(msg.getContent());
}
}
- MyServerHandler
public class MyServerHandler extends SimpleChannelInboundHandler<MessageProtocol> {
private int count = 0;
// 接收的 Handler 继承了 SimpleChannelInboundHandler,以 MessageProtocol 的类型接收消息
@Override
protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {
// 接收到数据,并处理
int len = msg.getLen();
byte[] content = msg.getContent();
System.out.println("服务器第 " + (++count) +" 次接收到信息如下:");
System.out.println("长度:" + len);
System.out.println("内容:" + new String(content, CharsetUtil.UTF_8));
// 回复消息
String response = UUID.randomUUID().toString();
int length = response.getBytes(CharsetUtil.UTF_8).length;
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLen(length);
messageProtocol.setContent(response.getBytes());
ctx.writeAndFlush(messageProtocol);
}
}
- MyClientHandler
public class MyClientHandler extends SimpleChannelInboundHandler<MessageProtocol> {
private int count = 0;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 使用客户端发送 5 条数据,"今天天气冷,吃火锅" 编号
for (int i = 0; i < 5; i++) {
String message = "今天天气冷,吃火锅" + i;
byte[] content = message.getBytes(CharsetUtil.UTF_8);
int length = content.length;
// 创建协议包对象
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLen(length);
messageProtocol.setContent(content);
ctx.writeAndFlush(messageProtocol);
}
}
}
- 结果