Netty是一個基於非同步與事件驅動的網路應用程式框架,它支援快速與簡單地開發可維護的高效能的伺服器與客戶端。
所謂事件驅動就是由通過各種事件響應來決定程式的流程,在Netty中到處都充滿了非同步與事件驅動,這種特點使得應用程式可以以任意的順序響應在任意的時間點產生的事件,它帶來了非常高的可伸縮性,讓你的應用可以在需要處理的工作不斷增長時,通過某種可行的方式或者擴大它的處理能力來適應這種增長。
Netty提供了高效能與易用性,它具有以下特點:
-
擁有設計良好且統一的API,支援NIO與OIO(阻塞IO)等多種傳輸型別,支援真正的無連線UDP Socket。
-
簡單而強大的執行緒模型,可高度定製執行緒(池)。
-
良好的模組化與解耦,支援可擴充套件和靈活的事件模型,可以很輕鬆地分離關注點以複用邏輯元件(可插拔的)。
-
效能高效,擁有比Java核心API更高的吞吐量,通過zero-copy功能以實現最少的記憶體複製消耗。
-
內建了許多常用的協議編解碼器,如HTTP、SSL、WebScoket等常見協議可以通過Netty做到開箱即用。使用者也可以利用Netty簡單方便地實現自己的應用層協議。
大多數人使用Netty主要還是為了提高應用的效能,而高效能則離不開非阻塞IO。Netty的非阻塞IO是基於Java NIO的,並且對其進行了封裝(直接使用Java NIO API在高複雜度下的應用中是一項非常繁瑣且容易出錯的操作,而Netty幫你封裝了這些複雜操作)。
NIO可以稱為New IO也可以稱為Non-blocking IO,它比Java舊的阻塞IO在效能上要高效許多(如果讓每一個連線中的IO操作都單獨建立一個執行緒,那麼阻塞IO並不會比NIO在效能上落後,但不可能建立無限多的執行緒,在連線數非常多的情況下會很糟糕)。
-
ByteBuffer:NIO的資料傳輸是基於緩衝區的,ByteBuffer正是NIO資料傳輸中所使用的緩衝區抽象。ByteBuffer支援在堆外分配記憶體,並且嘗試避免在執行I/O操作中的多餘複製。一般的I/O操作都需要進行系統呼叫,這樣會先切換到核心態,核心態要先從檔案讀取資料到它的緩衝區,只有等資料準備完畢後,才會從核心態把資料寫到使用者態,所謂的阻塞IO其實就是說的在等待資料準備好的這段時間內進行阻塞。如果想要避免這個額外的核心操作,可以通過使用mmap(虛擬記憶體對映)的方式來讓使用者態直接操作檔案。
-
Channel:它類似於檔案描述符,簡單地來說它代表了一個實體(如一個硬體裝置、檔案、Socket或者一個能夠執行一個或多個不同的I/O操作的程式元件)。你可以從一個Channel中讀取資料到緩衝區,也可以將一個緩衝區中的資料寫入到Channel。
-
Selector:選擇器是NIO實現的關鍵,NIO採用的是I/O多路複用的方式來實現非阻塞,Selector通過在一個執行緒中監聽每個Channel的IO事件來確定有哪些已經準備好進行IO操作的Channel,因此可以在任何時間檢查任意的讀操作或寫操作的完成狀態。這種方式避免了等待IO操作準備資料時的阻塞,使用較少的執行緒便可以處理許多連線,減少了執行緒切換與維護的開銷。
瞭解了NIO的實現思想之後,我覺得還很有必要了解一下Unix中的I/O模型,Unix中擁有以下5種I/O模型:
-
阻塞I/O(Blocking I/O)
-
非阻塞I/O(Non-blocking I/O)
-
I/O多路複用(I/O multiplexing (select and poll))
-
訊號驅動I/O(signal driven I/O (SIGIO))
-
非同步I/O(asynchronous I/O (the POSIX aio_functions))
阻塞I/O模型是最常見的I/O模型,通常我們使用的InputStream/OutputStream都是基於阻塞I/O模型。在上圖中,我們使用UDP作為例子,recvfrom()函式是UDP協議用於接收資料的函式,它需要使用系統呼叫並一直阻塞到核心將資料準備好,之後再由核心緩衝區複製資料到使用者態(即是recvfrom()接收到資料),所謂阻塞就是在等待核心準備資料的這段時間內什麼也不幹。
舉個生活中的例子,阻塞I/O就像是你去餐廳吃飯,在等待飯做好的時間段中,你只能在餐廳中坐著乾等(如果你在玩手機那麼這就是非阻塞I/O了)。
在非阻塞I/O模型中,核心在資料尚未準備好的情況下回返回一個錯誤碼EWOULDBLOCK
,而recvfrom並沒有在失敗的情況下選擇阻塞休眠,而是不斷地向核心詢問是否已經準備完畢,在上圖中,前三次核心都返回了EWOULDBLOCK
,直到第四次詢問時,核心資料準備完畢,然後開始將核心中快取的資料複製到使用者態。這種不斷詢問核心以檢視某種狀態是否完成的方式被稱為polling(輪詢)
。
非阻塞I/O就像是你在點外賣,只不過你非常心急,每隔一段時間就要打電話問外賣小哥有沒有到。
I/O多路複用的思想跟非阻塞I/O是一樣的,只不過在非阻塞I/O中,是在recvfrom的使用者態(或一個執行緒)中去輪詢核心,這種方式會消耗大量的CPU時間。而I/O多路複用則是通過select()或poll()系統呼叫來負責進行輪詢,以實現監聽I/O讀寫事件的狀態。如上圖中,select監聽到一個datagram可讀時,就交由recvfrom去傳送系統呼叫將核心中的資料複製到使用者態。
這種方式的優點很明顯,通過I/O多路複用可以監聽多個檔案描述符,且在核心中完成監控的任務。但缺點是至少需要兩個系統呼叫(select()與recvfrom())。
I/O多路複用同樣適用於點外賣這個例子,只不過你在等外賣的期間完全可以做自己的事情,當外賣到的時候會通過外賣APP或者由外賣小哥打電話來通知你。
Unix中提供了兩種I/O多路複用函式,select()和poll()。select()的相容性更好,但它在單個程式中所能監控的檔案描述符是有限的,這個值與FD_SETSIZE
相關,32位系統中預設為1024,64位系統中為2048。select()還有一個缺點就是他輪詢的方式,它採取了線性掃描的輪詢方式,每次都要遍歷FD_SETSIZE個檔案描述符,不管它們是否活不活躍的。poll()本質上與select()的實現沒有區別,不過在資料結構上區別很大,使用者必須分配一個pollfd結構陣列,該陣列維護在核心態中,正因如此,poll()並不像select()那樣擁有大小上限的限制,但缺點同樣也很明顯,大量的fd陣列會在使用者態與核心態之間不斷複製,不管這樣的複製是否有意義。
還有一種比select()與poll()更加高效的實現叫做epoll(),它是由Linux核心2.6推出的可伸縮的I/O多路複用實現,目的是為了替代select()與poll()。epoll()同樣沒有檔案描述符上限的限制,它使用一個檔案描述符來管理多個檔案描述符,並使用一個紅黑樹來作為儲存結構。同時它還支援邊緣觸發(edge-triggered)與水平觸發(level-triggered)兩種模式(poll()只支援水平觸發),在邊緣觸發模式下,epoll_wait
僅會在新的事件物件首次被加入到epoll時返回,而在水平觸發模式下,epoll_wait
會在事件狀態未變更前不斷地觸發。也就是說,邊緣觸發模式只會在檔案描述符變為就緒狀態時通知一次,水平觸發模式會不斷地通知該檔案描述符直到被處理。
關於epoll_wait
請參考如下epoll API。
// 建立一個epoll物件並返回它的檔案描述符。
// 引數flags允許修改epoll的行為,它只有一個有效值EPOLL_CLOEXEC。
int epoll_create1(int flags);
// 配置物件,該物件負責描述監控哪些檔案描述符和哪些事件。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 等待與epoll_ctl註冊的任何事件,直至事件發生一次或超時。
// 返回在events中發生的事件,最多同時返回maxevents個。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
複製程式碼
epoll另一亮點是採用了事件驅動的方式而不是輪詢,在epoll_ctl
中註冊的檔案描述符在事件觸發的時候會通過一個回撥機制來啟用該檔案描述符,epoll_wait
便可以收到通知。這樣效率就不會與檔案描述符的數量成正比。epoll還採用了mmap來減少核心態與使用者態之間的資料傳輸開銷。
在Java NIO2(從JDK1.7開始引入)中,只要Linux核心版本在2.6以上,就會採用epoll,如下原始碼所示(DefaultSelectorProvider.java)。
public static SelectorProvider create() {
String osname = AccessController.doPrivileged(
new GetPropertyAction("os.name"));
if ("SunOS".equals(osname)) {
return new sun.nio.ch.DevPollSelectorProvider();
}
// use EPollSelectorProvider for Linux kernels >= 2.6
if ("Linux".equals(osname)) {
String osversion = AccessController.doPrivileged(
new GetPropertyAction("os.version"));
String[] vers = osversion.split("\\.", 0);
if (vers.length >= 2) {
try {
int major = Integer.parseInt(vers[0]);
int minor = Integer.parseInt(vers[1]);
if (major > 2 || (major == 2 && minor >= 6)) {
return new sun.nio.ch.EPollSelectorProvider();
}
} catch (NumberFormatException x) {
// format not recognized
}
}
}
return new sun.nio.ch.PollSelectorProvider();
}
複製程式碼
訊號驅動I/O模型使用到了訊號,核心在資料準備就緒時會通過訊號來進行通知。我們首先開啟了一個訊號驅動I/O套接字,並使用sigaction系統呼叫來安裝訊號處理程式,核心直接返回,不會阻塞使用者態。當datagram準備好時,核心會傳送SIGIO訊號,recvfrom接收到訊號後會傳送系統呼叫開始進行I/O操作。
這種模型的優點是主程式(執行緒)不會被阻塞,當資料準備就緒時,通過訊號處理程式來通知主程式(執行緒)準備進行I/O操作與對資料的處理。
我們之前討論的各種I/O模型無論是阻塞還是非阻塞,它們所說的阻塞都是指的資料準備階段。非同步I/O模型同樣依賴於訊號處理程式來進行通知,但與以上I/O模型都不相同的是,非同步I/O模型通知的是I/O操作已經完成,而不是資料準備完成。
可以說非同步I/O模型才是真正的非阻塞,主程式只管做自己的事情,然後在I/O操作完成時呼叫回撥函式來完成一些對資料的處理操作即可。
閒扯了這麼多,想必大家已經對I/O模型有了一個深刻的認識。之後,我們將會結合部分原始碼(Netty4.X)來探討Netty中的各大核心元件,以及如何使用Netty,你會發現實現一個Netty程式是多麼簡單(而且還伴隨了高效能與可維護性)。
本文作者為SylvanasSun(sylvanas.sun@gmail.com),首發於SylvanasSun’s Blog。 原文連結:https://sylvanassun.github.io/2017/11/30/2017-11-30-netty_introduction/ (轉載請務必保留本段宣告,並且保留超連結。)
ByteBuf
網路傳輸的基本單位是位元組,在Java NIO中提供了ByteBuffer作為位元組緩衝區容器,但該類的API使用起來不太方便,所以Netty實現了ByteBuf作為其替代品,下面是使用ByteBuf的優點:
-
相比ByteBuffer使用起來更加簡單。
-
通過內建的複合緩衝區型別實現了透明的zero-copy。
-
容量可以按需增長。
-
讀和寫使用了不同的索引指標。
-
支援鏈式呼叫。
-
支援引用計數與池化。
-
可以被使用者自定義的緩衝區型別擴充套件。
在討論ByteBuf之前,我們先需要了解一下ByteBuffer的實現,這樣才能比較深刻地明白它們之間的區別。
ByteBuffer繼承於abstract class Buffer
(所以還有LongBuffer、IntBuffer等其他型別的實現),本質上它只是一個有限的線性的元素序列,包含了三個重要的屬性。
-
Capacity:緩衝區中元素的容量大小,你只能將capacity個數量的元素寫入緩衝區,一旦緩衝區已滿就需要清理緩衝區才能繼續寫資料。
-
Position:指向下一個寫入資料位置的索引指標,初始位置為0,最大為capacity-1。當寫模式轉換為讀模式時,position需要被重置為0。
-
Limit:在寫模式中,limit是可以寫入緩衝區的最大索引,也就是說它在寫模式中等價於緩衝區的容量。在讀模式中,limit表示可以讀取資料的最大索引。
由於Buffer中只維護了position一個索引指標,所以它在讀寫模式之間的切換需要呼叫一個flip()方法來重置指標。使用Buffer的流程一般如下:
-
寫入資料到緩衝區。
-
呼叫flip()方法。
-
從緩衝區中讀取資料
-
呼叫buffer.clear()或者buffer.compact()清理緩衝區,以便下次寫入資料。
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
// 分配一個48位元組大小的緩衝區
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf); // 讀取資料到緩衝區
while (bytesRead != -1) {
buf.flip(); // 將position重置為0
while(buf.hasRemaining()){
System.out.print((char) buf.get()); // 讀取資料並輸出到控制檯
}
buf.clear(); // 清理緩衝區
bytesRead = inChannel.read(buf);
}
aFile.close();
複製程式碼
Buffer中核心方法的實現也非常簡單,主要就是在操作指標position。
/**
* Sets this buffer's mark at its position.
*
* @return This buffer
*/
public final Buffer mark() {
mark = position; // mark屬性是用來標記當前索引位置的
return this;
}
// 將當前索引位置重置為mark所標記的位置
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
// 翻轉這個Buffer,將limit設定為當前索引位置,然後再把position重置為0
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
// 清理緩衝區
// 說是清理,也只是把postion與limit進行重置,之後再寫入資料就會覆蓋之前的資料了
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
// 返回剩餘空間
public final int remaining() {
return limit - position;
}
複製程式碼
Java NIO中的Buffer API操作的麻煩之處就在於讀寫轉換需要手動重置指標。而ByteBuf沒有這種繁瑣性,它維護了兩個不同的索引,一個用於讀取,一個用於寫入。當你從ByteBuf讀取資料時,它的readerIndex將會被遞增已經被讀取的位元組數,同樣的,當你寫入資料時,writerIndex則會遞增。readerIndex的最大範圍在writerIndex的所在位置,如果試圖移動readerIndex超過該值則會觸發異常。
ByteBuf中名稱以read或write開頭的方法將會遞增它們其對應的索引,而名稱以get或set開頭的方法則不會。ByteBuf同樣可以指定一個最大容量,試圖移動writerIndex超過該值則會觸發異常。
public byte readByte() {
this.checkReadableBytes0(1); // 檢查readerIndex是否已越界
int i = this.readerIndex;
byte b = this._getByte(i);
this.readerIndex = i + 1; // 遞增readerIndex
return b;
}
private void checkReadableBytes0(int minimumReadableBytes) {
this.ensureAccessible();
if(this.readerIndex > this.writerIndex - minimumReadableBytes) {
throw new IndexOutOfBoundsException(String.format("readerIndex(%d) + length(%d) exceeds writerIndex(%d): %s", new Object[]{Integer.valueOf(this.readerIndex), Integer.valueOf(minimumReadableBytes), Integer.valueOf(this.writerIndex), this}));
}
}
public ByteBuf writeByte(int value) {
this.ensureAccessible();
this.ensureWritable0(1); // 檢查writerIndex是否會越過capacity
this._setByte(this.writerIndex++, value);
return this;
}
private void ensureWritable0(int minWritableBytes) {
if(minWritableBytes > this.writableBytes()) {
if(minWritableBytes > this.maxCapacity - this.writerIndex) {
throw new IndexOutOfBoundsException(String.format("writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s", new Object[]{Integer.valueOf(this.writerIndex), Integer.valueOf(minWritableBytes), Integer.valueOf(this.maxCapacity), this}));
} else {
int newCapacity = this.alloc().calculateNewCapacity(this.writerIndex + minWritableBytes, this.maxCapacity);
this.capacity(newCapacity);
}
}
}
// get與set只對傳入的索引進行了檢查,然後對其位置進行get或set
public byte getByte(int index) {
this.checkIndex(index);
return this._getByte(index);
}
public ByteBuf setByte(int index, int value) {
this.checkIndex(index);
this._setByte(index, value);
return this;
}
複製程式碼
ByteBuf同樣支援在堆內和堆外進行分配。在堆內分配也被稱為支撐陣列模式,它能在沒有使用池化的情況下提供快速的分配和釋放。
ByteBuf heapBuf = Unpooled.copiedBuffer(bytes);
if (heapBuf.hasArray()) { // 判斷是否有一個支撐陣列
byte[] array = heapBuf.array();
// 計算第一個位元組的偏移量
int offset = heapBuf.arrayOffset() + heapBuf.readerIndex();
int length = heapBuf.readableBytes(); // 獲得可讀位元組
handleArray(array,offset,length); // 呼叫你的處理方法
}
複製程式碼
另一種模式為堆外分配,Java NIO ByteBuffer類在JDK1.4時就已經允許JVM實現通過JNI呼叫來在堆外分配記憶體(呼叫malloc()函式在JVM堆外分配記憶體),這主要是為了避免額外的緩衝區複製操作。
ByteBuf directBuf = Unpooled.directBuffer(capacity);
if (!directBuf.hasArray()) {
int length = directBuf.readableBytes();
byte[] array = new byte[length];
// 將位元組複製到陣列中
directBuf.getBytes(directBuf.readerIndex(),array);
handleArray(array,0,length);
}
複製程式碼
ByteBuf還支援第三種模式,它被稱為複合緩衝區,為多個ByteBuf提供了一個聚合檢視。在這個檢視中,你可以根據需要新增或者刪除ByteBuf例項,ByteBuf的子類CompositeByteBuf實現了該模式。
一個適合使用複合緩衝區的場景是HTTP協議,通過HTTP協議傳輸的訊息都會被分成兩部分——頭部和主體,如果這兩部分由應用程式的不同模組產生,將在訊息傳送時進行組裝,並且該應用程式還會為多個訊息複用相同的訊息主體,這樣對於每個訊息都將會建立一個新的頭部,產生了很多不必要的記憶體操作。使用CompositeByteBuf是一個很好的選擇,它消除了這些額外的複製,以幫助你複用這些訊息。
CompositeByteBuf messageBuf = Unpooled.compositeBuffer();
ByteBuf headerBuf = ....;
ByteBuf bodyBuf = ....;
messageBuf.addComponents(headerBuf,bodyBuf);
for (ByteBuf buf : messageBuf) {
System.out.println(buf.toString());
}
複製程式碼
CompositeByteBuf透明的實現了zero-copy,zero-copy其實就是避免資料在兩個記憶體區域中來回的複製。從作業系統層面上來講,zero-copy指的是避免在核心態與使用者態之間的資料緩衝區複製(通過mmap避免),而Netty中的zero-copy更偏向於在使用者態中的資料操作的優化,就像使用CompositeByteBuf來複用多個ByteBuf以避免額外的複製,也可以使用wrap()方法來將一個位元組陣列包裝成ByteBuf,又或者使用ByteBuf的slice()方法把它分割為多個共享同一記憶體區域的ByteBuf,這些都是為了優化記憶體的使用率。
那麼如何建立ByteBuf呢?在上面的程式碼中使用到了Unpooled,它是Netty提供的一個用於建立與分配ByteBuf的工具類,建議都使用這個工具類來建立你的緩衝區,不要自己去呼叫建構函式。經常使用的是wrappedBuffer()與copiedBuffer(),它們一個是用於將一個位元組陣列或ByteBuffer包裝為一個ByteBuf,一個是根據傳入的位元組陣列與ByteBuffer/ByteBuf來複製出一個新的ByteBuf。
// 通過array.clone()來複制一個陣列進行包裝
public static ByteBuf copiedBuffer(byte[] array) {
return array.length == 0?EMPTY_BUFFER:wrappedBuffer((byte[])array.clone());
}
// 預設是堆內分配
public static ByteBuf wrappedBuffer(byte[] array) {
return (ByteBuf)(array.length == 0?EMPTY_BUFFER:new UnpooledHeapByteBuf(ALLOC, array, array.length));
}
// 也提供了堆外分配的方法
private static final ByteBufAllocator ALLOC;
public static ByteBuf directBuffer(int initialCapacity) {
return ALLOC.directBuffer(initialCapacity);
}
複製程式碼
相對底層的分配方法是使用ByteBufAllocator,Netty實現了PooledByteBufAllocator和UnpooledByteBufAllocator,前者使用了jemalloc(一種malloc()的實現)來分配記憶體,並且實現了對ByteBuf的池化以提高效能。後者分配的是未池化的ByteBuf,其分配方式與之前講的一致。
Channel channel = ...;
ByteBufAllocator allocator = channel.alloc();
ByteBuf buffer = allocator.directBuffer();
do something.......
複製程式碼
為了優化記憶體使用率,Netty提供了一套手動的方式來追蹤不活躍物件,像UnpooledHeapByteBuf這種分配在堆內的物件得益於JVM的GC管理,無需額外操心,而UnpooledDirectByteBuf是在堆外分配的,它的內部基於DirectByteBuffer,DirectByteBuffer會先向Bits類申請一個額度(Bits還擁有一個全域性變數totalCapacity,記錄了所有DirectByteBuffer總大小),每次申請前都會檢視是否已經超過-XX:MaxDirectMemorySize所設定的上限,如果超限就會嘗試呼叫Sytem.gc(),以試圖回收一部分記憶體,然後休眠100毫秒,如果記憶體還是不足,則只能丟擲OOM異常。堆外記憶體的回收雖然有了這麼一層保障,但為了提高效能與使用率,主動回收也是很有必要的。由於Netty還實現了ByteBuf的池化,像PooledHeapByteBuf和PooledDirectByteBuf就必須依賴於手動的方式來進行回收(放回池中)。
Netty使用了引用計數器的方式來追蹤那些不活躍的物件。引用計數的介面為ReferenceCounted,它的思想很簡單,只要ByteBuf物件的引用計數大於0,就保證該物件不會被釋放回收,可以通過手動呼叫release()與retain()方法來操作該物件的引用計數值遞減或遞增。使用者也可以通過自定義一個ReferenceCounted的實現類,以滿足自定義的規則。
package io.netty.buffer;
public abstract class AbstractReferenceCountedByteBuf extends AbstractByteBuf {
// 由於ByteBuf的例項物件會非常多,所以這裡沒有將refCnt包裝為AtomicInteger
// 而是使用一個全域性的AtomicIntegerFieldUpdater來負責操作refCnt
private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> refCntUpdater = AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, "refCnt");
// 每個ByteBuf的初始引用值都為1
private volatile int refCnt = 1;
public int refCnt() {
return this.refCnt;
}
protected final void setRefCnt(int refCnt) {
this.refCnt = refCnt;
}
public ByteBuf retain() {
return this.retain0(1);
}
// 引用計數值遞增increment,increment必須大於0
public ByteBuf retain(int increment) {
return this.retain0(ObjectUtil.checkPositive(increment, "increment"));
}
public static int checkPositive(int i, String name) {
if(i <= 0) {
throw new IllegalArgumentException(name + ": " + i + " (expected: > 0)");
} else {
return i;
}
}
// 使用CAS操作不斷嘗試更新值
private ByteBuf retain0(int increment) {
int refCnt;
int nextCnt;
do {
refCnt = this.refCnt;
nextCnt = refCnt + increment;
if(nextCnt <= increment) {
throw new IllegalReferenceCountException(refCnt, increment);
}
} while(!refCntUpdater.compareAndSet(this, refCnt, nextCnt));
return this;
}
public boolean release() {
return this.release0(1);
}
public boolean release(int decrement) {
return this.release0(ObjectUtil.checkPositive(decrement, "decrement"));
}
private boolean release0(int decrement) {
int refCnt;
do {
refCnt = this.refCnt;
if(refCnt < decrement) {
throw new IllegalReferenceCountException(refCnt, -decrement);
}
} while(!refCntUpdater.compareAndSet(this, refCnt, refCnt - decrement));
if(refCnt == decrement) {
this.deallocate();
return true;
} else {
return false;
}
}
protected abstract void deallocate();
}
複製程式碼
Channel
Netty中的Channel與Java NIO的概念一樣,都是對一個實體或連線的抽象,但Netty提供了一套更加通用的API。就以網路套接字為例,在Java中OIO與NIO是截然不同的兩套API,假設你之前使用的是OIO而又想更改為NIO實現,那麼幾乎需要重寫所有程式碼。而在Netty中,只需要更改短短几行程式碼(更改Channel與EventLoop的實現類,如把OioServerSocketChannel替換為NioServerSocketChannel),就可以完成OIO與NIO(或其他)之間的轉換。
每個Channel最終都會被分配一個ChannelPipeline和ChannelConfig,前者持有所有負責處理入站與出站資料以及事件的ChannelHandler,後者包含了該Channel的所有配置設定,並且支援熱更新,由於不同的傳輸型別可能具有其特別的配置,所以該類可能會實現為ChannelConfig的不同子類。
Channel是執行緒安全的(與之後要講的執行緒模型有關),因此你完全可以在多個執行緒中複用同一個Channel,就像如下程式碼所示。
final Channel channel = ...
final ByteBuf buffer = Unpooled.copiedBuffer("Hello,World!", CharsetUtil.UTF_8).retain();
Runnable writer = new Runnable() {
@Override
public void run() {
channel.writeAndFlush(buffer.duplicate());
}
};
Executor executor = Executors.newCachedThreadPool();
executor.execute(writer);
executor.execute(writer);
.......
複製程式碼
Netty除了支援常見的NIO與OIO,還內建了其他的傳輸型別。
Nmae | Package | Description |
---|---|---|
NIO | io.netty.channel.socket.nio | 以Java NIO為基礎實現 |
OIO | io.netty.channel.socket.oio | 以java.net為基礎實現,使用阻塞I/O模型 |
Epoll | io.netty.channel.epoll | 由JNI驅動epoll()實現的更高效能的非阻塞I/O,它只能使用在Linux |
Local | io.netty.channel.local | 本地傳輸,在JVM內部通過管道進行通訊 |
Embedded | io.netty.channel.embedded | 允許在不需要真實網路傳輸的環境下使用ChannelHandler,主要用於對ChannelHandler進行測試 |
NIO、OIO、Epoll我們應該已經很熟悉了,下面主要說說Local與Embedded。
Local傳輸用於在同一個JVM中執行的客戶端和伺服器程式之間的非同步通訊,與伺服器Channel相關聯的SocketAddress並沒有繫結真正的物理網路地址,它會被儲存在登錄檔中,並在Channel關閉時登出。因此Local傳輸不會接受真正的網路流量,也就是說它不能與其他傳輸實現進行互操作。
Embedded傳輸主要用於對ChannelHandler進行單元測試,ChannelHandler是用於處理訊息的邏輯元件,Netty通過將入站訊息與出站訊息都寫入到EmbeddedChannel中的方式(提供了write/readInbound()與write/readOutbound()來讀寫入站與出站訊息)來實現對ChannelHandler的單元測試。
ChannelHandler
ChannelHandler充當了處理入站和出站資料的應用程式邏輯的容器,該類是基於事件驅動的,它會響應相關的事件然後去呼叫其關聯的回撥函式,例如當一個新的連線被建立時,ChannelHandler的channelActive()方法將會被呼叫。
關於入站訊息和出站訊息的資料流向定義,如果以客戶端為主視角來說的話,那麼從客戶端流向伺服器的資料被稱為出站,反之為入站。
入站事件是可能被入站資料或者相關的狀態更改而觸發的事件,包括:連線已被啟用、連線失活、讀取入站資料、使用者事件、發生異常等。
出站事件是未來將會觸發的某個動作的結果的事件,這些動作包括:開啟或關閉遠端節點的連線、將資料寫(或沖刷)到套接字。
ChannelHandler的主要用途包括:
-
對入站與出站資料的業務邏輯處理
-
記錄日誌
-
將資料從一種格式轉換為另一種格式,實現編解碼器。以一次HTTP協議(或者其他應用層協議)的流程為例,資料在網路傳輸時的單位為位元組,當客戶端傳送請求到伺服器時,伺服器需要通過解碼器(處理入站訊息)將位元組解碼為協議的訊息內容,伺服器在傳送響應的時候(處理出站訊息),還需要通過編碼器將訊息內容編碼為位元組。
-
捕獲異常
-
提供Channel生命週期內的通知,如Channel活動時與非活動時
Netty中到處都充滿了非同步與事件驅動,而回撥函式正是用於響應事件之後的操作。由於非同步會直接返回一個結果,所以Netty提供了ChannelFuture(實現了java.util.concurrent.Future)來作為非同步呼叫返回的佔位符,真正的結果會在未來的某個時刻完成,到時候就可以通過ChannelFuture對其進行訪問,每個Netty的出站I/O操作都將會返回一個ChannelFuture。
Netty還提供了ChannelFutureListener介面來監聽ChannelFuture是否成功,並採取對應的操作。
Channel channel = ...
ChannelFuture future = channel.connect(new InetSocketAddress("192.168.0.1",6666));
// 註冊一個監聽器
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
if (future.isSuccess()) {
// do something....
} else {
// 輸出錯誤資訊
Throwable cause = future.cause();
cause.printStackTrace();
// do something....
}
}
});
複製程式碼
ChannelFutureListener介面中還提供了幾個簡單的預設實現,方便我們使用。
package io.netty.channel;
import io.netty.channel.ChannelFuture;
import io.netty.util.concurrent.GenericFutureListener;
public interface ChannelFutureListener extends GenericFutureListener<ChannelFuture> {
// 在Future完成時關閉
ChannelFutureListener CLOSE = new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) {
future.channel().close();
}
};
// 如果失敗則關閉
ChannelFutureListener CLOSE_ON_FAILURE = new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) {
if(!future.isSuccess()) {
future.channel().close();
}
}
};
// 將異常資訊傳遞給下一個ChannelHandler
ChannelFutureListener FIRE_EXCEPTION_ON_FAILURE = new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) {
if(!future.isSuccess()) {
future.channel().pipeline().fireExceptionCaught(future.cause());
}
}
};
}
複製程式碼
ChannelHandler介面定義了對它生命週期進行監聽的回撥函式,在ChannelHandler被新增到ChannelPipeline或者被移除時都會呼叫這些函式。
package io.netty.channel;
public interface ChannelHandler {
void handlerAdded(ChannelHandlerContext var1) throws Exception;
void handlerRemoved(ChannelHandlerContext var1) throws Exception;
/** @deprecated */
@Deprecated
void exceptionCaught(ChannelHandlerContext var1, Throwable var2) throws Exception;
// 該註解表明這個ChannelHandler可被其他執行緒複用
@Inherited
@Documented
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Sharable {
}
}
複製程式碼
入站訊息與出站訊息由其對應的介面ChannelInboundHandler與ChannelOutboundHandler負責,這兩個介面定義了監聽Channel的生命週期的狀態改變事件的回撥函式。
package io.netty.channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
public interface ChannelInboundHandler extends ChannelHandler {
// 當channel被註冊到EventLoop時被呼叫
void channelRegistered(ChannelHandlerContext var1) throws Exception;
// 當channel已經被建立,但還未註冊到EventLoop(或者從EventLoop中登出)被呼叫
void channelUnregistered(ChannelHandlerContext var1) throws Exception;
// 當channel處於活動狀態(連線到遠端節點)被呼叫
void channelActive(ChannelHandlerContext var1) throws Exception;
// 當channel處於非活動狀態(沒有連線到遠端節點)被呼叫
void channelInactive(ChannelHandlerContext var1) throws Exception;
// 當從channel讀取資料時被呼叫
void channelRead(ChannelHandlerContext var1, Object var2) throws Exception;
// 當channel的上一個讀操作完成時被呼叫
void channelReadComplete(ChannelHandlerContext var1) throws Exception;
// 當ChannelInboundHandler.fireUserEventTriggered()方法被呼叫時被呼叫
void userEventTriggered(ChannelHandlerContext var1, Object var2) throws Exception;
// 當channel的可寫狀態發生改變時被呼叫
void channelWritabilityChanged(ChannelHandlerContext var1) throws Exception;
// 當處理過程中發生異常時被呼叫
void exceptionCaught(ChannelHandlerContext var1, Throwable var2) throws Exception;
}
package io.netty.channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import java.net.SocketAddress;
public interface ChannelOutboundHandler extends ChannelHandler {
// 當請求將Channel繫結到一個地址時被呼叫
// ChannelPromise是ChannelFuture的一個子介面,定義瞭如setSuccess(),setFailure()等方法
void bind(ChannelHandlerContext var1, SocketAddress var2, ChannelPromise var3) throws Exception;
// 當請求將Channel連線到遠端節點時被呼叫
void connect(ChannelHandlerContext var1, SocketAddress var2, SocketAddress var3, ChannelPromise var4) throws Exception;
// 當請求將Channel從遠端節點斷開時被呼叫
void disconnect(ChannelHandlerContext var1, ChannelPromise var2) throws Exception;
// 當請求關閉Channel時被呼叫
void close(ChannelHandlerContext var1, ChannelPromise var2) throws Exception;
// 當請求將Channel從它的EventLoop中登出時被呼叫
void deregister(ChannelHandlerContext var1, ChannelPromise var2) throws Exception;
// 當請求從Channel讀取資料時被呼叫
void read(ChannelHandlerContext var1) throws Exception;
// 當請求通過Channel將資料寫到遠端節點時被呼叫
void write(ChannelHandlerContext var1, Object var2, ChannelPromise var3) throws Exception;
// 當請求通過Channel將緩衝中的資料沖刷到遠端節點時被呼叫
void flush(ChannelHandlerContext var1) throws Exception;
}
複製程式碼
通過實現ChannelInboundHandler或者ChannelOutboundHandler就可以完成使用者自定義的應用邏輯處理程式,不過Netty已經幫你實現了一些基本操作,使用者只需要繼承並擴充套件ChannelInboundHandlerAdapter或ChannelOutboundHandlerAdapter來作為自定義實現的起始點。
ChannelInboundHandlerAdapter與ChannelOutboundHandlerAdapter都繼承於ChannelHandlerAdapter,該抽象類簡單實現了ChannelHandler介面。
public abstract class ChannelHandlerAdapter implements ChannelHandler {
boolean added;
public ChannelHandlerAdapter() {
}
// 該方法不允許將此ChannelHandler共享複用
protected void ensureNotSharable() {
if(this.isSharable()) {
throw new IllegalStateException("ChannelHandler " + this.getClass().getName() + " is not allowed to be shared");
}
}
// 使用反射判斷實現類有沒有@Sharable註解,以確認該類是否為可共享複用的
public boolean isSharable() {
Class clazz = this.getClass();
Map cache = InternalThreadLocalMap.get().handlerSharableCache();
Boolean sharable = (Boolean)cache.get(clazz);
if(sharable == null) {
sharable = Boolean.valueOf(clazz.isAnnotationPresent(Sharable.class));
cache.put(clazz, sharable);
}
return sharable.booleanValue();
}
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
}
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
}
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.fireExceptionCaught(cause);
}
}
複製程式碼
ChannelInboundHandlerAdapter與ChannelOutboundHandlerAdapter預設只是簡單地將請求傳遞給ChannelPipeline中的下一個ChannelHandler,原始碼如下:
public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelInboundHandler {
public ChannelInboundHandlerAdapter() {
}
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelRegistered();
}
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelUnregistered();
}
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelActive();
}
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelInactive();
}
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.fireChannelRead(msg);
}
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelReadComplete();
}
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
ctx.fireUserEventTriggered(evt);
}
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelWritabilityChanged();
}
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.fireExceptionCaught(cause);
}
}
public class ChannelOutboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelOutboundHandler {
public ChannelOutboundHandlerAdapter() {
}
public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception {
ctx.bind(localAddress, promise);
}
public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) throws Exception {
ctx.connect(remoteAddress, localAddress, promise);
}
public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
ctx.disconnect(promise);
}
public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
ctx.close(promise);
}
public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
ctx.deregister(promise);
}
public void read(ChannelHandlerContext ctx) throws Exception {
ctx.read();
}
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ctx.write(msg, promise);
}
public void flush(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
}
複製程式碼
對於處理入站訊息,另外一種選擇是繼承SimpleChannelInboundHandler,它是Netty的一個繼承於ChannelInboundHandlerAdapter的抽象類,並在其之上實現了自動釋放資源的功能。
我們在瞭解ByteBuf時就已經知道了Netty使用了一套自己實現的引用計數演算法來主動釋放資源,假設你的ChannelHandler繼承於ChannelInboundHandlerAdapter或ChannelOutboundHandlerAdapter,那麼你就有責任去管理你所分配的ByteBuf,一般來說,一個訊息物件(ByteBuf)已經被消費(或丟棄)了,並且不會傳遞給ChannelHandler鏈中的下一個處理器(如果該訊息到達了實際的傳輸層,那麼當它被寫入或Channel關閉時,都會被自動釋放),那麼你就需要去手動釋放它。通過一個簡單的工具類ReferenceCountUtil的release方法,就可以做到這一點。
// 這個泛型為訊息物件的型別
public abstract class SimpleChannelInboundHandler<I> extends ChannelInboundHandlerAdapter {
private final TypeParameterMatcher matcher;
private final boolean autoRelease;
protected SimpleChannelInboundHandler() {
this(true);
}
protected SimpleChannelInboundHandler(boolean autoRelease) {
this.matcher = TypeParameterMatcher.find(this, SimpleChannelInboundHandler.class, "I");
this.autoRelease = autoRelease;
}
protected SimpleChannelInboundHandler(Class<? extends I> inboundMessageType) {
this(inboundMessageType, true);
}
protected SimpleChannelInboundHandler(Class<? extends I> inboundMessageType, boolean autoRelease) {
this.matcher = TypeParameterMatcher.get(inboundMessageType);
this.autoRelease = autoRelease;
}
public boolean acceptInboundMessage(Object msg) throws Exception {
return this.matcher.match(msg);
}
// SimpleChannelInboundHandler只是替你做了ReferenceCountUtil.release()
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
boolean release = true;
try {
if(this.acceptInboundMessage(msg)) {
this.channelRead0(ctx, msg);
} else {
release = false;
ctx.fireChannelRead(msg);
}
} finally {
if(this.autoRelease && release) {
ReferenceCountUtil.release(msg);
}
}
}
// 這個方法才是我們需要實現的方法
protected abstract void channelRead0(ChannelHandlerContext var1, I var2) throws Exception;
}
// ReferenceCountUtil中的原始碼,release方法對訊息物件的型別進行判斷然後呼叫它的release()方法
public static boolean release(Object msg) {
return msg instanceof ReferenceCounted?((ReferenceCounted)msg).release():false;
}
複製程式碼
ChannelPipeline
為了模組化與解耦合,不可能由一個ChannelHandler來完成所有應用邏輯,所以Netty採用了攔截器鏈的設計。ChannelPipeline就是用來管理ChannelHandler例項鏈的容器,它的職責就是保證例項鏈的流動。
每一個新建立的Channel都將會被分配一個新的ChannelPipeline,這種關聯關係是永久性的,一個Channel一生只能對應一個ChannelPipeline。
一個入站事件被觸發時,它會先從ChannelPipeline的最左端(頭部)開始一直傳播到ChannelPipeline的最右端(尾部),而出站事件正好與入站事件順序相反(從最右端一直傳播到最左端)。這個順序是定死的,Netty總是將ChannelPipeline的入站口作為頭部,而將出站口作為尾部。在事件傳播的過程中,ChannelPipeline會判斷下一個ChannelHandler的型別是否和事件的運動方向相匹配,如果不匹配,就跳過該ChannelHandler並繼續檢查下一個(保證入站事件只會被ChannelInboundHandler處理),一個ChannelHandler也可以同時實現ChannelInboundHandler與ChannelOutboundHandler,它在入站事件與出站事件中都會被呼叫。
在閱讀ChannelHandler的原始碼時,發現很多方法需要一個ChannelHandlerContext型別的引數,該介面是ChannelPipeline與ChannelHandler之間相關聯的關鍵。ChannelHandlerContext可以通知ChannelPipeline中的當前ChannelHandler的下一個ChannelHandler,還可以動態地改變當前ChannelHandler在ChannelPipeline中的位置(通過呼叫ChannelPipeline中的各種方法來修改)。
ChannelHandlerContext負責了在同一個ChannelPipeline中的ChannelHandler與其他ChannelHandler之間的互動,每個ChannelHandlerContext都對應了一個ChannelHandler。在DefaultChannelPipeline的原始碼中,已經表現的很明顯了。
public class DefaultChannelPipeline implements ChannelPipeline {
.........
// 頭部節點和尾部節點的引用變數
// ChannelHandlerContext在ChannelPipeline中是以連結串列的形式組織的
final AbstractChannelHandlerContext head;
final AbstractChannelHandlerContext tail;
.........
// 新增一個ChannelHandler到連結串列尾部
public final ChannelPipeline addLast(String name, ChannelHandler handler) {
return this.addLast((EventExecutorGroup)null, name, handler);
}
public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
final AbstractChannelHandlerContext newCtx;
synchronized(this) {
// 檢查ChannelHandler是否為一個共享物件(@Sharable)
// 如果該ChannelHandler沒有@Sharable註解,並且是已被新增過的那麼就丟擲異常
checkMultiplicity(handler);
// 返回一個DefaultChannelHandlerContext,注意該物件持有了傳入的ChannelHandler
newCtx = this.newContext(group, this.filterName(name, handler), handler);
this.addLast0(newCtx);
// 如果當前ChannelPipeline沒有被註冊,那麼就先加到未決連結串列中
if(!this.registered) {
newCtx.setAddPending();
this.callHandlerCallbackLater(newCtx, true);
return this;
}
// 否則就呼叫ChannelHandler中的handlerAdded()
EventExecutor executor = newCtx.executor();
if(!executor.inEventLoop()) {
newCtx.setAddPending();
executor.execute(new Runnable() {
public void run() {
DefaultChannelPipeline.this.callHandlerAdded0(newCtx);
}
});
return this;
}
}
this.callHandlerAdded0(newCtx);
return this;
}
// 將新的ChannelHandlerContext插入到尾部與尾部之前的節點之間
private void addLast0(AbstractChannelHandlerContext newCtx) {
AbstractChannelHandlerContext prev = this.tail.prev;
newCtx.prev = prev;
newCtx.next = this.tail;
prev.next = newCtx;
this.tail.prev = newCtx;
}
.....
}
複製程式碼
ChannelHandlerContext還定義了許多與Channel和ChannelPipeline重合的方法(像read()、write()、connect()這些用於出站的方法或者如fireChannelXXXX()這樣用於入站的方法),不同之處在於呼叫Channel或者ChannelPipeline上的這些方法,它們將會從頭沿著整個ChannelHandler例項鏈進行傳播,而呼叫位於ChannelHandlerContext上的相同方法,則會從當前所關聯的ChannelHandler開始,且只會傳播給例項鏈中的下一個ChannelHandler。而且,事件之間的移動(從一個ChannelHandler到下一個ChannelHandler)也是通過ChannelHandlerContext中的方法呼叫完成的。
public class DefaultChannelPipeline implements ChannelPipeline {
public final ChannelPipeline fireChannelRead(Object msg) {
// 注意這裡將頭節點傳入了進去
AbstractChannelHandlerContext.invokeChannelRead(this.head, msg);
return this;
}
}
abstract class AbstractChannelHandlerContext extends DefaultAttributeMap implements ChannelHandlerContext, ResourceLeakHint {
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 {
executor.execute(new Runnable() {
public void run() {
next.invokeChannelRead(m);
}
});
}
}
private void invokeChannelRead(Object msg) {
if(this.invokeHandler()) {
try {
((ChannelInboundHandler)this.handler()).channelRead(this, msg);
} catch (Throwable var3) {
this.notifyHandlerException(var3);
}
} else {
// 尋找下一個ChannelHandler
this.fireChannelRead(msg);
}
}
public ChannelHandlerContext fireChannelRead(Object msg) {
invokeChannelRead(this.findContextInbound(), msg);
return this;
}
private AbstractChannelHandlerContext findContextInbound() {
AbstractChannelHandlerContext ctx = this;
do {
ctx = ctx.next;
} while(!ctx.inbound); // 直到找到一個ChannelInboundHandler
return ctx;
}
}
複製程式碼
EventLoop
為了最大限度地提供高效能和可維護性,Netty設計了一套強大又易用的執行緒模型。在一個網路框架中,最重要的能力是能夠快速高效地處理在連線的生命週期內發生的各種事件,與之相匹配的程式構造被稱為事件迴圈,Netty定義了介面EventLoop來負責這項工作。
如果是經常用Java進行多執行緒開發的童鞋想必經常會使用到執行緒池,也就是Executor這套API。Netty就是從Executor(java.util.concurrent)之上擴充套件了自己的EventExecutorGroup(io.netty.util.concurrent),同時為了與Channel的事件進行互動,還擴充套件了EventLoopGroup介面(io.netty.channel)。在io.netty.util.concurrent包下的EventExecutorXXX負責實現執行緒併發相關的工作,而在io.netty.channel包下的EventLoopXXX負責實現網路程式設計相關的工作(處理Channel中的事件)。
在Netty的執行緒模型中,一個EventLoop將由一個永遠不會改變的Thread驅動,而一個Channel一生只會使用一個EventLoop(但是一個EventLoop可能會被指派用於服務多個Channel),在Channel中的所有I/O操作和事件都由EventLoop中的執行緒處理,也就是說一個Channel的一生之中都只會使用到一個執行緒。不過在Netty3,只有入站事件會被EventLoop處理,所有出站事件都會由呼叫執行緒處理,這種設計導致了ChannelHandler的執行緒安全問題。Netty4簡化了執行緒模型,通過在同一個執行緒處理所有事件,既解決了這個問題,還提供了一個更加簡單的架構。
package io.netty.channel;
public abstract class SingleThreadEventLoop extends SingleThreadEventExecutor implements EventLoop {
protected static final int DEFAULT_MAX_PENDING_TASKS = Math.max(16, SystemPropertyUtil.getInt("io.netty.eventLoop.maxPendingTasks", 2147483647));
private final Queue<Runnable> tailTasks;
protected SingleThreadEventLoop(EventLoopGroup parent, ThreadFactory threadFactory, boolean addTaskWakesUp) {
this(parent, threadFactory, addTaskWakesUp, DEFAULT_MAX_PENDING_TASKS, RejectedExecutionHandlers.reject());
}
protected SingleThreadEventLoop(EventLoopGroup parent, Executor executor, boolean addTaskWakesUp) {
this(parent, executor, addTaskWakesUp, DEFAULT_MAX_PENDING_TASKS, RejectedExecutionHandlers.reject());
}
protected SingleThreadEventLoop(EventLoopGroup parent, ThreadFactory threadFactory, boolean addTaskWakesUp, int maxPendingTasks, RejectedExecutionHandler rejectedExecutionHandler) {
super(parent, threadFactory, addTaskWakesUp, maxPendingTasks, rejectedExecutionHandler);
this.tailTasks = this.newTaskQueue(maxPendingTasks);
}
protected SingleThreadEventLoop(EventLoopGroup parent, Executor executor, boolean addTaskWakesUp, int maxPendingTasks, RejectedExecutionHandler rejectedExecutionHandler) {
super(parent, executor, addTaskWakesUp, maxPendingTasks, rejectedExecutionHandler);
this.tailTasks = this.newTaskQueue(maxPendingTasks);
}
// 返回它所在的EventLoopGroup
public EventLoopGroup parent() {
return (EventLoopGroup)super.parent();
}
public EventLoop next() {
return (EventLoop)super.next();
}
// 註冊Channel,這裡ChannelPromise和Channel關聯到了一起
public ChannelFuture register(Channel channel) {
return this.register((ChannelPromise)(new DefaultChannelPromise(channel, this)));
}
public ChannelFuture register(ChannelPromise promise) {
ObjectUtil.checkNotNull(promise, "promise");
promise.channel().unsafe().register(this, promise);
return promise;
}
// 剩下這些函式都是用於排程任務
public final void executeAfterEventLoopIteration(Runnable task) {
ObjectUtil.checkNotNull(task, "task");
if(this.isShutdown()) {
reject();
}
if(!this.tailTasks.offer(task)) {
this.reject(task);
}
if(this.wakesUpForTask(task)) {
this.wakeup(this.inEventLoop());
}
}
final boolean removeAfterEventLoopIterationTask(Runnable task) {
return this.tailTasks.remove(ObjectUtil.checkNotNull(task, "task"));
}
protected boolean wakesUpForTask(Runnable task) {
return !(task instanceof SingleThreadEventLoop.NonWakeupRunnable);
}
protected void afterRunningAllTasks() {
this.runAllTasksFrom(this.tailTasks);
}
protected boolean hasTasks() {
return super.hasTasks() || !this.tailTasks.isEmpty();
}
public int pendingTasks() {
return super.pendingTasks() + this.tailTasks.size();
}
interface NonWakeupRunnable extends Runnable {
}
}
複製程式碼
為了確保一個Channel的整個生命週期中的I/O事件會被一個EventLoop負責,Netty通過inEventLoop()方法來判斷當前執行的執行緒的身份,確定它是否是分配給當前Channel以及它的EventLoop的那一個執行緒。如果當前(呼叫)執行緒正是EventLoop中的執行緒,那麼所提交的任務將會被直接執行,否則,EventLoop將排程該任務以便稍後執行,並將它放入內部的任務佇列(每個EventLoop都有它自己的任務佇列,從SingleThreadEventLoop的原始碼就能發現很多用於排程內部任務佇列的方法),在下次處理它的事件時,將會執行佇列中的那些任務。這種設計可以讓任何執行緒與Channel直接互動,而無需在ChannelHandler中進行額外的同步。
從效能上來考慮,千萬不要將一個需要長時間來執行的任務放入到任務佇列中,它會影響到該佇列中的其他任務的執行。解決方案是使用一個專門的EventExecutor來執行它(ChannelPipeline提供了帶有EventExecutorGroup引數的addXXX()方法,該方法可以將傳入的ChannelHandler繫結到你傳入的EventExecutor之中),這樣它就會在另一條執行緒中執行,與其他任務隔離。
public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor {
.....
public void execute(Runnable task) {
if(task == null) {
throw new NullPointerException("task");
} else {
boolean inEventLoop = this.inEventLoop();
if(inEventLoop) {
this.addTask(task);
} else {
this.startThread();
this.addTask(task);
if(this.isShutdown() && this.removeTask(task)) {
reject();
}
}
if(!this.addTaskWakesUp && this.wakesUpForTask(task)) {
this.wakeup(inEventLoop);
}
}
}
public boolean inEventLoop(Thread thread) {
return thread == this.thread;
}
.....
}
複製程式碼
EventLoopGroup負責管理和分配EventLoop(建立EventLoop和為每個新建立的Channel分配EventLoop),根據不同的傳輸型別,EventLoop的建立和分配方式也不同。例如,使用NIO傳輸型別,EventLoopGroup就會只使用較少的EventLoop(一個EventLoop服務於多個Channel),這是因為NIO基於I/O多路複用,一個執行緒可以處理多個連線,而如果使用的是OIO,那麼新建立一個Channel(連線)就需要分配一個EventLoop(執行緒)。
Bootstrap
在深入瞭解地Netty的核心元件之後,發現它們的設計都很模組化,如果想要實現你自己的應用程式,就需要將這些元件組裝到一起。Netty通過Bootstrap類,以對一個Netty應用程式進行配置(組裝各個元件),並最終使它執行起來。對於客戶端程式和伺服器程式所使用到的Bootstrap類是不同的,後者需要使用ServerBootstrap,這樣設計是因為,在如TCP這樣有連線的協議中,伺服器程式往往需要一個以上的Channel,通過父Channel來接受來自客戶端的連線,然後建立子Channel用於它們之間的通訊,而像UDP這樣無連線的協議,它不需要每個連線都建立子Channel,只需要一個Channel即可。
一個比較明顯的差異就是Bootstrap與ServerBootstrap的group()方法,後者提供了一個接收2個EventLoopGroup的版本。
// 該方法在Bootstrap的父類AbstractBootstrap中,泛型B為它當前子類的型別(為了鏈式呼叫)
public B group(EventLoopGroup group) {
if(group == null) {
throw new NullPointerException("group");
} else if(this.group != null) {
throw new IllegalStateException("group set already");
} else {
this.group = group;
return this;
}
}
// ServerBootstrap中的實現,它也支援只用一個EventLoopGroup
public ServerBootstrap group(EventLoopGroup group) {
return this.group(group, group);
}
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
super.group(parentGroup);
if(childGroup == null) {
throw new NullPointerException("childGroup");
} else if(this.childGroup != null) {
throw new IllegalStateException("childGroup set already");
} else {
this.childGroup = childGroup;
return this;
}
}
複製程式碼
Bootstrap其實沒有什麼可以好說的,它就只是一個裝配工,將各個元件拼裝組合到一起,然後進行一些配置,有關它的詳細API請參考Netty JavaDoc。下面我們將通過一個經典的Echo客戶端與伺服器的例子,來梳理一遍建立Netty應用的流程。
首先實現的是伺服器,我們先實現一個EchoServerInboundHandler,處理入站訊息。
public class EchoServerInboundHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf) msg;
System.out.printf("Server received: %s \n", in.toString(CharsetUtil.UTF_8));
// 由於讀事件不是一次性就能把完整訊息傳送過來的,這裡並沒有呼叫writeAndFlush
ctx.write(in); // 直接把訊息寫回給客戶端(會被出站訊息處理器處理,不過我們的應用沒有實現任何出站訊息處理器)
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// 等讀事件已經完成時,沖刷之前寫資料的緩衝區
// 然後新增了一個監聽器,它會在Future完成時進行關閉該Channel.
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
.addListener(ChannelFutureListener.CLOSE);
}
// 處理異常,輸出異常資訊,然後關閉Channel
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
複製程式碼
伺服器的應用邏輯只有這麼多,剩下就是用ServerBootstrap進行配置了。
public class EchoServer {
private final int port;
public EchoServer(int port) {
this.port = port;
}
public void start() throws Exception {
final EchoServerInboundHandler serverHandler = new EchoServerInboundHandler();
EventLoopGroup group = new NioEventLoopGroup(); // 傳輸型別使用NIO
try {
ServerBootstrap b = new ServerBootstrap();
b.group(group) // 配置EventLoopGroup
.channel(NioServerSocketChannel.class) // 配置Channel的型別
.localAddress(new InetSocketAddress(port)) // 配置埠號
.childHandler(new ChannelInitializer<SocketChannel>() {
// 實現一個ChannelInitializer,它可以方便地新增多個ChannelHandler
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(serverHandler);
}
});
// i繫結地址,同步等待它完成
ChannelFuture f = b.bind().sync();
// 關閉這個Future
f.channel().closeFuture().sync();
} finally {
// 關閉應用程式,一般來說Netty應用只需要呼叫這個方法就夠了
group.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.printf(
"Usage: %s <port> \n",
EchoServer.class.getSimpleName()
);
return;
}
int port = Integer.parseInt(args[0]);
new EchoServer(port).start();
}
}
複製程式碼
接下來實現客戶端,同樣需要先實現一個入站訊息處理器。
public class EchoClientInboundHandler extends SimpleChannelInboundHandler<ByteBuf> {
/**
* 我們在Channel連線到遠端節點直接傳送一條訊息給伺服器
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("Hello, Netty!", CharsetUtil.UTF_8));
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
// 輸出從伺服器Echo的訊息
System.out.printf("Client received: %s \n", byteBuf.toString(CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
複製程式碼
然後配置客戶端。
public class EchoClient {
private final String host;
private final int port;
public EchoClient(String host, int port) {
this.host = host;
this.port = port;
}
public void start() throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.remoteAddress(new InetSocketAddress(host, port)) // 伺服器的地址
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new EchoClientInboundHandler());
}
});
ChannelFuture f = b.connect().sync(); // 連線到伺服器
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws Exception {
if (args.length != 2) {
System.err.printf("Usage: %s <host> <port> \n", EchoClient.class.getSimpleName());
return;
}
String host = args[0];
int port = Integer.parseInt(args[1]);
new EchoClient(host, port).start();
}
}
複製程式碼
實現一個Netty應用程式就是如此簡單,使用者大多數都是在編寫各種應用邏輯的ChannelHandler(或者使用Netty內建的各種實用ChannelHandler),然後只需要將它們全部新增到ChannelPipeline即可。