全文圍繞下圖,Netty-Channel的簡化版架構體系圖展開,從頂層Channel介面開始入手,往下遞進,閒言少敘,直接開擼
概述: 從圖中可以看到,從頂級介面Channel開始,在介面中定義了一套方法當作規範,緊接著的是來兩個抽象的介面實現類,在這個抽象類中對介面中的方法,進行了部分實現,然後開始根據不同的功能分支,分成服務端的Channel和客戶端的Channel
回顧
Channel的分類
根據服務端和客戶端,Channel可以分成兩類(這兩大類的分支見上圖):
- 服務端:
NioServerSocketChannel
- 客戶端:
NioSocketChannel
什麼是Channel?
channel是一個管道,用於連線位元組緩衝區Buf和另一端的實體,這個例項可以是Socket,也可以是File, 在Nio網路程式設計模型中, 服務端和客戶端進行IO資料互動(得到彼此推送的資訊)的媒介就是Channel
Netty對Jdk原生的ServerSocketChannel
進行了封裝和增強封裝成了NioXXXChannel
, 相對於原生的JdkChannel, Netty的Channel增加了如下的元件
- id 標識唯一身份資訊
- 可能存在的parent Channel
- 管道 pepiline
- 用於資料讀寫的unsafe內部類
- 關聯上相伴終生的NioEventLoop
本篇部落格,會追溯上圖中的體系關係,找出NioXXXChannel
的相對於jdk原生channel在哪裡新增的上面的新元件
原始碼開始-Channel
現在來到上圖的Channel
部分, 他是一個介面, netty用它規定了一個Channel
是該具有的功能,在它的文件對Channel的
是什麼,以及對各個元件進行了描述
- 闡述了channel是什麼,有啥用
Channel
通過ChannelPipeline
中的多個Handler
處理器,Channel
使用它處理IO資料Channel
中的所有Io操作都是非同步的,一經呼叫就馬上返回,於是Netty基於Jdk原生的Future
進行了封裝,ChannelFuture
, 讀寫操作會返回這個物件,實現自動通知IO操作已完成Channel
是可以有parent的, 如下
// 建立客戶端channel時,會把服務端的Channel設定成自己的parent
// 於是就像下面:
服務端的channel = 客戶端的channel.parent();
服務的channel.parent()==null;
此外,Channel還定義了大量的抽象方法, 如下:
/**
* todo 返回一個僅供內部使用的unsafe物件, Chanel上 IO資料的讀寫都是藉助這個類完成的
*/
Unsafe unsafe();
// 返回Channel的管道
ChannelPipeline pipeline();
ByteBufAllocator alloc();
@Override // todo 進入第一個實現 , 讀取Channel中的 IO資料
Channel read();
// 返回Channel id
ChannelId id();
// todo 返回channel所註冊的 eventLoop
EventLoop eventLoop();
// 返回當前Channel的父channel
Channel parent();
// todo 描述了 關於channel的 一些列配置資訊
ChannelConfig config();
// 檢查channel是否開啟
boolean isOpen();
// 檢查channel是否註冊
boolean isRegistered();
// todo 什麼是active 他說的是channel狀態, 什麼狀態呢? 當前channel 若和Selector正常的通訊就說明 active
boolean isActive();
// 返回channel的後設資料
ChannelMetadata metadata();
// 伺服器的ip地址
SocketAddress localAddress();
// remoteAddress 客戶端的ip地址
SocketAddress remoteAddress();
ChannelFuture closeFuture();
boolean isWritable();
long bytesBeforeUnwritable();
long bytesBeforeWritable();
@Override
Channel flush();
Channel重要的內部介面 unsafe
Netty中,真正幫助Channel完成IO讀寫操作的是它的內部類unsafe
, 原始碼如下, 很多重要的功能在這個介面中定義, 下面列舉的常用的方法
interface Unsafe {
// 把channel註冊進EventLoop
void register(EventLoop eventLoop, ChannelPromise promise);
// todo 給channel繫結一個 adress,
void bind(SocketAddress localAddress, ChannelPromise promise);
// 把channel註冊進Selector
void deregister(ChannelPromise promise);
// 從channel中讀取IO資料
void beginRead();
// 往channe寫入資料
void write(Object msg, ChannelPromise promise);
...
...
AbstractChanel
接著往下看,下面來到Channel
介面的直接實現類,AbstractChannel
他是個抽象類, AbstractChannel
重寫部分Channel
介面預定義的方法, 它的抽象內部類AbstractUnsafe
實現了Channel
的內部介面unsafe
我們現在是從上往下看,但是當我們建立物件使用的時候其實是使用的特化的物件,建立特化的物件就難免會調層層往上呼叫父類的構造方法, 所以我們看看AbstractChannel
的構造方法幹了什麼活? 原始碼如下:
protected AbstractChannel(Channel parent) {
this.parent = parent;
// todo channelId 代表Chanel唯一的身份標誌
id = newId();
// todo 建立一個unsafe物件
unsafe = newUnsafe();
// todo 在這裡初始化了每一個channel都會有的pipeline元件
pipeline = newChannelPipeline();
}
我們看,AbstractChannel
建構函式, 接受的子類傳遞進來的引數只有一個parent CHannel,而且,還不有可能為空, 所以在AbstractChannel
是沒有維護jdk底層的Channel的, 相反他會維護著Channel關聯的EventLoop,我是怎麼知道的呢? 首先,它的屬性中存在這個欄位,而且,將channel註冊進selector的Register()方法是AbastractChannel
重寫的,Selector在哪呢? 在EventLoop裡面,它怎麼得到的呢? 它的子類傳遞了給了它
終於看出來點眉目,構造方法做了四件事
- 設定parent
- 如果當前建立的channel是客戶端的channel,把parent初始化為他對應的parent
- 如果為服務端的channel,這就是null
- 建立唯一的id
- 建立針對channel進行io讀寫的
unsafe
- 建立channel的處理器handler鏈
channelPipeline
AbstractChannel中維護著EventLoop
AbstractChanel
的重要抽象內部類AbstractUnsafe
繼承了Channel
的內部介面Unsafe
他的原始碼如下,我貼出來了兩個重要的方法, 關於這兩個方法的解析,我寫在程式碼的下面
protected abstract class AbstractUnsafe implements Unsafe {
@Override
// todo 入參 eventLoop == SingleThreadEventLoop promise == NioServerSocketChannel + Executor
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
if (eventLoop == null) {
throw new NullPointerException("eventLoop");
}
if (isRegistered()) {
promise.setFailure(new IllegalStateException("registered to an event loop already"));
return;
}
if (!isCompatible(eventLoop)) {
promise.setFailure(
new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName()));
return;
}
// todo 賦值給自己的 事件迴圈, 把當前的eventLoop賦值給當前的Channel上 作用是標記後續的所有註冊的操作都得交給我這個eventLoop處理, 正好對應著下面的判斷
// todo 保證了 即便是在多執行緒的環境下一條channel 也只能註冊關聯上唯一的eventLoop,唯一的執行緒
AbstractChannel.this.eventLoop = eventLoop;
// todo 下面的分支判斷裡面執行的程式碼是一樣的!!, 為什麼? 這是netty的重點, 它大量的使用執行緒, 執行緒之間就會產生同步和併發的問題
// todo 下面的分支,目的就是把執行緒可能帶來的問題降到最低限度
// todo 進入inEventLoop() --> 判斷當前執行這行程式碼的執行緒是否就是 SingleThreadEventExecutor裡面維護的那條唯一的執行緒
// todo 解釋下面分支的必要性, 一個eventLoop可以註冊多個channel, 但是channel的整個生命週期中所有的IO事件,僅僅和它關聯上的thread有關係
// todo 而且,一個eventLoop在他的整個生命週期中,只和唯一的執行緒進行繫結,
//
// todo 當我們註冊channel的時候就得確保給他專屬它的thread,
// todo 如果是新的連線到了,
if (eventLoop.inEventLoop()) {
// todo 進入regist0()
register0(promise);
} else {
try {
// todo 如果不是,它以一個任務的形式提交 事件迴圈 , 新的任務在新的執行緒開始, 規避了多執行緒的併發
// todo 他是SimpleThreadEventExucutor中execute()實現的,把任務新增到執行佇列執行
eventLoop.execute(new Runnable() {
@Override
public void run() {
register0(promise);
}
});
} catch (Throwable t) {
logger.warn(
"Force-closing a channel whose registration task was not accepted by an event loop: {}",
AbstractChannel.this, t);
closeForcibly();
closeFuture.setClosed();
safeSetFailure(promise, t);
}
}
}
private void register0(ChannelPromise promise) {
try {
// check if the channel is still open as it could be closed in the mean time when the register
// call was outside of the eventLoop
if (!promise.setUncancellable() || !ensureOpen(promise)) {
return;
}
boolean firstRegistration = neverRegistered;
// todo 進入這個方法doRegister()
// todo 它把系統建立的ServerSocketChannel 註冊進了選擇器
doRegister();
neverRegistered = false;
registered = true;
// Ensure we call handlerAdded(...) before we actually notify the promise. This is needed as the
// user may already fire events through the pipeline in the ChannelFutureListener.
// todo 確保在 notify the promise前呼叫 handlerAdded(...)
// todo 這是必需的,因為使用者可能已經通過ChannelFutureListener中的管道觸發了事件。
// todo 如果需要的話,執行HandlerAdded()方法
// todo 正是這個方法, 回撥了前面我們新增 Initializer 中新增 Accpter的重要方法
pipeline.invokeHandlerAddedIfNeeded();
// todo !!!!!!! 觀察者模式!!!!!! 通知觀察者,誰是觀察者? 暫時理解ChannelHandler 是觀察者
safeSetSuccess(promise);
// todo 傳播行為, 傳播什麼行為呢? 在head---> ServerBootStraptAccptor ---> tail傳播事件ChannelRegistered , 也就是挨個呼叫它們的ChannelRegisted函式
pipeline.fireChannelRegistered();
// Only fire a channelActive if the channel has never been registered. This prevents firing
// multiple channel actives if the channel is deregistered and re-registered.
// todo 對於服務端: javaChannel().socket().isBound(); 即 當Channel繫結上了埠 isActive()才會返回true
// todo 對於客戶端的連線 ch.isOpen() && ch.isConnected(); 返回true , 就是說, Channel是open的 開啟狀態的就是true
if (isActive()) {
if (firstRegistration) {
// todo 在pipeline中傳播ChannelActive的行為,跟進去
pipeline.fireChannelActive();
} else if (config().isAutoRead()) {
// This channel was registered before and autoRead() is set. This means we need to begin read
// again so that we process inbound data.
//
// See https://github.com/netty/netty/issues/4805
// todo 可以接受客戶端的資料了
beginRead();
}
}
} catch (Throwable t) {
// Close the channel directly to avoid FD leak.
closeForcibly();
closeFuture.setClosed();
safeSetFailure(promise, t);
}
}
@Override
public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
assertEventLoop();
if (!promise.setUncancellable() || !ensureOpen(promise)) {
return;
}
// See: https://github.com/netty/netty/issues/576
if (Boolean.TRUE.equals(config().getOption(ChannelOption.SO_BROADCAST)) &&
localAddress instanceof InetSocketAddress &&
!((InetSocketAddress) localAddress).getAddress().isAnyLocalAddress() &&
!PlatformDependent.isWindows() && !PlatformDependent.maybeSuperUser()) {
// Warn a user about the fact that a non-root user can't receive a
// broadcast packet on *nix if the socket is bound on non-wildcard address.
logger.warn(
"A non-root user can't receive a broadcast packet if the socket " +
"is not bound to a wildcard address; binding to a non-wildcard " +
"address (" + localAddress + ") anyway as requested.");
}
boolean wasActive = isActive();
// todo 由於埠的繫結未完成,所以 wasActive是 false
try {
// todo 繫結埠, 進去就是NIO原生JDK繫結埠的程式碼
doBind(localAddress);
// todo 埠繫結完成 isActive()是true
} catch (Throwable t) {
safeSetFailure(promise, t);
closeIfClosed();
return;
}
// todo 根據上面的邏輯判斷, 結果為 true
if (!wasActive && isActive()) {
invokeLater(new Runnable() {
// todo 來到這裡很重要, 向下傳遞事件行為, 傳播行為的時候, 從管道的第一個節點開始傳播, 第一個節點被封裝成 HeadContext的物件
// todo 進入方法, 去 HeadContext裡面檢視做了哪些事情
// todo 她會觸發channel的read, 最終重新為 已經註冊進selector 的 chanel, 二次註冊新增上感性趣的accept事件
@Override
public void run() {
pipeline.fireChannelActive();
}
});
}
// todo 觀察者模式, 設定改變狀態, 通知觀察者的方法回撥
safeSetSuccess(promise);
}
AbstractChannel
抽象內部類的register(EventLoop,channelPromise)方法
這個方法,是將channel註冊進EventLoop的Selector, 它的呼叫順序如下:
本類方法 regist()--> 本類方法 register0() --> 本類抽象方法doRegister()
doRegister() 在這裡設計成抽象方法,等著子類去具體的實現, 為啥這樣做呢?
剛才說了,AbstractChannel
本身就是個模板,而且它僅僅維護了EventLoop,沒有拿到channel引用的它根本不可能進行註冊的邏輯,那誰有jdk原生channel的引用呢? 它的直接子類AbstractNioChannel
下面是AbstractNioChannel
的構造方法, 它自己維護jdk原生的Channel,所以由他重寫doRegister()
,
*/ // todo 無論是服務端的channel 還是客戶端的channel都會使用這個方法進行初始化
// // TODO: 2019/6/23 null ServerSocketChannel accept
// todo 如果是在建立NioSocketChannel parent==NioServerSocketChannel ch == SocketChanel
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
super(parent);// todo 繼續向上跟,建立基本的元件
// todo 如果是建立NioSocketChannel 這就是在儲存原生的jdkchannel
// todo 如果是建立NioServerSocketChannel 這就是在儲存ServerSocketChannel
this.ch = ch;
// todo 設定上感興趣的事件
this.readInterestOp = readInterestOp;
try {
// todo 作為服務端, ServerSocketChannel 設定為非阻塞的
// todo 作為客戶端 SocketChannel 設定為非阻塞的
ch.configureBlocking(false);
} catch (IOException e) {
AbstractChannel
抽象內部類的bind()方法
bind()方法的呼叫順序, 本類方法 bind()--> 本類的抽象方法 dobind()
方法的目的是給Channel繫結上屬於它的埠,同樣有一個抽象方法,等著子類去實現,因為我們已經知道了AbstractChannel
不維護channel的引用,於是我就去找dobind()
這個抽象函式的實現, 結果發現,AbstractChannel
的直接子類AbstractNioChannel
中根本不沒有他的實現,這是被允許的,因為AbstractNioChannel
本身也是抽象類, 到底是誰實現呢? 如下圖:在NioServerSocketChannel中獲取出 Jdk原生的channel, 客戶端和服務端的channel又不同,所以繫結埠這中特化的任務,交給他們自己實現
AbstractChannel
的beginRead()()方法
上面完成註冊之後,就去繫結埠,當埠繫結完成,就會channel處於active
狀態,下一步就是執行beginRead()
,執行的流程如下
本類抽象方法 beginRead()
--> 本類抽象方法doBeginRead()
這個read()
就是從已經繫結好埠的channel中讀取IO資料,和上面的方法一樣,對應沒有channel
應用的AbstractChannel
來說,netty把它設計成抽象方法,交給擁有jdk 原生channel
引用的AbstractNioChannel
實現
小結:
AbstractChannel
作為Channel的直接實現類,本身又是抽象類,於是它實現了Channel的預留的一些抽象方法, 初始化了channel的四個元件 id pipeline unsafe parent, 更為重要的是它的抽象內部類 實現了 關於nettyChannel的註冊,繫結,讀取資料的邏輯,而且以抽象類的方法,挖好了填空題等待子類的特化實現
遞進AbstractNioChannel
跟進構造方法
依然是來到AbstractNioChannel
的構造方法,發現它做了如下的構造工作:
- 把parent傳遞給了
AbstractChannel
- 把子類傳遞過來的Channel要告訴Selector的感興趣的選項儲存
- 設定channel為非阻塞
// todo 無論是服務端的channel 還是客戶端的channel都會使用這個方法進行初始化
// // TODO: 2019/6/23 null ServerSocketChannel accept
// todo 如果是在建立NioSocketChannel parent==NioServerSocketChannel ch == SocketChanel
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
super(parent);// todo 繼續向上跟,建立基本的元件
// todo 如果是建立NioSocketChannel 這就是在儲存原生的jdkchannel
// todo 如果是建立NioServerSocketChannel 這就是在儲存ServerSocketChannel
this.ch = ch;
// todo 設定上感興趣的事件
this.readInterestOp = readInterestOp;
try {
// todo 作為服務端, ServerSocketChannel 設定為非阻塞的
// todo 作為客戶端 SocketChannel 設定為非阻塞的
ch.configureBlocking(false);
} catch (IOException e) {
重寫了它父類的doRegister()
AbstractNioChannel
維護channel的引用,真正的實現把 jdk 原生的 channel註冊進 Selector中
@Override
protected void doRegister() throws Exception {
boolean selected = false;
for (;;) {
try {
// todo javaChannel() -- 返回SelectableChanel 可選擇的Channel,換句話說,可以和Selector搭配使用,他是channel體系的頂級抽象類, 實際的型別是 ServerSocketChannel
// todo eventLoop().unwrappedSelector(), -- > 獲取選擇器, 現在在AbstractNioChannel中 獲取到的eventLoop是BossGroup裡面的
// todo 到目前看, 他是把ServerSocketChannel(系統建立的) 註冊進了 EventLoop的選擇器
// todo 這裡的 最後一個引數是 this是當前的channel , 意思是把當前的Channel當成是一個 attachment(附件) 繫結到selector上 作用???
// todo 現在知道了attachment的作用了
// todo 1. 當channel在這裡註冊進 selector中返回一個selectionKey, 這個key告訴selector 這個channel是自己的
// todo 2. 當selector輪詢到 有channel出現了自己的感興趣的事件時, 需要從成百上千的channel精確的匹配出 出現Io事件的channel,
// todo 於是seleor就在這裡提前把channel存放入 attachment中, 後來使用
// todo 最後一個 this 引數, 如果是服務啟動時, 他就是NioServerSocketChannel 如果是客戶端他就是 NioSocketChannel
// todo 到目前為止, 雖然註冊上了,但是它不關心任何事件
selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
return;
} catch (CancelledKeyException e) {
新增內部介面
AbstractNioChannel
新新增了一個內部介面,作為原Channel的擴充套件,原始碼如下, 我們著重關心的就是這個新介面的 read()
方法, 它的作用是從channel去讀取IO資料,作為介面的抽象方法,它規範服務端和客戶端根據自己需求去不同的實現這個read()
怎麼特化實現這個read方法呢? 若是服務端,它read的結果就是一個新的客戶端的連線, 如果是客戶端,它read的結果就是 客戶端傳送過來的資料,所以這個read()
很有必要去特化
/**
* Read from underlying {@link SelectableChannel}
*/
// todo 兩個實現類, NioByteUnsafe , 處理關於客戶端發來的資訊
// todo NioMessageUnsafe 處理客戶端新進來的連線
void read();
/**
* Special {@link Unsafe} sub-type which allows to access the underlying {@link SelectableChannel}
*/
public interface NioUnsafe extends Unsafe {
/**
* Return underlying {@link SelectableChannel}
*/
SelectableChannel ch();
/**
* Finish connect
*/
void finishConnect();
void forceFlush();
}
AbstractNioChannel
的抽象內部內同時繼承了它父類的AbstractUnsafe
實現了當前的NioUnsafe
, 再往後看, 問題來了, 服務端和客戶端在的針對read的特化實現在哪裡呢? 想想看肯定在它子類的unsafe內部類中,如下圖,紫框框
一會再具體看這兩個 內部類是如何特化read的 注意啊,不再是抽象的了
再進一步 AbstractNioMessageChannel
它的建構函式如下, 只是呼叫父類的建構函式,傳遞引數
protected AbstractNioMessageChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
// todo 在進去
// todo null ServerSocketChannel accept
super(parent, ch, readInterestOp);
}
AbstractNioMessageChannel
的MessageNioUnsafe
對read()
特化實現
在read方法中,我們可以看到,他呼叫是本類的抽象方法doReadMessages(List<Object> buf)
, 方法的實現類是繼承體系的最底層的NioServerSocketChannel
, 因為他就是那個特化的服務端channel
當然如果我們一開始跟進read()時,來到的客戶端的AbstractNioByteChannel
,現在我們找到的doReadMessage()
就是由 客戶端的channelNioSocketChannel
完成的doReadBytes()
// todo 用於處理新連結進來的內部類
private final class NioMessageUnsafe extends AbstractNioUnsafe {
// todo 這個容器用於存放臨時讀到的連線
private final List<Object> readBuf = new ArrayList<Object>();
// todo 接受新連結的 read來到這裡
@Override
public void read() {
...
doBeginRead(buf);
...
}
// todo 處理新的連線 是在 NioServerSocketChannel中實現的, 進入檢視
protected abstract int doReadMessages(List<Object> buf) throws Exception;
最終,特化的channel實現
現在我們就來到了最底層,整張繼承圖就全部展現在眼前了,下面就去看看,特化的服務端Channel NioServetSocketChannel
和NioSocketChannel
對 doReadMessages()
和doReadBytes()
的各自實現
服務端, 我們看到了,它的特化read()
是在建立新的 Jdk遠端channel, 因為它在建立新的連線chanel
@Override
protected int doReadMessages(List<Object> buf) throws Exception {
// todo java Nio底層在這裡 建立jdk底層的 原生channel
SocketChannel ch = SocketUtils.accept(javaChannel());
try {
if (ch != null) {
// todo 把java原生的channel, 封裝成 Netty自定義的封裝的channel , 這裡的buf是list集合物件,由上一層傳遞過來的
// todo this -- NioServerSocketChannel
// todo ch -- SocketChnnel
buf.add(new NioSocketChannel(this, ch));
return 1;
}
...
客戶端, 讀取客戶端傳送過來的IO資料
@Override
protected int doReadBytes(ByteBuf byteBuf) throws Exception {
final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
allocHandle.attemptedBytesRead(byteBuf.writableBytes());
return byteBuf.writeBytes(javaChannel(), allocHandle.attemptedBytesRead());
}
小結:
Netty的channel繼承體系,到現在就完成了, 相信,當我們現在再正著從 NioServerEventLoop
入手,看他的初始化過程應該很簡單了, 其中我希望自己可以牢記幾個點
AbstractChannel
維護NioChannel
的EventLoop
AbstractNioChannel
維護jdk原生channel
AbstractChannel
中的AbstractUnsafe
主要是定義了一套模板,給子類提供了填空題,下面的三個填空- 註冊 把chanel註冊進Selector
- 繫結 把chanel繫結上埠
- 新增感興趣的事件, 給建立出來的channel二次註冊上netty可以處理的感興趣的事件
- channel的io操作是unsafe內部類完成的
- 服務端從channel,讀取出新連線
NioMessageUnsafe
- 客戶端從channel,讀取出資料
NioByteUnsafe
- 服務端從channel,讀取出新連線