在RPC框架中,粘包与拆包问题是必须解决的一个问题,因为RPC框架中,各个微服务相互之间都是维护一个TCP长连接,比如dubbo就是一个全双工的长连接。由于微服务往对方发送消息的时候,所有的请求都是使用的同一个连接,这样就会产生粘包和拆包的问题,就会出现丢包的情况,Netty提供了更好的解决方案。
1、粘包和拆包
产生粘包和拆包的主要原因是:操作系统在发送TCP数据包的时候,底层会有一个缓冲区,例如1024字节大小,如果一次请求发送的数据量比较小,没达到缓存区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题;如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包问题,也就是将一个大的包拆分为多个小包进行发送。
2、常见解决方案
(1)客户端在发送数据包的时候,每个包都固定长度,比如1024字节大小,如果客户端发送的数据长度不足1024个字节,则通过补充空格的方式补全到指定长度进行发送;
(2)客户端在每个包的末尾使用固定的分隔符,例如\n\r,如果一个包被拆分了,则等待下一个包发送过来之后找到其中的\r\n,然后对其拆分后的头部部分与前一个包的剩余部分进行合并,这样就得到了一个完整的包;
(3)将消息分为头部和消息体,在头部中保存有当前整个消息的长度,只有在读取到足够长度的消息之后才算是读到了一个完整的消息;
(4)通过自定义协议进行粘包和拆包的处理;
3、Netty提供的粘包和拆包解决方案
3.1 FixedLengthFrameDecoder
对于使用固定长度的粘包和拆包场景,可以使用FixedLengthFrameDecoder,该解码器会每次读取固定长度的消息,如果当前读取的消息不足指定的长度,那么就会等待下一个消息到达后进行补全操作。其使用也比较简单,只需要在构造函数中指定每个消息的长度即可。这里需要注意的是:FixedLengthFrameDecoder只是一个解码器,Netty也只提供了一个解码器,这是因为对于解码器是需要等待下一个包的进行补全的,代码相对比较复杂,而对于编码器,用户可以自定义,因为编码器只需要将长度不足的部分进行补全即可。
//server端
public void bind(int port) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//这里将FixedLengthFrameDecoder添加到pipeline中,指定长度为20
pipeline.addLast(new FixedLengthFrameDecoder(20));
//将前一步解码得到的数据转成字符串
pipeline.addLast(new StringDecoder());
//这里FixedLengthFrameEncoder自定义,用户将长度不够指定长度的进行补全操作
pipeline.addLast(new FixedLengthFrameEncoder(20));
//数据处理
pipeline.addLast(new EchoServerHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
上面的pipeline中,对于入栈的数据,这里主要添加了FixedLengthFrameDecoder和StringDecoder,前面一个用于处理固定长度消息的粘包和拆包问题,第二个则是将处理之后的消息转换成字符串。最后有EchoServerHandler处理最终的数据,处理完毕后,将处理得到的数据交由FixedLengthFrameEncoder处理,自定义编码器,主要作用是将长度不足20的消息进行空格补齐操作。这里FixedLengthFrameEncoder继承了MessageToByteEncoder类,重写了ecoder()方法,在该方法中,主要将消息长度不足20的消息进行空格补全操作;
//自定义FixedLengthFrameEncoder编码器
public class FixedLengthFrameEncoder extends MessageToByteEncoder<String> {
private int length;
public FixedLengthFrameEncoder(int length) {
this.length = length;
}
@Override
protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out)
throws Exception {
//对于超过指定长度的消息,这里需要抛出异常信息
if (msg.length() > length) {
throw new UnsupportedOperationException("message length is too large, it is limited, length is: " + length);
}
//如果长度不够,需要进行补全操作
if (msg.length() < length) {
msg = addSpace(msg);
}
//将msg写出
ctx.writeAndFlush(Unpooled.wrappedBuffer(msg.getBytes()));
}
/**
* 补全操作
* @param message
* @return
*/
private String addSpace(String message) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < length - message.length(); i++) {
builder.append(" ");
}
return builder.toString();
}
}
ServerHandler最终处理数据的响应,然后发送响应数据给客户端:
public class EchoServerHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg)
throws Exception {
System.out.println("server receives message:" + msg.trim());
ctx.writeAndFlush("hello client");
}
}
//client客户端
public void connect(String host, int port) throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
/**
* 对服务器发送的消息进行粘包和拆包处理,由于服务器发送的消息已经进行空格补全操作
*/
ChannelPipeline pipeline = ch.pipeline();
//并且长度为20,因而这里指定的长度为20
pipeline.addLast(new FixedLengthFrameDecoder(20));
//将粘包和拆包处理得到的消息转换成字符串
pipeline.addLast(new StringDecoder());
//对客户端发送的数据进行补全操作,保证长度为20
pipeline.addLast(new FixedLengthFrameEncoder(20));
//客户端发送数据给服务器,并处理服务器响应的消息
pipeline.addLast(new EchoClientHandler());
}
});
ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
channelFuture.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
对于客户端而言,其消息的处理流程其实与服务器相似,对于入栈消息,需要进行粘包和拆包处理,然后将其转成字符串,对于出栈消息,则需要将长度不足20的消息进行空格补全操作。客户端与服务器端处理的主要区别在于handler不一样。
EchoClientHandler继承了SimpleChannelInboundHandler,重写了channelRead0()和channelActive()两个方法,这两个方法的主要作用在于:channelActive()会在客户端连接上服务器时执行,也就是说,其连上服务器之后就会往服务器发送消息。而channelRead0()主要是在服务器发送响应给客户端时执行,这里主要输出服务器响应消息,对于服务器而言,EchoServerHandler只重写了channelRead0()方法,这是因为服务器只需要等待客户端发送消息过来,然后在该方法中进行处理,处理完成后直接将响应发送给客户端。
public class EchoClientHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg)
throws Exception {
System.out.println("client receives message: " + msg.trim());
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush("hello server");
}
}