前言
本章学习Netty的入栈事件处理和解码处理。
-
ACCEPT入栈事件:创建Channel并注册到Selector
-
READ入栈事件:读取数据
-
实现自定义协议:自定义编解码器
-
ByteToMessageDecoder:如何处理TCP粘包拆包
-
解码异常的处理方式:Dubbo是如何处理解码异常的
一、回顾
1、ChannelPipeline
一个ChannelHandler封装为一个ChannelHandlerContext放入ChannelPipeline。
一个ChannelPipeline组装了ChannelHandlerContext形成的双向链表,头节点是HeadContext,尾节点是TailContext。
出栈事件从TailContext开始传播到HeadContext,入栈事件从HeadContext传播到TailContext。
2、出栈
出栈事件的传播方式,案例:服务端注册Channel后触发的read出栈事件(设置关注事件为ACCEPT)。
出栈事件是通过ChannelOutboundInvoker->ChannelOutboundHandler->Unsafe的方式传播的。
其中涉及的关键类如下:Channel(ChannelOutboundInvoker)->Pipeline(ChannelOutboundInvoker)->TailContext(ChannelOutboundInvoker)->ChannelOutboundHandlers->HeadContext(ChannelOutboundHandler) ->Unsafe。
二、ACCEPT
回顾NioEventLoop中processSelectedKey方法处理SelectionKey上激活的事件,如果有READ或ACCEPT事件发生,会调用Channel对应的Unsafe实现类处理。
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
// ...
try {
int readyOps = k.readyOps();
// ...
// READ或ACCEPT事件,调用unsafe的read方法
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}
对于服务端NioServerSocketChannel来说,这里会关注ACCEPT事件,对应的Unsafe实现类NioMessageUnsafe。抽象父类AbstractNioMessageChannel.NioMessageUnsafe实现read方法(注意区别于入栈的read事件,入栈的read事件是设置关注事件为READ或ACCEPT)。
private final class NioMessageUnsafe extends AbstractNioUnsafe {
// 保存NioServerSocketChannel
private final List<Object> readBuf = new ArrayList<Object>();
@Override
public void read() {
assert eventLoop().inEventLoop();
// Unsafe作为Channel的内部类,可以拿到Channel里的很多东西
final ChannelConfig config = config();
final ChannelPipeline pipeline = pipeline();
final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
allocHandle.reset(config);
boolean closed = false;
Throwable exception = null;
try {
// 1. 接收JDK SocketChannel,封装为Netty NioSocketChannel放入readBuf
try {
do {
int localRead = doReadMessages(readBuf);
if (localRead == 0) {
break;
}
if (localRead < 0) {
closed = true;
break;
}
allocHandle.incMessagesRead(localRead);
} while (allocHandle.continueReading());
} catch (Throwable t) {
exception = t;
}
// 2. 触发入栈事件 channelRead
int size = readBuf.size();
for (int i = 0; i < size; i ++) {
readPending = false;
pipeline.fireChannelRead(readBuf.get(i));
}
readBuf.clear();
allocHandle.readComplete();
// 3. 触发入栈事件 channelReadComplete
pipeline.fireChannelReadComplete();
// ...
} finally {
// ...
}
}
}
服务端ACCEPT事件处理分为三步:
- doReadMessages:接收JDK SocketChannel,封装为Netty NioSocketChannel放入readBuf。
- fireChannelRead:触发入栈事件channelRead,这里是重点。
- fireChannelReadComplete:当处理完所有连接,触发入栈事件channelReadComplete。
NioServerSocketChannel接收SocketChannel,并封装为NioSocketChannel放入结果集。
@Override
protected int doReadMessages(List<Object> buf) throws Exception {
SocketChannel ch = SocketUtils.accept(javaChannel());
if (ch != null) {
buf.add(new NioSocketChannel(this, ch));
return 1;
}
return 0;
}
接着Unsafe通过Pipeline传播channelRead入栈事件。Pipeline会将ChannelRead事件,从HeadContext开始传播到TailContext。注意HeadContext即是ChannelInboundHandler可以处理入栈事件,同时又是ChannelInboundInvoker可以通过fire方法向后传播入栈事件。
// DefaultChannelPipeline
@Override
public final ChannelPipeline fireChannelRead(Object msg) {
// 注意传入静态方法的是headContext实例
AbstractChannelHandlerContext.invokeChannelRead(head, msg);
return this;
}
// AbstractChannelHandlerContext
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
next.invokeChannelRead(m);
} else {
}
}
// AbstractChannelHandlerContext(HeadContext)获取对应Handler也是HeadContext
private void invokeChannelRead(Object msg) {
// 这里的handler获取的也是HeadContext自己
((ChannelInboundHandler) handler()).channelRead(this, msg);
}
// HeadContext作为ChannelInboundHandler
// 同时又是ChannelInboundInvoker
// 继续传播channelRead给下一个节点
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ctx.fireChannelRead(msg);
}
这里Server端的NioServerSocketChannel在初始化时,通过ChannelInitializer加入了ServerBootstrap.ServerBootstrapAcceptor这个特殊的InboundHandler。这个Netty提供的Handler会将客户端NioSocketChannel注册到child EventLoopGroup。这里所有的childXXX都来源于我们的配置(回顾第九章的ServerBootStrap和第10章的Server端启动初始化Channel)。另外这里就终止了channelRead向后传播的过程,因为没有调用ctx.fireChannelRead方法继续向后传播。
private static class ServerBootstrapAcceptor extends ChannelInboundHandlerAdapter {
private final EventLoopGroup childGroup;
private final ChannelHandler childHandler;
private final Entry<ChannelOption<?>, Object>[] childOptions;
private final Entry<AttributeKey<?>, Object>[] childAttrs;
private final Runnable enableAutoReadTask;
ServerBootstrapAcceptor(
final Channel channel, EventLoopGroup childGroup, ChannelHandler childHandler,
Entry<ChannelOption<?>, Object>[] childOptions, Entry<AttributeKey<?>, Object>[] childAttrs) {
this.childGroup = childGroup;
this.childHandler = childHandler;
this.childOptions = childOptions;
this.childAttrs = childAttrs;
}
// 服务端接收客户端Channel
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
final Channel child = (Channel) msg; // 一个强转,因为doReadMessages放入的是Channel实例
child.pipeline().addLast(childHandler); // 往往是配置的ChannelInitializer
setChannelOptions(child, childOptions, logger);
setAttributes(child, childAttrs);
childGroup.register(child).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
forceClose(child, future.cause());
}
}
});
}
}
这里childGroup.register方法就不说了,与第十章注册Channel部分基本一致,无非是从EventLoopGroup中选择一个EventLoop与Channel绑定。
三、READ
对于NioSocketChannel,关注READ事件。当通道上发生READ事件说明有字节可读,会调用NioByteUnsafe的read方法(注意区别于入栈的read事件,入栈的read事件是设置关注事件为READ或ACCEPT)。
// 读取对端传来的字节
@Override
public final void read() {
final ChannelConfig config = config();
final ChannelPipeline pipeline = pipeline();
final ByteBufAllocator allocator = config.getAllocator();
final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
allocHandle.reset(config);
ByteBuf byteBuf = null;
do {
// Handle根据情况,创建合适容量的ByteBuf,来承载Channel中的数据,每次容量可能发生变化
byteBuf = allocHandle.allocate(allocator);
// 从底层JDKChannel读取字节到byteBuf,记录读取字节数
allocHandle.lastBytesRead(doReadBytes(byteBuf));
if (allocHandle.lastBytesRead() <= 0) {
byteBuf.release();
byteBuf = null;
break;
}
// 累计循环次数
allocHandle.incMessagesRead(1);
readPending = false;
// 触发channelRead
pipeline.fireChannelRead(byteBuf);
byteBuf = null;
// 判断是否可以继续读取
} while (allocHandle.continueReading());
allocHandle.readComplete();
// 触发ChannelReadComplete
pipeline.fireChannelReadComplete();
// ...
}
首先NioSocketChannel#doReadBytes方法将数据读取到ByteBuf中。
@Override
protected int doReadBytes(ByteBuf byteBuf) throws Exception {
final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
allocHandle.attemptedBytesRead(byteBuf.writableBytes());
return byteBuf.writeBytes(javaChannel(), allocHandle.attemptedBytesRead());
}
接着和ACCEPT事件一样,也是触发channelRead入栈事件,区别是这里传递的是ByteBuf而不是NioSocketChannel。一般来说,channelRead事件要经过一个解码器再进入业务Handler,而业务Handler一般是在业务线程池中执行,防止阻塞IO线程(WorkerGroup中的EventLoop)。
另外,read方法循环处理对端发来的字节时,通过allocHandle控制循环次数,保证对于一个Channel不处理过多数据,防止因为某个客户端Channel阻塞其他客户端Channel的请求。
主要处理逻辑在DefaultMaxMessagesRecvByteBufAllocator.MaxMessageHandle抽象类中,它的实现类是AdaptiveRecvByteBufAllocator.HandleImpl。
// DefaultMaxMessagesRecvByteBufAllocator.MaxMessageHandle
// 判断read方法是否可以继续循环
// 入参=defaultMaybeMoreSupplier
@Override
public boolean continueReading(UncheckedBooleanSupplier maybeMoreDataSupplier) {
// 默认true,是否自动设置关注read事件
return config.isAutoRead() &&
// respectMaybeMoreData = true
// maybeMoreDataSupplier 根据当前循环的情况,判断是否可能有更多数据需要读取
(!respectMaybeMoreData || maybeMoreDataSupplier.get()) &&
// 循环次数 < 16
totalMessages < maxMessagePerRead &&
// 累计读字节数 > 0
totalBytesRead > 0;
}
// 判断是否可能有更多的字节需要读取
private final UncheckedBooleanSupplier defaultMaybeMoreSupplier = new UncheckedBooleanSupplier() {
@Override
public boolean get() {
// 尝试读取的字节 是否等于 实际从channel读取到的字节
// 如果等于表示可能还有新来的字节需要从channel读取
return attemptedBytesRead == lastBytesRead;
}
};
AdaptiveRecvByteBufAllocator.HandleImpl会根据实际读取的字节数,动态调整指标,控制下次allocate方法创建ByteBuf的容量。具体逻辑不展开,主要知道每次读取Channel字节数据多少,Netty做了动态控制。
- 底层Channel读取完成,触发lastBytesRead方法
- continueReading判断跳出循环,触发readComplete方法
- 通道无数据可读时跳出循环,触发readComplete方法
private final class HandleImpl extends MaxMessageHandle {
private final int minIndex;
private final int maxIndex;
private int index;
private int nextReceiveBufferSize;
private boolean decreaseNow;
HandleImpl(int minIndex, int maxIndex, int initial) {
this.minIndex = minIndex;
this.maxIndex = maxIndex;
index = getSizeTableIndex(initial);
nextReceiveBufferSize = SIZE_TABLE[index];
}
// 猜测本次需要allocate多大的内存来承载Channel中的字节
@Override
public int guess() {
return nextReceiveBufferSize;
}
// 读取通道数据后触发,入参是Channel本次读取的字节数量
@Override
public void lastBytesRead(int bytes) {
if (bytes == attemptedBytesRead()) {
record(bytes);
}
super.lastBytesRead(bytes);
}
// 本次通道读取循环退出调用
@Override
public void readComplete() {
record(totalBytesRead());
}
// 根据实际读取的字节数,动态调整指标,控制guess方法返回下次创建ByteBuf的容量
private void record(int actualReadBytes) {
if (actualReadBytes <= SIZE_TABLE[max(0, index - INDEX_DECREMENT)]) {
if (decreaseNow) {
index = max(index - INDEX_DECREMENT, minIndex);
nextReceiveBufferSize = SIZE_TABLE[index];
decreaseNow = false;
} else {
decreaseNow = true;
}
} else if (actualReadBytes >= nextReceiveBufferSize) {
index = min(index + INDEX_INCREMENT, maxIndex);
nextReceiveBufferSize = SIZE_TABLE[index];
decreaseNow = false;
}
}
}
四、解码如何解决粘包拆包
READ入栈事件往往需要一个解码Handler,将ByteBuf转换为业务报文。这一章看看如何使用Netty提供的解码器框架,用户代码如何配合Netty的解码框架解决粘包拆包问题。
1、如何使用ByteToMessageDecoder实现自己的解码器
公共类
首先,先定义一个协议。整个数据报分为header和body两部分,XHeader定义如下。
定义两类报文类型:XRequest是请求报文,XResponse是响应报文。
XDecoder继承Netty的ByteToMessageDecoder,是一个InboundChannelHandler,负责处理自定义协议解码。
public class XDecoder extends ByteToMessageDecoder {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
// 1. 如果字节不足头部大小,直接返回
if (in.readableBytes() < XHeader.HEADER_SIZE) {
System.out.println("receive part header, header size = " + in.readableBytes());
return;
}
// 2. 头部解析
int readerIndex = in.readerIndex(); // 先将读索引保存下来,之后可以恢复
if (in.readShort() != XHeader.MAGIC) {
System.out.println("MAGIC ERROR, close channel " + ctx.channel().remoteAddress());
ctx.channel().close();
return;
// throw new IllegalArgumentException("MAGIC ERROR");
}
if (in.readByte() != XHeader.VERSION_1) {
System.out.println("VERSION ERROR, close channel " + ctx.channel().remoteAddress());
ctx.channel().close();
return;
// throw new IllegalArgumentException("VERSION ERROR");
}
byte type = in.readByte();
int length = in.readInt();
// 3. 剩余可读字节数 不足 头部中的长度标识,恢复读索引返回
if (in.readableBytes() < length) {
System.out.println("receive part data, length in header = " + length + ", data size = " + in.readableBytes());
in.readerIndex(readerIndex);
return;
}
// 4. 解析body
byte[] data = new byte[length];
in.readBytes(data);
if (type == XHeader.REQUEST) {
XRequest xRequest = objectMapper.readValue(data, XRequest.class);
out.add(xRequest);
} else if (type == XHeader.RESPONSE) {
XResponse xResponse = objectMapper.readValue(data, XResponse.class);
out.add(xResponse);
}
// 未知type会继续下一次报文解析
}
}
服务端
XServer是服务端,Pipeline加入了通用解码器、Response编码器、Request业务处理器。
public class XServer {
public static void main(String[] args) throws InterruptedException {
ServerBootstrap bootstrap = new ServerBootstrap();
EventLoopGroup boss = new NioEventLoopGroup(1);
EventLoopGroup worker = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors());
try {
bootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler())
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast("decoder", new XDecoder()) // 解码器
.addLast("encoder", new XResponseEncoder()) // 编码器
.addLast("xreq", new XRequestHandler()); // 业务处理器
}
});
ChannelFuture future = bootstrap.bind(9999).sync();
System.out.println("server start at port 9999");
future.channel().closeFuture().sync();
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
// Xresponse编码器
public class XResponseEncoder extends MessageToByteEncoder<XResponse> {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void encode(ChannelHandlerContext ctx, XResponse msg, ByteBuf out) throws Exception {
out.writeShort(XHeader.MAGIC);
out.writeByte(XHeader.VERSION_1);
out.writeByte(XHeader.RESPONSE);
byte[] bytes = objectMapper.writeValueAsBytes(msg);
out.writeInt(bytes.length);
out.writeBytes(bytes);
}
}
// 业务handler 处理客户端请求,这里将客户端的data转换为大写返回
public class XRequestHandler extends SimpleChannelInboundHandler<XRequest> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, XRequest msg) throws Exception {
XResponse response = new XResponse();
response.code = "0";
response.msg = "success";
response.data = msg.data.toUpperCase();
System.out.println(new ObjectMapper().writeValueAsString(msg));
ctx.writeAndFlush(response);
}
}
客户端
XClient是客户端,Pipeline加入了通用解码器、Request编码器、Response业务处理器
public class XClient {
private static final int PORT = 9999;
public static void main(String[] args) throws InterruptedException {
ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID);
Bootstrap bootstrap = new Bootstrap();
NioEventLoopGroup group = new NioEventLoopGroup(1);
try {
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline()
.addLast("decoder", new XDecoder()) // 解码器
.addLast("encoder", new XRequestEncoder()) // 请求编码器
.addLast("xres", new XResponseHandler()); // 响应处理器
}
});
ChannelFuture future = bootstrap.connect("127.0.0.1", PORT).sync();
assert future.isSuccess();
System.out.println("connect success");
// 1. 正常调用服务端
writeSuccess(future.channel());
} finally {
group.shutdownGracefully();
}
}
// 循环给服务端发送{"data": "hello"}
private static void writeSuccess(Channel channel) throws InterruptedException {
XRequest request = new XRequest();
request.data = "hello";
while (channel.isActive()) {
channel.writeAndFlush(request);
Thread.sleep(1000);
}
}
}
// XRequest编码器
public class XRequestEncoder extends MessageToByteEncoder<XRequest> {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void encode(ChannelHandlerContext ctx, XRequest msg, ByteBuf out) throws Exception {
out.writeShort(XHeader.MAGIC);
out.writeByte(XHeader.VERSION_1);
out.writeByte(XHeader.REQUEST);
byte[] bytes = objectMapper.writeValueAsBytes(msg);
out.writeInt(bytes.length);
out.writeBytes(bytes);
}
}
// XResponse业务处理器
public class XResponseHandler extends SimpleChannelInboundHandler<XResponse> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, XResponse msg) throws Exception {
System.out.println(new ObjectMapper().writeValueAsString(msg));
}
}
2、ByteToMessageDecoder
ByteToMessageDecoder是Netty提供给用的抽象解码器实现类,用户只要实现抽象decode方法即可实现解码。ByteToMessageDecoder的优势就是协助用户解决了TCP粘包拆包问题,只不过需要用户小心地实现decode方法。
先来看一下ByteToMessageDecoder的成员变量,可以看到其内部维护了一个累积ByteBuf,用于对同一个Channel的字节数据累积。
public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter {
// 累积ByteBuf
ByteBuf cumulation;
// 累加器
private Cumulator cumulator = MERGE_CUMULATOR;
// 每次ChannelRead是否仅仅处理一次ByteBuf(调用一次用户的decode方法)默认false
private boolean singleDecode;
// 是否是首次累加
private boolean first;
}
当READ事件被触发后,NioSocketChannel会读取JDKChannel中的字节数据到一个ByteBuf中,这个ByteBuf就是ByteToMessageDecoder的入参msg。从整体上看ByteToMessageDecoder的channelRead方法分为几步:
- Cumulator累积msg到cumulation
- 调用用户的decode方法
- finally资源释放,将decode完成的数据通过channelRead向后传播
// ByteToMessageDecoder
// 累积ByteBuf
ByteBuf cumulation;
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
// 认为是个ArrayList,只不过用到了对象池和FastThreadLocal技术
CodecOutputList out = CodecOutputList.newInstance();
try {
// 累积msg中的可读字节到cumulation,这里会释放msg
first = cumulation == null;
cumulation = cumulator.cumulate(ctx.alloc(),
first ? Unpooled.EMPTY_BUFFER : cumulation, (ByteBuf) msg);
// 调用用户的decode方法...
callDecode(ctx, cumulation, out);
} catch (DecoderException e) {
throw e;
} catch (Exception e) {
throw new DecoderException(e);
} finally {
if (cumulation != null && !cumulation.isReadable()) {
// 如果cumulation已经被读完了,释放并设置为空
numReads = 0;
cumulation.release();
cumulation = null;
} else if (++numReads >= discardAfterReads) {
// 防止cumulation过度扩容膨胀
// 当channelRead处理过16次以后,丢弃累加器中的已读字节
numReads = 0;
discardSomeReadBytes();
}
// 将解码成功的out列表里的报文向后传播
int size = out.size();
firedChannelRead |= out.insertSinceRecycled();
fireChannelRead(ctx, out, size);
}
} else {
ctx.fireChannelRead(msg);
}
}
Cumulator
/**
* Cumulate {@link ByteBuf}s.
*/
public interface Cumulator {
/**
* Cumulate the given {@link ByteBuf}s and return the {@link ByteBuf} that holds the cumulated bytes.
* The implementation is responsible to correctly handle the life-cycle of the given {@link ByteBuf}s and so
* call {@link ByteBuf#release()} if a {@link ByteBuf} is fully consumed.
*/
ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in);
}
Cumulator是实现粘包拆包的一个关键接口,负责将输入buf累加到一个累积buf中。根据javadoc看到,如果输入buf读取完毕,应当在这里release释放资源。
ByteToMessageDecoder提供了两种实现,一种是基于内存拷贝的,一种是基于CompositeByteBuf的(Netty零拷贝其中之一)。
MERGE_CUMULATOR即为默认的累加器实现,基于内存拷贝的方式将in累计到cumulation中。
/**
* Cumulate {@link ByteBuf}s by merge them into one {@link ByteBuf}'s, using memory copies.
*/
public static final Cumulator MERGE_CUMULATOR = new Cumulator() {
@Override
public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
// 累加buf没数据可读 且 in是一个非composite的bytebuf,直接使用in作为累加buffer
if (!cumulation.isReadable() && in.isContiguous()) {
cumulation.release();
return in;
}
try {
final int required = in.readableBytes();
// 如果累加buf的可用空间不够了,执行扩容
if (required > cumulation.maxWritableBytes() ||
(required > cumulation.maxFastWritableBytes() && cumulation.refCnt() > 1) ||
cumulation.isReadOnly()) {
return expandCumulation(alloc, cumulation, in);
}
// 将in的可读数据,都写入累加buf
cumulation.writeBytes(in, in.readerIndex(), required);
in.readerIndex(in.writerIndex());
return cumulation;
} finally {
// in写入累加器后,就可以释放了
// (io.netty.channel.nio.AbstractNioByteChannel.NioByteUnsafe.read创建的ByteBuf)
in.release();
}
}
};
COMPOSITE_CUMULATOR是基于CompositeByteBuf的零拷贝累加器,根据javadoc描述,这种累加器的劣势在于用户实现decode逻辑较为复杂,且使用这种累加器实现的Decoder速度较慢。
/**
* Cumulate {@link ByteBuf}s by add them to a {@link CompositeByteBuf} and so do no memory copy whenever possible.
* Be aware that {@link CompositeByteBuf} use a more complex indexing implementation so depending on your use-case
* and the decoder implementation this may be slower then just use the {@link #MERGE_CUMULATOR}.
*/
public static final Cumulator COMPOSITE_CUMULATOR = new Cumulator() {
@Override
public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
if (!cumulation.isReadable()) {
cumulation.release();
return in;
}
CompositeByteBuf composite = null;
try {
if (cumulation instanceof CompositeByteBuf && cumulation.refCnt() == 1) {
composite = (CompositeByteBuf) cumulation;
if (composite.writerIndex() != composite.capacity()) {
composite.capacity(composite.writerIndex());
}
} else {
composite = alloc.compositeBuffer(Integer.MAX_VALUE).addFlattenedComponents(true, cumulation);
}
composite.addFlattenedComponents(true, in);
in = null;
return composite;
} finally {
if (in != null) {
in.release();
if (composite != null && composite != cumulation) {
composite.release();
}
}
}
}
};
decode
callDecode方法负责循环执行用户的decode方法,注意入参in是外部传入的累加buf,out集合存放用户解码后的数据。
/**
* Called once data should be decoded from the given {@link ByteBuf}. This method will call
* {@link #decode(ChannelHandlerContext, ByteBuf, List)} as long as decoding should take place.
*
* @param ctx the {@link ChannelHandlerContext} which this {@link ByteToMessageDecoder} belongs to
* @param in the {@link ByteBuf} from which to read data
* @param out the {@link List} to which decoded messages should be added
*/
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
try {
while (in.isReadable()) {
int outSize = out.size();
if (outSize > 0) {
fireChannelRead(ctx, out, outSize);
out.clear();
// ...
outSize = 0;
}
int oldInputLength = in.readableBytes();
// 执行用户的decode方法
// 这里源码是decodeRemovalReentryProtection方法
// 简化展示为decode不影响主要逻辑
decode(ctx, in, out);
if (ctx.isRemoved()) {
break;
}
// out列表没有新增元素
if (outSize == out.size()) {
if (oldInputLength == in.readableBytes()) {
// 可读字节数没有发生变化,中断本次解码
break;
} else {
// 可读字节数发生变化,继续解码
continue;
}
}
// out列表新增元素了,但是byteBuf的可读字节数没有变化(writeIndex - readIndex)
if (oldInputLength == in.readableBytes()) {
throw new DecoderException(...);
}
// out列表新增元素了,byteBuf的可读字节数也发生变化了,继续下一次循环
// 是否仅读取一次就结束循环(默认false)
if (isSingleDecode()) {
break;
}
}
} catch (DecoderException e) {
throw e;
} catch (Exception cause) {
throw new DecoderException(cause);
}
}
callDecode循环控制条件依赖于框架与用户的约定,循环是否中断取决于用户decode如何操作累加buf(in)和解码结果集(out)。
根据上面的分析,用户的decode方法要如何解决粘包拆包问题呢?
解决粘包问题,需要让循环继续循环,保证累加Buffer可读字节发生变化且结果集大小变化,即解码累加buffer的一个数据包放入out结果集;当然自己在decode方法内部循环解决粘包问题也是可以的。
解决拆包问题,需要让循环退出,保证可读字节不变,结果集大小不变,让循环直接break,即如果用户decode按照自定义协议不能读取到完整报文,就恢复buffer读下标,直接返回。
例如XDecoder自定义协议的解码实现。
拆包问题:当读取字节不足时,保证读索引不变化,不操作结果集out直接返回即可,外部解码循环会中断。
粘包问题:每次decode只处理一个数据包,让Netty框架外部循环调用decode方法。
public class XDecoder extends ByteToMessageDecoder {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
// 1. 如果字节不足头部大小,直接返回
if (in.readableBytes() < XHeader.HEADER_SIZE) {
return;
}
// 2. 头部解析
int readerIndex = in.readerIndex(); // 先将读索引保存下来,之后可以恢复
if (in.readShort() != XHeader.MAGIC) {
// ...
// 抛出异常?关闭通道?恢复读索引返回?
}
if (in.readByte() != XHeader.VERSION_1) {
// ...
}
byte type = in.readByte();
int length = in.readInt();
// 3. 剩余可读字节数 不足 头部中的长度标识,恢复读索引返回
if (in.readableBytes() < length) {
in.readerIndex(readerIndex);
return;
}
// 4. 解析body
// ...
}
}
五、解码异常如何处理
自定义协议解码,如果对端传过来的数据报无法通过自定义协议的校验,应该如何处理?
int readerIndex = in.readerIndex();
if (in.readShort() != XHeader.MAGIC) {
// ...
// 抛出异常?关闭通道?恢复读索引返回?
}
方案一:恢复读索引返回?
恢复读索引返回,不操作out结果集,将非法报文将始终保存在累加buffer中。内存溢出?
方案二:抛出异常?
相当于读索引不恢复,跳过已读数据,退出本次解码循环,等待下一次channelRead事件。但是下一次读到的可能还是异常数据?
方案三:关闭通道?
貌似没什么风险,既然客户端的数据报是非法的。业务是否允许?
来看看Dubbo是如何处理的,客户端用XClient连接20880端口,直接发送ByteBuf;服务端用dubbo-2.7.9版本的demo。
package org.apache.dubbo.demo.provider;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Application {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/dubbo-provider.xml");
context.start();
System.in.read();
}
}
Dubbo编码规则如下:
场景一:Dubbo发送魔数校验不通过的报文
客户端如下。
private static void writeDubboError(Channel channel) throws InterruptedException {
final long start = System.currentTimeMillis();
channel.closeFuture().addListener(new GenericFutureListener<Future<? super Void>>() {
@Override
public void operationComplete(Future<? super Void> future) throws Exception {
System.out.println("dubbo channel关闭超时时间:" + (System.currentTimeMillis() - start) / 1000);
}
});
// 循环发送0给dubbo
while (channel.isActive()) {
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer().writeBytes(new byte[1]);
channel.writeAndFlush(byteBuf);
Thread.sleep(100);
}
}
先看测试结果,Dubbo服务端的累加Buffer在持续增长。
客户端在持续发送了180s数据后,channel被关闭了。
首先Dubbo会校验前两个字节的魔数。
// ExchangeCodec
@Override
protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] header) throws IOException {
// 校验魔数0xdabb,如果魔数校验不通过走父类decode
if (readable > 0 && header[0] != MAGIC_HIGH
|| readable > 1 && header[1] != MAGIC_LOW) {
int length = header.length;
// 将可读字节都读入header
if (header.length < readable) {
header = Bytes.copyOf(header, readable);
buffer.readBytes(header, length, readable - length);
}
// 循环判断直到找到magic_high和magic_low
for (int i = 1; i < header.length - 1; i++) {
if (header[i] == MAGIC_HIGH && header[i + 1] == MAGIC_LOW) {
buffer.readerIndex(buffer.readerIndex() - header.length + i);
header = Bytes.copyOf(header, i);
break;
}
}
// 调用父类decode
return super.decode(channel, buffer, readable, header);
}
}
如果魔数校验不通过,走父类TelnetCodec判断能否解码,最终TelnetCodec判定没有收到回车符,返回DecodeResult.NEED_MORE_INPUT。
// TelnetCodec
protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] message) throws IOException {
// ...省略其他
byte[] enter = null;
for (Object command : ENTER) {
if (endsWith(message, (byte[]) command)) {
enter = (byte[]) command;
break;
}
}
if (enter == null) {
return DecodeResult.NEED_MORE_INPUT;
}
}
最终,最外部的ByteToMessageDecoder实现类InternalDecoder,选择重置累加Buffer的读索引,等待下一次channelRead(方案一),这就是累加Buffer在持续增长的原因。同时看到Dubbo是在用户decode实现里循环解码,并不依赖ByteToMessageDecoder的循环解码。
private class InternalDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf input, List<Object> out) throws Exception {
// 封装input到Dubbo自己的ChannelBuffer中
ChannelBuffer message = new NettyBackedChannelBuffer(input);
// 封装NettyChannel
NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
do {
int saveReaderIndex = message.readerIndex();
Object msg = codec.decode(channel, message);
if (msg == Codec2.DecodeResult.NEED_MORE_INPUT) {
// 重置读索引并退出
message.readerIndex(saveReaderIndex);
break;
} else {
// 加入结果集
if (msg != null) {
out.add(msg);
}
}
} while (message.readable());
}
}
此外,客户端在发送了180s非法请求报文之后,channel连接就断开了。原因是Dubbo使用了Netty提供的IdleStateHandler,默认当客户端连接空闲超过180s后就会断开连接(注意Handler顺序,空闲检测Handler在解码器之前)。
NettyServer#doOpen方法,启动Netty服务端。
protected void doOpen() throws Throwable {
bootstrap.group(bossGroup, workerGroup)
// ...
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 空闲连接超时时长,默认180s
int idleTimeout = UrlUtils.getIdleTimeout(getUrl());
// 编解码器
NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this);
if (getUrl().getParameter(SSL_ENABLED_KEY, false)) {
ch.pipeline().addLast("negotiation",
SslHandlerInitializer.sslServerHandler(getUrl(), nettyServerHandler));
}
ch.pipeline()
.addLast("decoder", adapter.getDecoder())
.addLast("encoder", adapter.getEncoder())
// 180s无读写,发送IdleStateEvent
.addLast("server-idle-handler", new IdleStateHandler(0, 0, idleTimeout, MILLISECONDS))
.addLast("handler", nettyServerHandler);
}
});
//...
}
NettyServerHandler处理IdleStateEvent,关闭Channel。
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
channel.close();
// ...
}
}
场景二:Dubbo发送不存在的序列化方式
根据Dubbo解码规则,发送不存在的序列化方式(第3个字节的低5位)0给Dubbo服务端。
private static void writeDubboError2(Channel channel) throws InterruptedException {
final long start = System.currentTimeMillis();
channel.closeFuture().addListener(new GenericFutureListener<Future<? super Void>>() {
@Override
public void operationComplete(Future<? super Void> future) throws Exception {
System.out.println("dubbo channel关闭超时时间:" + (System.currentTimeMillis() - start) / 1000);
}
});
String body = "hello";
while (channel.isActive()) {
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer()
.writeShort(MAGIC) // 2字节 魔数
.writeByte(0) // 1字节 序列化方式/事件类型/请求类型/twoway...
.writeByte(1) // 1字节 状态
.writeLong(1L) // 8字节 id
.writeInt(body.length()) // 4字节 body长度
.writeBytes(body.getBytes()); // body
channel.writeAndFlush(buf);
Thread.sleep(1000);
}
}
Dubbo服务端异常日志,从日志中看到这里跳过了5个字节:
[10/04/21 11:35:36:480 CST] NettyServerWorker-5-1 WARN dubbo.DubboCodec: [DUBBO] Decode response failed: Unrecognized serialize type from consumer: 0, dubbo version: , current host: 192.168.0.104
java.io.IOException: Unrecognized serialize type from consumer: 0
at org.apache.dubbo.remoting.transport.CodecSupport.getSerialization(CodecSupport.java:89)
at org.apache.dubbo.remoting.transport.CodecSupport.deserialize(CodecSupport.java:95)
at org.apache.dubbo.rpc.protocol.dubbo.DubboCodec.decodeBody(DubboCodec.java:108)
at org.apache.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:132)
at org.apache.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:90)
at org.apache.dubbo.rpc.protocol.dubbo.DubboCountCodec.decode(DubboCountCodec.java:48)
at
[10/04/21 11:35:36:481 CST] NettyServerWorker-5-1 WARN codec.ExchangeCodec: [DUBBO] Skip input stream 5, dubbo version: , current host: 192.168.0.104
Dubbo将Buffer封装为InputStream解析报文,解码异常内部会catch住,在最外部的decode方法的finally块中跳过整段数据报。
// ExchangeCodec
@Override
protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] header) throws IOException {
// 校验魔数0xdabb,如果魔数校验不通过走父类decode
if (readable > 0 && header[0] != MAGIC_HIGH
|| readable > 1 && header[1] != MAGIC_LOW) {
// ...
}
// 可读字节不足16,收到半包,直接返回
if (readable < HEADER_LENGTH) {
return DecodeResult.NEED_MORE_INPUT;
}
// body长度不足,收到半包,直接返回
int len = Bytes.bytes2int(header, 12);
int tt = len + HEADER_LENGTH;
if (readable < tt) {
return DecodeResult.NEED_MORE_INPUT;
}
// buffer转换为InputStream,底层还是读写buffer,可读字节为len
ChannelBufferInputStream is = new ChannelBufferInputStream(buffer, len);
try {
return decodeBody(channel, is, header);
} finally {
// 如果decodeBody处理完了以后,还有可读字节,全部跳过
if (is.available() > 0) {
try {
if (logger.isWarnEnabled()) {
logger.warn("Skip input stream " + is.available());
}
StreamUtils.skipUnusedStream(is);
} catch (IOException e) {
logger.warn(e.getMessage(), e);
}
}
}
}
// DubboCodec
protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {
byte flag = header[2], proto = (byte) (flag & SERIALIZATION_MASK);
// get request id.
long id = Bytes.bytes2long(header, 4);
if ((flag & FLAG_REQUEST) == 0) {
// decode response.
Response res = new Response(id);
if ((flag & FLAG_EVENT) != 0) {
res.setEvent(true);
}
// get status.
byte status = header[3];
res.setStatus(status);
try {
if (status == Response.OK) {
// ...
} else {
// 走这里根据proto反序列化buffer中的数据,发生异常
ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
res.setErrorMessage(in.readUTF());
}
} catch (Throwable t) {
if (log.isWarnEnabled()) {
log.warn("Decode response failed: " + t.getMessage(), t);
}
res.setStatus(Response.CLIENT_ERROR);
res.setErrorMessage(StringUtils.toString(t));
}
return res;
} else {
// decode request.
}
}
这个场景与前一个不同点是,这里会返回一个异常的业务Response对象,最终会加入结果集,所以不会触发空闲连接检测,不会关闭channel。
// DubboCountCodec
@Override
public Object decode(Channel channel, ChannelBuffer buffer) throws IOException {
int save = buffer.readerIndex();
MultiMessage result = MultiMessage.create();
do {
// 这里正常返回解析后的对象
Object obj = codec.decode(channel, buffer);
if (Codec2.DecodeResult.NEED_MORE_INPUT == obj) {
buffer.readerIndex(save);
break;
} else {
result.addMessage(obj);
logMessageLength(obj, buffer.readerIndex() - save);
save = buffer.readerIndex();
}
} while (true);
if (result.isEmpty()) {
return Codec2.DecodeResult.NEED_MORE_INPUT;
}
if (result.size() == 1) {
return result.get(0);
}
return result;
}
总结
- ACCEPT入栈事件:服务端NioServerSocketChannel关注ACCEPT事件,当通道中有accept事件触发时,会调用NioMessageUnsafe的read方法处理,接收NioSocketChannel。虽然处理的是SelectionKey上的accept事件,但是Unsafe会通过Pipeline触发channelRead。Netty的ServerBootstrap.ServerBootstrapAcceptor这个入栈处理器channelRead方法,将客户端NioSocketChannel注册并初始化。
- READ入栈事件:NioSocketChannel关注READ事件,当通道中有READ事件触发时,会调用NioByteUnsafe从Channel中读取ByteBuf,并调用Pipeline触发channelRead。channelRead通常会经过解码Handler和业务Handler。
- 实现自定义协议:通过继承ByteToMessageDecoder实现解码,继承MessageToByteEncoder实现编码。
- ByteToMessageDecoder:Netty提供的解码抽象类,其中解码decode方法需要用户配合Netty框架实现。入参ByteBuf是一个会持续累加的buffer,入参out集合用于存储解码成功的业务对象。此外,decode方法通过控制累加buffer和out集合,可以解决粘包拆包问题。
- Dubbo解码异常的处理方式:
- 魔数校验不通过,持续累积客户端的数据到累加Buffer中,直到180s触发空闲连接检测,关闭channel。
- 序列化方式不存在,跳过整个数据报并将异常业务对象放入解码结果集。