,其中**T就是這個Handler處理的資料的型別**(上一個Handler已經替我們解碼好了),訊息到達這個Handler時,Netty會自動呼叫這個Handler中的**channelRead0**(ChannelHandlerContext,T)方法,T是傳遞過來的資料物件,在這個方法中我們便可以任意寫我們的業務邏輯了。
Netty從某方面來說就是一套NIO框架,在Java NIO基礎上做了封裝,所以要想學好Netty我建議先理解好Java NIO,
NIO可以稱為New IO也可以稱為Non-blocking IO,它比Java舊的阻塞IO在效能上要高效許多(如果讓每一個連線中的IO操作都單獨建立一個執行緒,那麼阻塞IO並不會比NIO在效能上落後,但不可能建立無限多的執行緒,在連線數非常多的情況下會很糟糕)。
* ByteBuffer:NIO的資料傳輸是基於緩衝區的,ByteBuffer正是NIO資料傳輸中所使用的緩衝區抽象。**ByteBuffer支援在堆外分配記憶體**,並且嘗試避免在執行I/O操作中的多餘複製。一般的I/O操作都需要進行系統呼叫,這樣會先切換到核心態,核心態要先從檔案讀取資料到它的緩衝區,只有等資料準備完畢後,才會從核心態把資料寫到使用者態,所謂的**阻塞IO**其實就是說的**在等待資料準備好的這段時間內進行阻塞**。如果想要避免這個額外的核心操作,可以透過使用mmap(虛擬記憶體對映)的方式來讓使用者態直接操作檔案。
* Channel:它類似於(fd)檔案描述符,簡單地來說它**代表了一個實體**(如一個硬體裝置、檔案、Socket或者一個能夠執行一個或多個不同的I/O操作的程式元件)。你可以從一個Channel中讀取資料到緩衝區,也可以將一個緩衝區中的資料寫入到Channel。
* Selector:選擇器是NIO實現的關鍵,NIO採用的是I/O多路複用的方式來實現非阻塞,Selector透過在**一個執行緒中監聽每個Channel的IO事件來確定有哪些已經準備好進行IO操作的Channel**,因此可**以在任何時間檢查任意的讀操作或寫操作的完成狀態**。這種方式**避免了等待IO操作準備資料時的阻塞**,使用較少的執行緒便可以處理許多連線,減少了執行緒切換與維護的開銷。
[![](http://wx2.sinaimg.cn/large/63503acbly1flys7n7hvaj20h90doglj.jpg)](http://wx2.sinaimg.cn/large/63503acbly1flys7n7hvaj20h90doglj.jpg)
瞭解了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))
[![](http://wx3.sinaimg.cn/large/63503acbly1flz1e7kzblj20wb0ftq3l.jpg)](http://wx3.sinaimg.cn/large/63503acbly1flz1e7kzblj20wb0ftq3l.jpg)
![](file:///C:/Users/xiaok/Pictures/%E5%A4%8D%E4%B9%A0%E6%96%87%E6%A1%A3%E5%9B%BE%E7%89%87_%E5%8B%BF%E5%88%A0/%E6%AF%94%E7%89%B9%E6%88%AA%E5%9B%BE2019-01-12-16-40-18.png?lastModify=1549709160)
阻塞I/O模型是最常見的I/O模型,通常我們使用的InputStream/OutputStream都是基於阻塞I/O模型。在上圖中,我們使用UDP作為例子,recvfrom()函式是UDP協議用於接收資料的函式,它需要**使用系統呼叫並一直阻塞到核心將資料準備好**,之後再由核心緩衝區複製資料到使用者態(即是recvfrom()接收到資料),所謂**阻塞就是在等待核心準備資料的這段時間內什麼也不幹**。
舉個生活中的例子,阻塞I/O就像是你去餐廳吃飯,在等待飯做好的時間段中,你只能在餐廳中坐著乾等(如果你在玩手機那麼這就是非阻塞I/O了)。
[![](http://wx2.sinaimg.cn/large/63503acbly1flz1e8lh7rj20wb0ft0ty.jpg)](http://wx2.sinaimg.cn/large/63503acbly1flz1e8lh7rj20wb0ft0ty.jpg)
> 在非阻塞I/O模型中,核心在**資料尚未準備好**的情況下回**返回一個錯誤碼`EWOULDBLOCK`**,而**recvfrom**並沒有在失敗的情況下選擇阻塞休眠,而是**不斷地向核心詢問是否已經準備完畢**,在上圖中,前三次核心都返回了`EWOULDBLOCK`,直到第四次詢問時,核心資料準備完畢,然後開始將核心中快取的資料複製到使用者態。這種不斷詢問核心以檢視某種狀態是否完成的方式被稱為**polling(輪詢)**。
非阻塞I/O就像是你在點外賣,只不過你非常心急,**每隔一段時間就要打電話問外賣小哥有沒有到**。
[![](http://wx3.sinaimg.cn/large/63503acbly1flz1e989dfj20wh0g80tw.jpg)](http://wx3.sinaimg.cn/large/63503acbly1flz1e989dfj20wh0g80tw.jpg)
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`便可以收到通知**。這樣效率就不會與檔案描述符的數量成正比
在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();
}
[![](http://wx3.sinaimg.cn/large/63503acbly1flz1e9uk8aj20wb0ft3zn.jpg)](http://wx3.sinaimg.cn/large/63503acbly1flz1e9uk8aj20wb0ft3zn.jpg)
訊號驅動I/O模型使用到了**訊號**,核心在資料**準備就緒**時會**透過訊號來進行通知**。我們首先**開啟**了一個訊號驅動I/O套接字,並使用sigaction系統呼叫來安裝訊號處理程式,核心直接返回,不會阻塞使用者態。當datagram準備好時,核心會傳送SIGIN訊號,recvfrom接收到訊號後會傳送系統呼叫開始進行I/O操作。
這種模型的優點是**主程式(執行緒)不會被阻塞**,當資料準備就緒時,**透過訊號處理程式**來通知主程式(執行緒)準備進行I/O操作與對資料的處理。
[![](http://wx2.sinaimg.cn/large/63503acbly1flz1eai66rj20wb0g8aau.jpg)](http://wx2.sinaimg.cn/large/63503acbly1flz1eai66rj20wb0g8aau.jpg)
我們之前討論的各種I/O模型無論是阻塞還是非阻塞,它們所說的**阻塞都是指的資料準備階段**。**非同步I/O**模型**同樣依賴**於**訊號**處理程式來進行通知,但與以上I/O模型都不相同的是,非同步I/O模型通知的是**I/O操作**已經完成,而不是**資料準備**完成。
可以說**非同步I/O模型才是真正的非阻塞**,主程式只管做自己的事情,然後在I/O操作完成時呼叫回撥函式來完成一些對資料的處理操作即可。
閒扯了這麼多,想必大家已經對I/O模型有了一個深刻的認識。之後,我們將會結合部分原始碼(Netty4.X)來探討Netty中的各大核心元件,以及如何使用Netty,你會發現實現一個Netty程式是多麼簡單(而且還伴隨了高效能與可維護性)。
![](https://netty.io/images/components.png)
### 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表示可以讀取資料的最大索引。
[![](http://tutorials.jenkov.com/images/java-nio/buffers-modes.png)](http://tutorials.jenkov.com/images/java-nio/buffers-modes.png)
由於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。
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);
}
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所設定的上限,**如果超限就會嘗試呼叫System.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 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(或其他)之間的轉換。
[![](http://wx2.sinaimg.cn/large/63503acbly1fm103i127ej20xe0f074y.jpg)](http://wx2.sinaimg.cn/large/63503acbly1fm103i127ej20xe0f074y.jpg)
每個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 {
// 在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與ChannelOutboundHandle**r負責,這兩個介面定義了監聽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 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) {
//ByteBuf的釋放
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。
[![](http://wx3.sinaimg.cn/large/63503acbly1fm1er9l4jfj213h0fcq3d.jpg)](http://wx3.sinaimg.cn/large/63503acbly1fm1er9l4jfj213h0fcq3d.jpg)
一個入站事件被觸發時,它會先從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中的事件)。
[![](http://wx3.sinaimg.cn/large/63503acbly1fm296hz0p9j20ff0kc3z2.jpg)](http://wx3.sinaimg.cn/large/63503acbly1fm296hz0p9j20ff0kc3z2.jpg)
在Netty的執行緒模型中,一個EventLoop將由一個永遠不會改變的Thread驅動,而一個Channel一生只會使用一個EventLoop(但是一個EventLoop可能會被