前言
netty原始碼分析之pipeline(一)中,我們已經瞭解了pipeline在netty中所處的角色,像是一條流水線,控制著位元組流的讀寫,本文,我們在這個基礎上繼續深挖pipeline在事件傳播,異常傳播等方面的細節
主要內容
接下來,本文分以下幾個部分進行
- netty中的Unsafe到底是幹什麼的
- pipeline中的head
- pipeline中的inBound事件傳播
- pipeline中的tail
- pipeline中的outBound事件傳播
- pipeline 中異常的傳播
##Unsafe到底是幹什麼的
之所以Unsafe放到pipeline中講,是因為unsafe和pipeline密切相關,pipeline中的有關io的操作最終都是落地到unsafe,所以,有必要先講講unsafe
初識Unsafe
顧名思義,unsafe是不安全的意思,就是告訴你不要在應用程式裡面直接使用Unsafe以及他的衍生類物件。
netty官方的解釋如下
Unsafe operations that should never be called from user-code. These methods are only provided to implement the actual transport, and must be invoked from an I/O thread
Unsafe 在Channel定義,屬於Channel的內部類,表明Unsafe和Channel密切相關
下面是unsafe介面的所有方法
interface Unsafe {
RecvByteBufAllocator.Handle recvBufAllocHandle();
SocketAddress localAddress();
SocketAddress remoteAddress();
void register(EventLoop eventLoop, ChannelPromise promise);
void bind(SocketAddress localAddress, ChannelPromise promise);
void connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise);
void disconnect(ChannelPromise promise);
void close(ChannelPromise promise);
void closeForcibly();
void beginRead();
void write(Object msg, ChannelPromise promise);
void flush();
ChannelPromise voidPromise();
ChannelOutboundBuffer outboundBuffer();
}
複製程式碼
按功能可以分為分配記憶體,Socket四元組資訊,註冊事件迴圈,繫結網路卡埠,Socket的連線和關閉,Socket的讀寫,看的出來,這些操作都是和jdk底層相關
Unsafe 繼承結構
NioUnsafe
在 Unsafe
基礎上增加了以下幾個介面
public interface NioUnsafe extends Unsafe {
SelectableChannel ch();
void finishConnect();
void read();
void forceFlush();
}
複製程式碼
從增加的介面以及類名上來看,NioUnsafe
增加了可以訪問底層jdk的SelectableChannel
的功能,定義了從SelectableChannel
讀取資料的read
方法
AbstractUnsafe
實現了大部分Unsafe
的功能
AbstractNioUnsafe
主要是通過代理到其外部類AbstractNioChannel
拿到了與jdk nio相關的一些資訊,比如SelectableChannel
,SelectionKey
等等
NioSocketChannelUnsafe
和NioByteUnsafe
放到一起講,其實現了IO的基本操作,讀,和寫,這些操作都與jdk底層相關
NioMessageUnsafe
和 NioByteUnsafe
是處在同一層次的抽象,netty將一個新連線的建立也當作一個io操作來處理,這裡的Message的含義我們可以當作是一個SelectableChannel
,讀的意思就是accept一個SelectableChannel
,寫的意思是針對一些無連線的協議,比如UDP來操作的,我們先不用關注
Unsafe的分類
從以上繼承結構來看,我們可以總結出兩種型別的Unsafe分類,一個是與連線的位元組資料讀寫相關的NioByteUnsafe
,一個是與新連線建立操作相關的NioMessageUnsafe
NioByteUnsafe
中的讀:委託到外部類NioSocketChannel
protected int doReadBytes(ByteBuf byteBuf) throws Exception {
final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
allocHandle.attemptedBytesRead(byteBuf.writableBytes());
return byteBuf.writeBytes(javaChannel(), allocHandle.attemptedBytesRead());
}
複製程式碼
最後一行已經與jdk底層以及netty中的ByteBuf相關,將jdk的 SelectableChannel
的位元組資料讀取到netty的ByteBuf
中
NioMessageUnsafe
中的讀:委託到外部類NioSocketChannel
protected int doReadMessages(List<Object> buf) throws Exception {
SocketChannel ch = javaChannel().accept();
if (ch != null) {
buf.add(new NioSocketChannel(this, ch));
return 1;
}
return 0;
}
複製程式碼
NioMessageUnsafe
的讀操作很簡單,就是呼叫jdk的accept()
方法,新建立一條連線
NioByteUnsafe
中的寫:委託到外部類NioSocketChannel
@Override
protected int doWriteBytes(ByteBuf buf) throws Exception {
final int expectedWrittenBytes = buf.readableBytes();
return buf.readBytes(javaChannel(), expectedWrittenBytes);
}
複製程式碼
最後一行已經與jdk底層以及netty中的ByteBuf相關,將netty的ByteBuf
中的位元組資料寫到jdk的 SelectableChannel
中
NioMessageUnsafe
的寫,在tcp協議層面我們基本不會涉及,暫時忽略,udp協議的讀者可以自己去研究一番~
關於Unsafe
我們就先了解這麼多
##pipeline中的head
netty原始碼分析之pipeline(一)中,我們瞭解到head節點在pipeline中第一個處理IO事件,新連線接入和讀事件在reactor執行緒的第二個步驟被檢測到
NioEventLoop
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
//新連線的已準備接入或者已存在的連線有資料可讀
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
}
複製程式碼
讀操作直接依賴到unsafe來進行,新連線的接入在netty原始碼分析之新連線接入全解析中已詳細闡述,這裡不再描述,下面將重點放到連線位元組資料流的讀寫
NioByteUnsafe
@Override
public final void read() {
final ChannelConfig config = config();
final ChannelPipeline pipeline = pipeline();
// 建立ByteBuf分配器
final ByteBufAllocator allocator = config.getAllocator();
final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
allocHandle.reset(config);
ByteBuf byteBuf = null;
do {
// 分配一個ByteBuf
byteBuf = allocHandle.allocate(allocator);
// 將資料讀取到分配的ByteBuf中去
allocHandle.lastBytesRead(doReadBytes(byteBuf));
if (allocHandle.lastBytesRead() <= 0) {
byteBuf.release();
byteBuf = null;
close = allocHandle.lastBytesRead() < 0;
break;
}
// 觸發事件,將會引發pipeline的讀事件傳播
pipeline.fireChannelRead(byteBuf);
byteBuf = null;
} while (allocHandle.continueReading());
pipeline.fireChannelReadComplete();
}
複製程式碼
同樣,我抽出了核心程式碼,細枝末節先剪去,NioByteUnsafe
要做的事情可以簡單地分為以下幾個步驟
- 拿到Channel的config之後拿到ByteBuf分配器,用分配器來分配一個ByteBuf,ByteBuf是netty裡面的位元組資料載體,後面讀取的資料都讀到這個物件裡面
- 將Channel中的資料讀取到ByteBuf
- 資料讀完之後,呼叫
pipeline.fireChannelRead(byteBuf);
從head節點開始傳播至整個pipeline
這裡,我們的重點其實就是 pipeline.fireChannelRead(byteBuf);
DefaultChannelPipeline
final AbstractChannelHandlerContext head;
//...
head = new HeadContext(this);
public final ChannelPipeline fireChannelRead(Object msg) {
AbstractChannelHandlerContext.invokeChannelRead(head, msg);
return this;
}
複製程式碼
結合這幅圖
可以看到,資料從head節點開始流入,在進行下一步之前,我們先把head節點的功能過一遍
HeadContext
final class HeadContext extends AbstractChannelHandlerContext
implements ChannelOutboundHandler, ChannelInboundHandler {
private final Unsafe unsafe;
HeadContext(DefaultChannelPipeline pipeline) {
super(pipeline, null, HEAD_NAME, false, true);
unsafe = pipeline.channel().unsafe();
setAddComplete();
}
@Override
public ChannelHandler handler() {
return this;
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// NOOP
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
// NOOP
}
@Override
public void bind(
ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise)
throws Exception {
unsafe.bind(localAddress, promise);
}
@Override
public void connect(
ChannelHandlerContext ctx,
SocketAddress remoteAddress, SocketAddress localAddress,
ChannelPromise promise) throws Exception {
unsafe.connect(remoteAddress, localAddress, promise);
}
@Override
public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
unsafe.disconnect(promise);
}
@Override
public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
unsafe.close(promise);
}
@Override
public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
unsafe.deregister(promise);
}
@Override
public void read(ChannelHandlerContext ctx) {
unsafe.beginRead();
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
unsafe.write(msg, promise);
}
@Override
public void flush(ChannelHandlerContext ctx) throws Exception {
unsafe.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.fireExceptionCaught(cause);
}
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
invokeHandlerAddedIfNeeded();
ctx.fireChannelRegistered();
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelUnregistered();
// Remove all handlers sequentially if channel is closed and unregistered.
if (!channel.isOpen()) {
destroy();
}
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelActive();
readIfIsAutoRead();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelInactive();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.fireChannelRead(msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelReadComplete();
readIfIsAutoRead();
}
private void readIfIsAutoRead() {
if (channel.config().isAutoRead()) {
channel.read();
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
ctx.fireUserEventTriggered(evt);
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelWritabilityChanged();
}
}
複製程式碼
從head節點繼承的兩個介面看,TA既是一個ChannelHandlerContext,同時又屬於inBound(最新程式碼已經加上這一介面)和outBound Handler
在傳播讀寫事件的時候,head的功能只是簡單地將事件傳播下去,如ctx.fireChannelRead(msg);
在真正執行讀寫操作的時候,例如在呼叫writeAndFlush()
等方法的時候,最終都會委託到unsafe執行,而當一次資料讀完,channelReadComplete
方法首先被呼叫,TA要做的事情除了將事件繼續傳播下去之外,還得繼續向reactor執行緒註冊讀事件,即呼叫readIfIsAutoRead()
, 我們簡單跟一下
HeadContext
private void readIfIsAutoRead() {
if (channel.config().isAutoRead()) {
channel.read();
}
}
複製程式碼
AbstractChannel
@Override
public Channel read() {
pipeline.read();
return this;
}
複製程式碼
預設情況下,Channel都是預設開啟自動讀取模式的,即只要Channel是active的,讀完一波資料之後就繼續向selector註冊讀事件,這樣就可以連續不斷得讀取資料,最終,通過pipeline,還是傳遞到head節點
HeadContext
@Override
public void read(ChannelHandlerContext ctx) {
unsafe.beginRead();
}
複製程式碼
委託到了 NioByteUnsafe
NioByteUnsafe
@Override
public final void beginRead() {
doBeginRead();
}
複製程式碼
AbstractNioChannel
@Override
protected void doBeginRead() throws Exception {
// Channel.read() or ChannelHandlerContext.read() was called
final SelectionKey selectionKey = this.selectionKey;
if (!selectionKey.isValid()) {
return;
}
readPending = true;
final int interestOps = selectionKey.interestOps();
if ((interestOps & readInterestOp) == 0) {
selectionKey.interestOps(interestOps | readInterestOp);
}
}
複製程式碼
doBeginRead()
做的事情很簡單,拿到處理過的selectionKey
,然後如果發現該selectionKey若在某個地方被移除了readInterestOp
操作,這裡給他加上,事實上,標準的netty程式是不會走到這一行的,只有在三次握手成功之後,如下方法被呼叫
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelActive();
readIfIsAutoRead();
}
複製程式碼
才會將readInterestOp
註冊到SelectionKey上,可結合 netty原始碼分析之新連線接入全解析 來看
總結一點,head節點的作用就是作為pipeline的頭節點開始傳遞讀寫事件,呼叫unsafe進行實際的讀寫操作,下面,進入pipeline中非常重要的一環,inbound事件的傳播
##pipeline中的inBound事件傳播
在 netty原始碼分析之新連線接入全解析 一文中,我們沒有詳細描述為啥pipeline.fireChannelActive();
最終會呼叫到AbstractNioChannel.doBeginRead()
,瞭解pipeline中的事件傳播機制,你會發現相當簡單
DefaultChannelPipeline
public final ChannelPipeline fireChannelActive() {
AbstractChannelHandlerContext.invokeChannelActive(head);
return this;
}
複製程式碼
三次握手成功之後,pipeline.fireChannelActive();
被呼叫,然後以head節點為引數,直接一個靜態呼叫
AbstractChannelHandlerContext
static void invokeChannelActive(final AbstractChannelHandlerContext next) {
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
next.invokeChannelActive();
} else {
executor.execute(new Runnable() {
@Override
public void run() {
next.invokeChannelActive();
}
});
}
}
複製程式碼
首先,netty為了確保執行緒的安全性,將確保該操作在reactor執行緒中被執行,這裡直接呼叫 HeadContext.fireChannelActive()
方法
HeadContext
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelActive();
readIfIsAutoRead();
}
複製程式碼
我們先看 ctx.fireChannelActive();
,跟進去之前我們先看下當前pipeline的情況
[2.png]
AbstractChannelHandlerContext
public ChannelHandlerContext fireChannelActive() {
final AbstractChannelHandlerContext next = findContextInbound();
invokeChannelActive(next);
return this;
}
複製程式碼
首先,呼叫 findContextInbound()
找到下一個inbound節點,由於當前pipeline的雙向連結串列結構中既有inbound節點,又有outbound節點,讓我們看看netty是怎麼找到下一個inBound節點的
AbstractChannelHandlerContext
private AbstractChannelHandlerContext findContextInbound() {
AbstractChannelHandlerContext ctx = this;
do {
ctx = ctx.next;
} while (!ctx.inbound);
return ctx;
}
複製程式碼
這段程式碼很清楚地表明,netty尋找下一個inBound節點的過程是一個線性搜尋的過程,他會遍歷雙向連結串列的下一個節點,直到下一個節點為inBound(關於inBound和outBound,netty原始碼分析之pipeline(一)已有說明,這裡不再詳細分析)
找到下一個節點之後,執行 invokeChannelActive(next);
,一個遞迴呼叫,直到最後一個inBound節點——tail節點
TailContext
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception { }
複製程式碼
Tail節點的該方法為空,結束呼叫,同理,可以分析所有的inBound事件的傳播,正常情況下,即使用者如果不覆蓋每個節點的事件傳播操作,幾乎所有的事件最後都落到tail節點,所以,我們有必要研究一下tail節點所具有的功能
##pipeline中的tail
final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler {
TailContext(DefaultChannelPipeline pipeline) {
super(pipeline, null, TAIL_NAME, true, false);
setAddComplete();
}
@Override
public ChannelHandler handler() {
return this;
}
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception { }
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { }
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception { }
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception { }
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { }
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception { }
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { }
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
// This may not be a configuration error and so don`t log anything.
// The event may be superfluous for the current pipeline configuration.
ReferenceCountUtil.release(evt);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
onUnhandledInboundException(cause);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
onUnhandledInboundMessage(msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { }
}
複製程式碼
正如我們前面所提到的,tail節點的大部分作用即終止事件的傳播(方法體為空),除此之外,有兩個重要的方法我們必須提一下,exceptionCaught()
和channelRead()
exceptionCaught
protected void onUnhandledInboundException(Throwable cause) {
try {
logger.warn(
"An exceptionCaught() event was fired, and it reached at the tail of the pipeline. " +
"It usually means the last handler in the pipeline did not handle the exception.",
cause);
} finally {
ReferenceCountUtil.release(cause);
}
}
複製程式碼
異常傳播的機制和inBound事件傳播的機制一樣,最終如果使用者自定義節點沒有處理的話,會落到tail節點,tail節點可不會簡單地吞下這個異常,而是向你發出警告,相信使用netty的同學對這段警告不陌生吧?
channelRead
protected void onUnhandledInboundMessage(Object msg) {
try {
logger.debug(
"Discarded inbound message {} that reached at the tail of the pipeline. " +
"Please check your pipeline configuration.", msg);
} finally {
ReferenceCountUtil.release(msg);
}
}
複製程式碼
另外,tail節點在發現位元組資料(ByteBuf)或者decoder之後的業務物件在pipeline流轉過程中沒有被消費,落到tail節點,tail節點就會給你發出一個警告,告訴你,我已經將你未處理的資料給丟掉了
總結一下,tail節點的作用就是結束事件傳播,並且對一些重要的事件做一些善意提醒
pipeline中的outBound事件傳播
上一節中,我們在闡述tail節點的功能時,忽略了其父類AbstractChannelHandlerContext
所具有的功能,這一節中,我們以最常見的writeAndFlush操作來看下pipeline中的outBound事件是如何向外傳播的
典型的訊息推送系統中,會有類似下面的一段程式碼
Channel channel = getChannel(userInfo);
channel.writeAndFlush(pushInfo);
複製程式碼
這段程式碼的含義就是根據使用者資訊拿到對應的Channel,然後給使用者推送訊息,跟進 channel.writeAndFlush
NioSocketChannel
public ChannelFuture writeAndFlush(Object msg) {
return pipeline.writeAndFlush(msg);
}
複製程式碼
從pipeline開始往外傳播
public final ChannelFuture writeAndFlush(Object msg) {
return tail.writeAndFlush(msg);
}
複製程式碼
Channel 中大部分outBound事件都是從tail開始往外傳播, writeAndFlush()
方法是tail繼承而來的方法,我們跟進去
AbstractChannelHandlerContext
public ChannelFuture writeAndFlush(Object msg) {
return writeAndFlush(msg, newPromise());
}
public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
write(msg, true, promise);
return promise;
}
複製程式碼
這裡提前說一點,netty中很多io操作都是非同步操作,返回一個ChannelFuture
給呼叫方,呼叫方拿到這個future可以在適當的時機拿到操作的結果,或者註冊回撥,後面的原始碼系列會深挖,這裡就帶過了,我們繼續
AbstractChannelHandlerContext
private void write(Object msg, boolean flush, ChannelPromise promise) {
AbstractChannelHandlerContext next = findContextOutbound();
final Object m = pipeline.touch(msg, next);
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
if (flush) {
next.invokeWriteAndFlush(m, promise);
} else {
next.invokeWrite(m, promise);
}
} else {
AbstractWriteTask task;
if (flush) {
task = WriteAndFlushTask.newInstance(next, m, promise);
} else {
task = WriteTask.newInstance(next, m, promise);
}
safeExecute(executor, task, promise, m);
}
}
複製程式碼
netty為了保證程式的高效執行,所有的核心的操作都在reactor執行緒中處理,如果業務執行緒呼叫Channel的讀寫方法,netty會將該操作封裝成一個task,隨後在reactor執行緒中執行,參考 netty原始碼分析之揭開reactor執行緒的面紗(三),非同步task的執行
這裡我們為了不跑偏,假設是在reactor執行緒中(上面的這段例子其實是在業務執行緒中),先呼叫findContextOutbound()
方法找到下一個outBound()
節點
AbstractChannelHandlerContext
private AbstractChannelHandlerContext findContextOutbound() {
AbstractChannelHandlerContext ctx = this;
do {
ctx = ctx.prev;
} while (!ctx.outbound);
return ctx;
}
複製程式碼
找outBound節點的過程和找inBound節點類似,反方向遍歷pipeline中的雙向連結串列,直到第一個outBound節點next
,然後呼叫next.invokeWriteAndFlush(m, promise)
AbstractChannelHandlerContext
private void invokeWriteAndFlush(Object msg, ChannelPromise promise) {
if (invokeHandler()) {
invokeWrite0(msg, promise);
invokeFlush0();
} else {
writeAndFlush(msg, promise);
}
}
複製程式碼
呼叫該節點的ChannelHandler的write方法,flush方法我們暫且忽略,後面會專門講writeAndFlush的完整流程
AbstractChannelHandlerContext
private void invokeWrite0(Object msg, ChannelPromise promise) {
try {
((ChannelOutboundHandler) handler()).write(this, msg, promise);
} catch (Throwable t) {
notifyOutboundHandlerException(t, promise);
}
}
複製程式碼
我們在使用outBound型別的ChannelHandler中,一般會繼承 ChannelOutboundHandlerAdapter
,所以,我們需要看看他的 write
方法是怎麼處理outBound事件傳播的
ChannelOutboundHandlerAdapter
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ctx.write(msg, promise);
}
複製程式碼
很簡單,他除了遞迴呼叫 ctx.write(msg, promise);
之外,啥事也沒幹,在netty原始碼分析之pipeline(一)我們已經知道,pipeline的雙向連結串列結構中,最後一個outBound節點是head節點,因此資料最終會落地到他的write方法
HeadContext
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
unsafe.write(msg, promise);
}
複製程式碼
這裡,加深了我們對head節點的理解,即所有的資料寫出都會經過head節點,我們在下一節會深挖,這裡暫且到此為止
實際情況下,outBound類的節點中會有一種特殊型別的節點叫encoder,它的作用是根據自定義編碼規則將業務物件轉換成ByteBuf,而這類encoder 一般繼承自 MessageToByteEncoder
下面是一段
public abstract class DataPacketEncoder extends MessageToByteEncoder<DatePacket> {
@Override
protected void encode(ChannelHandlerContext ctx, DatePacket msg, ByteBuf out) throws Exception {
// 這裡拿到業務物件msg的資料,然後呼叫 out.writeXXX()系列方法編碼
}
}
複製程式碼
為什麼業務程式碼只需要覆蓋這裡的encod方法,就可以將業務物件轉換成位元組流寫出去呢?通過前面的呼叫鏈條,我們需要檢視一下其父類MessageToByteEncoder
的write方法是怎麼處理業務物件的
MessageToByteEncoder
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ByteBuf buf = null;
try {
// 需要判斷當前編碼器能否處理這類物件
if (acceptOutboundMessage(msg)) {
I cast = (I) msg;
// 分配記憶體
buf = allocateBuffer(ctx, cast, preferDirect);
try {
encode(ctx, cast, buf);
} finally {
ReferenceCountUtil.release(cast);
}
// buf到這裡已經裝載著資料,於是把該buf往前丟,知道head節點
if (buf.isReadable()) {
ctx.write(buf, promise);
} else {
buf.release();
ctx.write(Unpooled.EMPTY_BUFFER, promise);
}
buf = null;
} else {
// 如果不能處理,就將outBound事件繼續往前面傳播
ctx.write(msg, promise);
}
} catch (EncoderException e) {
throw e;
} catch (Throwable e) {
throw new EncoderException(e);
} finally {
if (buf != null) {
buf.release();
}
}
複製程式碼
先呼叫 acceptOutboundMessage
方法判斷,該encoder是否可以處理msg對應的類的物件(暫不展開),通過之後,就強制轉換,這裡的泛型I對應的是DataPacket
,轉換之後,先開闢一段記憶體,呼叫encode()
,即回到DataPacketEncoder
中,將buf裝滿資料,最後,如果buf中被寫了資料(buf.isReadable()
),就將該buf往前丟,一直傳遞到head節點,被head節點的unsafe消費掉
當然,如果當前encoder不能處理當前業務物件,就簡單地將該業務物件向前傳播,直到head節點,最後,都處理完之後,釋放buf,避免堆外記憶體洩漏
##pipeline 中異常的傳播
我們通常在業務程式碼中,會加入一個異常處理器,統一處理pipeline過程中的所有的異常,並且,一般該異常處理器需要載入自定義節點的最末尾,即
此類ExceptionHandler一般繼承自 ChannelDuplexHandler
,標識該節點既是一個inBound節點又是一個outBound節點,我們分別分析一下inBound事件和outBound事件過程中,ExceptionHandler是如何才處理這些異常的
inBound異常的處理
我們以資料的讀取為例,看下netty是如何傳播在這個過程中發生的異常
我們前面已經知道,對於每一個節點的資料讀取都會呼叫AbstractChannelHandlerContext.invokeChannelRead()
方法
AbstractChannelHandlerContext
private void invokeChannelRead(Object msg) {
try {
((ChannelInboundHandler) handler()).channelRead(this, msg);
} catch (Throwable t) {
notifyHandlerException(t);
}
}
複製程式碼
可以看到該節點最終委託到其內部的ChannelHandler處理channelRead,而在最外層catch整個Throwable,因此,我們在如下使用者程式碼中的異常會被捕獲
public class BusinessHandler extends ChannelInboundHandlerAdapter {
@Override
protected void channelRead(ChannelHandlerContext ctx, Object data) throws Exception {
//...
throw new BusinessException(...);
//...
}
}
複製程式碼
上面這段業務程式碼中的 BusinessException
會被 BusinessHandler
所在的節點捕獲,進入到 notifyHandlerException(t);
往下傳播,我們看下它是如何傳播的
AbstractChannelHandlerContext
private void notifyHandlerException(Throwable cause) {
// 略去了非關鍵程式碼,讀者可自行分析
invokeExceptionCaught(cause);
}
複製程式碼
private void invokeExceptionCaught(final Throwable cause) {
handler().exceptionCaught(this, cause);
}
複製程式碼
可以看到,此Hander中異常優先由此Handelr中的exceptionCaught
方法來處理,預設情況下,如果不覆寫此Handler中的exceptionCaught
方法,呼叫
ChannelInboundHandlerAdapter
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
ctx.fireExceptionCaught(cause);
}
複製程式碼
AbstractChannelHandlerContext
public ChannelHandlerContext fireExceptionCaught(final Throwable cause) {
invokeExceptionCaught(next, cause);
return this;
}
複製程式碼
到了這裡,已經很清楚了,如果我們在自定義Handler中沒有處理異常,那麼預設情況下該異常將一直傳遞下去,遍歷每一個節點,直到最後一個自定義異常處理器ExceptionHandler來終結,收編異常
Exceptionhandler
public Exceptionhandler extends ChannelDuplexHandler {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
// 處理該異常,並終止異常的傳播
}
}
複製程式碼
到了這裡,你應該知道為什麼異常處理器要加在pipeline的最後了吧?
outBound異常的處理
然而對於outBound事件傳播過程中所發生的異常,該Exceptionhandler
照樣能完美處理,為什麼?
我們以前面提到的writeAndFlush
方法為例,來看看outBound事件傳播過程中的異常最後是如何落到Exceptionhandler
中去的
前面我們知道,channel.writeAndFlush()
方法最終也會呼叫到節點的 invokeFlush0()
方法(write機制比較複雜,我們留到後面的文章中將)
AbstractChannelHandlerContext
private void invokeWriteAndFlush(Object msg, ChannelPromise promise) {
if (invokeHandler()) {
invokeWrite0(msg, promise);
invokeFlush0();
} else {
writeAndFlush(msg, promise);
}
}
private void invokeFlush0() {
try {
((ChannelOutboundHandler) handler()).flush(this);
} catch (Throwable t) {
notifyHandlerException(t);
}
}
複製程式碼
而invokeFlush0()
會委託其內部的ChannelHandler的flush方法,我們一般實現的即是ChannelHandler的flush方法
private void invokeFlush0() {
try {
((ChannelOutboundHandler) handler()).flush(this);
} catch (Throwable t) {
notifyHandlerException(t);
}
}
複製程式碼
好,假設在當前節點在flush的過程中發生了異常,都會被 notifyHandlerException(t);
捕獲,該方法會和inBound事件傳播過程中的異常傳播方法一樣,也是輪流找下一個異常處理器,而如果異常處理器在pipeline最後面的話,一定會被執行到,這就是為什麼該異常處理器也能處理outBound異常的原因
關於為啥 ExceptionHandler
既能處理inBound,又能處理outBound型別的異常的原因,總結一點就是,在任何節點中發生的異常都會往下一個節點傳遞,最後終究會傳遞到異常處理器
總結
最後,老樣子,我們做下總結
- 一個Channel對應一個Unsafe,Unsafe處理底層操作,NioServerSocketChannel對應NioMessageUnsafe, NioSocketChannel對應NioByteUnsafe
- inBound事件從head節點傳播到tail節點,outBound事件從tail節點傳播到head節點
- 異常傳播只會往後傳播,而且不分inbound還是outbound節點,不像outBound事件一樣會往前傳播
如果你想系統地學Netty,我的小冊《Netty 入門與實戰:仿寫微信 IM 即時通訊系統》可以幫助你
如果你想系統學習Netty原理,那麼你一定不要錯過我的Netty原始碼分析系列視訊:Java 讀原始碼之 Netty 深入剖析