netty學習筆記(一)—結合reactor模式探索netty對網路io的處理機制
阿新 • • 發佈:2019-01-31
Reactor與Proactor簡介
reactor、proactor常見的翻譯是反應器(堆)、前攝器,這名字聽著總讓人一頭霧水的,抓不著本質。後來看看對應形容詞的英文釋義,再結合技術角度的描述,總算有了基礎的認識:reactive: reacting to events or situations rather than starting or doing new things yourself.
proactive: making things happen or change rather than reacting to events.
很明顯,這對詞意思相對,reactive 被動地響應事件,而proactive則主動促使事情發生或改變。
Reactor 架構:由Dispatcher中的event loop迴圈選擇有已就緒操作的通道,觸發對應的事件,分派給相應的handler去處理。其中的 io read/write 操作都是 non-blocking 的。同步的事件分派模型。
Proactor 架構:其本身只需要向作業系統發起非同步操作請求,由OS代表Proactor框架執行相應操作,並在操作完成時新增相應的完成事件至Completeion Dispatcher(本質上就是一個佇列)中,Proactor框架在Completeion Dispatcher的event loop中獲取完成事件,分派給相應的handler處理。其中的 io read/write 操作都是 asynchronous 的。非同步的事件分派模型。
網上看了不少文章到此可能就轉入下一部分了,但實際上我們還是沒有完全弄明白Reactor和Proactor的區別。前者採用的是 non-blocking io,而後者是 asynchronous io,我覺得這才是導致它們有不同的根本原因。non-blocking io需要使用者態(即應用)執行緒輪詢(polling)檢查是否有資料到達核心(kernel)空間,如果有,就發起read操作將資料從核心空間拷貝到應用空間,進而處理資料;write操作亦如此。而asynchronous io只需要使用者態執行緒發起read操作,然後就可以去做其他工作(即處理其他請求,而非掛起等待),作業系統會在完成read操作即已將資料從核心拷貝到應用空間時通知應用去處理資料,在此期間應用完全不需要等待read操作返回結果。顯然,服務同樣多的客戶端,前者需要消耗較多的使用者態執行緒資源,而後者只需要較少的執行緒資源即可滿足需求,這直接導致它們採用的執行緒策略、併發策略也不同。這兩種I/O模型的不同也導致它們採用的事件分派模型不同。有關Reactor和Proactor更詳細的闡述請查閱參考資源。 有關blocking, nonblocking, multiplexing, signal driven(SIGIO), asynchronous 五種I/O模型的區別以及對Synchronous、Asynchronous I/O的解釋請參考《UNIX Network Programming(Volume 1)》6.2 I/O Models,講解的非常棒,簡潔明瞭,通俗易懂。
支援多個reactor的Reactor模型
mainReactor 負責響應連線請求,呼叫acceptor接受請求,建立新連線,並將新連線交給一個subReactor維護,以後就由這個subReactor為此連線提供服務直至連線關閉。subReactor 負責響應讀寫請求,迴圈檢查哪些通道有已準備就緒的操作,觸發相應事件,並分派給具體的handler處理。
探索netty的啟動過程
其實netty就是一個Reactor模式的實現,下面以netty自帶的示例EchoServer為入口,開啟netty內部實現的探索之旅。先來看看netty server的啟動,乍一看就三步:1. 為ServerBootstrap配置channel factory
2. 為ServerBootstrap配置pipeline factory (1)
3. 繫結埠
但實際上,在這簡單的外表下隱藏了許多蠻複雜的工作,主要集中在第一、三步。
第一步中以bossExecutor、workerExecutor兩個執行緒池為引數建立了一個NioServerSocketChannelFactory,在此物件的構造過程中,完成了NioServerBossPool、NioWorkerPool的建立和初始化,池大小分別為1、n(>1)。初始化pool時,建立了相應數量的NioServerBoss、NioWorker,並提交給bossExecutor、workerExecutor,讓它們在各自的I/O執行緒中執行起來了。這點很重要哦,因為Boss和Worker的執行緒體就是一個event loop,它們無限輪詢著以即時響應已準備就緒的I/O操作——觸發相應的事件,分派給handler處理。 NioServerBoss和NioWorker都通過繼承AbstractNioSelector實現了Runnable介面,都持有且維護著一個Selector物件。AbstractNioSelector類是netty nio部分的核心,其中的邏輯很複雜,包括Boss和Worker的啟動執行、rebuildSelector以繞過jdk epoll(..) bug等。為了便於理解,對其做了精簡:
第三步是明修棧道,暗度陳倉。說是繫結埠,實則先從NioServerSocketChannelFactory建立一個NioServerSocketChannel,在該Channel的建構函式中開啟、配置socket,觸發上行的channel open事件,以讓與當前channel關聯的pipeline中的Binder事件處理器處理此事件【特別提醒:此pipeline並非由(1)處配給的pipeline factory建立,具體請參考ServerBootstrap.bindAsync(localAddress)。server與client間每個新建立的channel關聯的pipeline都由(1)處配給的pipeline factory建立,每個pipeline中裝著netty使用者自定義的事件處理器】。ServerBootstrap.Binder中處理channel open事件的邏輯: 1. 為剛建立的NioServerSocketchannel配置pipelineFactory,此pipelineFactory就是(1)處設定給ServerBootstrap的那個abstract class AbstractNioSelector implements NioSelector { // nio Selector protected volatile Selector selector; // 當前物件需要處理的任務佇列 private final Queue taskQueue = new ConcurrentLinkedQueue(); public void register(Channel channel, ChannelFuture future) { // omitted } // 無線迴圈,這就是event loop public void run() { for (;;) { try { // 處理任務佇列 processTaskQueue(); // 處理selector,以響應已就緒的I/O操作 process(selector); } catch (Throwable t) { try { Thread.sleep(1000); } catch (InterruptedException e) { // Ignore. } } } } private void processTaskQueue() { for (;;) { final Runnable task = taskQueue.poll(); if (task == null) { break; } task.run(); } } protected abstract void process(Selector selector) throws IOException; }
2. 為該channel配置其他選項
3. 繼續傳播channel open事件
4. 傳送下行的埠繫結事件,最終由NioServerSocketPipelineSink處理該事件,其呼叫boss.bind(...)將埠繫結任務新增至boss的任務佇列,由boss在執行期間擇機執行實際的埠繫結操作。
NioServerSocketChannel有幾個很重要的關聯物件,瞭解這些物件之間的關係對從整體把握理解netty程式碼很重要,其精簡程式碼如下:
class NioServerSocketChannel ... {
final ServerSocketChannel socket;
final Boss boss;
final WorkerPool<NioWorker> workerPool;
private final ServerSocketChannelConfig config;
NioServerSocketChannel(
ChannelFactory factory,
ChannelPipeline bossPipeline,
ChannelSink sink, Boss boss, WorkerPool<NioWorker> workerPool) {
super(factory,bossPipeline, sink);
this.boss = boss;
this.workerPool = workerPool;
// Ignore ..
socket = ServerSocketChannel.open();
// Ignore ..
socket.configureBlocking(false);
// Ignore ..
config = new DefaultServerSocketChannelConfig(socket.socket());
fireChannelOpen(this);
}
}
至此,netty server已經啟動完成了。不過前面配置pipeline factory的邏輯看起來蠻讓人困惑的,待我們從整體理清netty各部分的職責,以及相互之間的協作後再來拾掇它。
結合Reactor模式,探索netty對網路I/O的處理機制
NioServerBoss負責監聽、接受client的連線請求,建立代表新連線的NioAcceptedSocketChannel物件,並將其分派給workerPool中的一個NioWorker,由該worker為其服務;然後將新channel的註冊任務新增至指派的NioWorker的任務佇列中(呼叫worker.register(...))。回頭看看第二部分的Reactor模型,會發現NioServerBoss扮演的就是mainReactor角色。注意哦,由於workerPool中NioWorker數量有限,因此就必然需要一個NioWorker服務於多個channel,即多個channel都在同一個NioWorke持有的selector處註冊,這樣一個io thread就可以服務多個channel了,這就是I/O多路複用(i/o multiplexing)。NioWorker負責響應所服務的通道中已準備就緒的I/O讀寫操作,觸發相應的事件以使匹配的事件處理器處理此事件。同樣地,你會發現NioWorker扮演的就是subReactor角色。NilWorker繼承自AbstractNioWorker,該類中包含了read/write的幾乎所有邏輯,有點複雜,有空再細看。 現在能看清netty的整體脈絡了,是時候回頭捋捋配置pipeline factory的邏輯了。channel與pipeline是一對一的關係,每個channel都由一個pipeline物件與之關聯,這個pipeline中裝著處理該channel上已準備就緒的I/O操作的handlers。NioServerBoss為NioServerSocketChannel服務,該channel上可能發生的就是accept操作,Boss負責接受、建立新的連線物件NioAcceptedSocketChannel。NioAcceptedSocketChannel上可能發生的是read/write操作,而這正是netty使用者感興趣的操作,使用者需要自定義handler來處理它們。NioServerSocketChannel只有一個,而NioAcceptedSocketChannel則會有許多,前者只需要一個pipeline與之關聯就夠了,而對後者來說則需要有一個工廠為它們中的每一個建立一個pipeline。而netty剛啟動完成時,還沒有client來連線,當然就不存在NioAcceptedSocketChannel了。但是這個pipeline factory總得先儲存到某個地方,待需要時能直接取用才行吧。NioServerSocketChannel剛好又是NioAcceptedSocketChannel的parent channel,那麼把pipeline factory作為其配置物件中的一部分儲存起來自然是個不錯的選擇。從這個工廠中創建出來的每個pipeline都裝著使用者自定義的handlers。自然而然地,pipeline factory需要由使用者在啟動netty server時指定(就在(1)處),此配置經由ServerBootstrap傳遞給由其建立的NioServerSocketChannel。而NioServerSocketChannel關聯的pipeline中要裝進包含什麼邏輯的handler則是固定不變的,所以由netty自己建立pipeline,自己實現好handler,即ServerBootstrap.Binder。 噴了這麼多口水,上張圖做個總結吧。
豐滿了不少細節後,我們再回頭看看整體的脈絡。
注意哦,本系列筆記是以netty-3.6.6-final原始碼和文件作為學習材料的,netty的迭代很快,總是不斷有新特性出現,特別是netty4和5,變化比較大,但是基本機制和模式沒有變,變的是概念模型,以及效能優化。