零、寫在前面
本文雖然是講Netty,但實(shí)際更關(guān)注的是Netty中的NIO的實(shí)現(xiàn),所以對于Netty中的OIO(Old I/O)并沒有做過多的描述,或者說根本只字未提,所以本文中所述的所有實(shí)現(xiàn)細(xì)節(jié)都是基于NIO版本的。
Netty作為一個(gè)已經(jīng)發(fā)展了十多年的框架,已然非常成熟了,其中有大量的細(xì)節(jié)是普通使用者不知道或者不關(guān)心的,所以本文難免有遺漏或者紕漏的地方,如果你發(fā)現(xiàn)了請告知。
本文不涉及Netty5的部分。
雖然這一節(jié)叫「寫在前面」,但實(shí)際上是最后寫的。
一、零拷貝
Netty4和Netty3中的buffer
包里的類有很大的區(qū)別,但提供的特性大致相同,其中很重要的一個(gè)是提供了「零拷貝」的特性。
在處理請求或生成回復(fù)時(shí),往往要使用已有數(shù)據(jù),并對之進(jìn)行截取、拼接等操作。假設(shè)現(xiàn)在要進(jìn)行一個(gè)拼接字符串(bytes1
和bytes2
)的操作,如果使用Java NIO中的java.nio.ByteBuffer
類的話,我們把它假設(shè)成一個(gè)byte數(shù)組(其底層真正的存儲(chǔ)也是如此),往往要生成一個(gè)更大的byte數(shù)組newBytes
,然后將bytes1
和bytes2
分別復(fù)制到newBytes
的地址中去——其實(shí)newBytes
中的數(shù)據(jù)已經(jīng)都存在內(nèi)存中了,只是分屬不同的數(shù)組,存儲(chǔ)在不連續(xù)的內(nèi)存上而已——這么做需要在內(nèi)存中做額外的拷貝。
而使用Netty中的Buffer類(如io.netty.buffer.ByteBuf
)的話,則不會(huì)生成新的newBytes
數(shù)組,而是生成一個(gè)新的對象指向原來的兩個(gè)數(shù)組bytes1
和bytes2
。
并不是說Java NIO中的這種拷貝的策略不好,拋開場景去談性能是沒有意義的。
Netty3中零拷貝的API和Netty4不盡相同,但實(shí)現(xiàn)原理是一樣的,這里拿Netty4來舉例,代碼1中對使用Java NIO的java.nio.ByteBuffer
和Netty4的io.netty.buffer.ByteBuf
拼接數(shù)據(jù)進(jìn)行對比。
//代碼1
public static void main(String[] args) {
byte[] byte1 = "he ".getBytes();
byte[] byte2 = "llo ".getBytes();
ByteBuffer b1 = ByteBuffer.allocate(10);
b1.put(byte1);
ByteBuffer b2 = ByteBuffer.allocate(10);
b2.put(byte2);
ByteBuffer b3 = ByteBuffer.allocate(20);
ByteBuffer[] b4 = {b1, b2}; #1
b3.put(b1.array());
b3.put(b2.array()); #2
//讀取內(nèi)容
System.out.println(new String(b3.array()));
System.out.println("b1 addr:" + b1.array());
System.out.println("b2 addr:" + b2.array());
System.out.println("b3 addr:" + b3.array());
ByteBuf nb1 = Unpooled.buffer(10);
nb1.writeBytes(byte1);
ByteBuf nb2 = Unpooled.buffer(10);
nb2.writeBytes(byte2);
// nb2.array();
ByteBuf nb3 = Unpooled.wrappedBuffer(nb1, nb2);
nb3.array(); #3
//讀取內(nèi)容 #4
byte[] bytes = new byte[20];
for(int i =0; i< nb3.capacity(); i++) {
bytes[i] = nb3.getByte(i);
}
System.out.println(new String(bytes));
}
輸出:
he llo
b1 addr:[B@4aa298b7
b2 addr:[B@7d4991ad
b3 addr:[B@28d93b30
he llo
可以看到,如果使用java.nio.ByteBuffer
進(jìn)行拼接,需要在#2的地方進(jìn)行數(shù)組內(nèi)存的拷貝,為了進(jìn)一步提高這種場景下的系統(tǒng)性能,在使用Unpooled.wrappedBuffer(ByteBuf... buffers)
進(jìn)行拼接時(shí)并沒有進(jìn)行內(nèi)存的拷貝,所以會(huì)在#3的地方拋出UnsupportedOperationException
異常,因?yàn)榇藭r(shí)b3
中已經(jīng)沒有一個(gè)數(shù)組是存儲(chǔ)自身實(shí)際內(nèi)容了,Unpooled.wrappedBuffer
返回的對象是io.netty.buffer.CompositeByteBuf.CompositeByteBuf
的實(shí)例(具體邏輯見代碼2)。
//代碼2
public static ByteBuf wrappedBuffer(int maxNumComponents, ByteBuf... buffers) {
switch (buffers.length) {
case 0:
break;
case 1:
ByteBuf buffer = buffers[0];
if (buffer.isReadable()) {
return wrappedBuffer(buffer.order(BIG_ENDIAN));
} else {
buffer.release();
}
break;
default:
for (int i = 0; i < buffers.length; i++) {
ByteBuf buf = buffers[i];
if (buf.isReadable()) {
return new CompositeByteBuf(ALLOC, false, maxNumComponents, buffers, i, buffers.length);
}
buf.release();
}
break;
}
return EMPTY_BUFFER;
}
當(dāng)然你也可以使用代碼1
中#1行的方法,創(chuàng)建一個(gè)ByteBuffer的數(shù)組,由于Java的數(shù)組中使用「引用」來指向其成員對象,這樣就防止了內(nèi)存拷貝,但這會(huì)帶來另一個(gè)問題,在進(jìn)行拼接之后,其結(jié)果是一個(gè)「ByteBuffer數(shù)組」而非「ByteBuffer對象」,這樣會(huì)給實(shí)際編程帶來很多不便。而使用ByteBuf的拼接則能在返回一個(gè)ByteBuf對象的同時(shí)又防止了內(nèi)存拷貝,這就是Netty中所謂的零拷貝。
同時(shí),更多的Netty中自定義的這些Buffer類可以帶來的好處如下:
- 根據(jù)這些類你可以自定義自己的Buffer類。
- 透明的零拷貝實(shí)現(xiàn)。
-
ByteBuf
提供了很多開箱即用的「訪問特定類型字節(jié)數(shù)組(如getChar(int index)
)」特性。 - 不需要每次都調(diào)用
flip()
來轉(zhuǎn)換讀與寫。 - 比
ByteBuffer
的性能要更好(初始化時(shí)不寫0,不用GC)。
二、垃圾回收(GC)
你可能已經(jīng)發(fā)現(xiàn)了,上一節(jié)中舉得例子(要在拼接字符串的時(shí)候,得到一個(gè)同一類型——此處假設(shè)為Aclass
——的對象,且實(shí)現(xiàn)零內(nèi)存拷貝)中,完全可以在類Aclass
中定義一個(gè)ByteBuffer數(shù)組,然后再增加對于Aclass
中數(shù)組索引到這個(gè)ByteBuffer數(shù)組中的索引映射就好了。實(shí)際上,Netty中也是這么實(shí)現(xiàn)的。
//代碼3
//org.jboss.netty.buffer.CompositeChannelBuffer.getByte(int)的實(shí)現(xiàn)
public byte getByte(int index) {
//找到相應(yīng)的子對象
int componentId = componentId(index);
//返回子對象中的字節(jié)數(shù)組中的相應(yīng)字節(jié)
return components[componentId].getByte(index - indices[componentId]);
}
而Netty中關(guān)于ByteBuf更有爭議的部分在于,在ByteBuf的內(nèi)存的管理上,它實(shí)現(xiàn)了自己的對象池。
自定義對象池,To be or not be
Netty的對象池是基于ThreadLocal的,所以線程與線程之間的池是不相關(guān)的。
Netty做了這么復(fù)雜的事情想優(yōu)化內(nèi)存的使用,以至于在Netty4中又進(jìn)一步引入了自定義的對象池。在這個(gè)池中,Netty實(shí)現(xiàn)了自己的內(nèi)存管理(分配和釋放)。按照Netty文檔上的說法,在處理網(wǎng)絡(luò)事件時(shí),往往需要在短時(shí)間內(nèi)分配大量的、生命周期很短的對象,而如果要等待JVM的GC來回收這些對象,速度會(huì)很慢,同時(shí)GC本身也是要消耗資源的。
熟悉垃圾回收算法的朋友對于「引用計(jì)數(shù)」一定不陌生,iOS的Runtime中垃圾回收使用的就是引用計(jì)數(shù),它是一種更高效、更原始的垃圾回收方法。高效體現(xiàn)在它的內(nèi)存回收更「實(shí)時(shí)而直接」,原始體現(xiàn)在你需要在程序中顯式地對引用計(jì)數(shù)進(jìn)行增加和減少。當(dāng)一個(gè)對象的引用計(jì)數(shù)降為0
,則其內(nèi)存會(huì)被回收。這種方式在手機(jī)這種「資源相而言更緊張」的設(shè)備上會(huì)帶來很好的性能表現(xiàn)。
而JVM中采用的是「基于分代垃圾回收的構(gòu)建引用樹」的方法,所有不在這棵樹上的對象則可回收,可想而知,構(gòu)建這棵樹本身就會(huì)消耗一定的資源,另外「分代垃圾回收」較「引用計(jì)數(shù)」也更復(fù)雜。
對于某些有這種需求(短時(shí)間內(nèi)分配大量的、生命周期很短)的對象,Netty中使用引用計(jì)數(shù)來管理這些對象的分配和釋放。具體的方法大致如下:
- 首先Netty在JVM堆上申請一塊較大的內(nèi)存。
- Netty的一直存儲(chǔ)著指向這塊內(nèi)存中對象的引用,使得JVM的GC不去回收這塊內(nèi)存。
- 當(dāng)在Netty中需要申請一塊生命周期較短的對象時(shí)(如ByteBuf),其真實(shí)內(nèi)存就放入這塊內(nèi)存,同時(shí)維護(hù)一個(gè)這個(gè)對象的引用計(jì)數(shù),在Netty中其初始值為1。
- 當(dāng)某個(gè)對象的引用計(jì)數(shù)降為0時(shí),將這塊內(nèi)存標(biāo)識為可用。
應(yīng)用程序構(gòu)建自己的內(nèi)存池的做法是有爭議的,往往會(huì)帶來內(nèi)存泄漏的結(jié)果,也不能獲得JVM的GC算法帶來的好處。但Netty的內(nèi)存池已經(jīng)證明,合理的使用內(nèi)存池能夠帶來更好的性能。
直接I/O(Direct I/O),To be or not to be
在介紹I/O模型「從I/O模型到Netty(一)」時(shí),就提到過Direct I/O,它帶來的好處是在做I/O操作時(shí),不需要把內(nèi)存從用戶空間拷貝到內(nèi)核空間,節(jié)省了一部分資源,但在JVM的環(huán)境中申請Direct I/O要比在堆上分配內(nèi)存消耗更多的性能。而利用Netty的對象池,剛好可以抵消這部分消耗,由池管理的Direct I/O的內(nèi)存分配節(jié)省了GC的消耗。
有些地方會(huì)使用「零拷貝」來指代Direct I/O相對于Buffered I/O省去的那次拷貝(在用戶空間和內(nèi)核空間之間進(jìn)行拷貝)
三、事件模型
假設(shè)在某種場景下,整個(gè)程序的目的都是處理單一的事情(比如一個(gè)web服務(wù)器的目的只是處理請求),我們可以將「與處理請求無關(guān)」的邏輯封裝到一個(gè)框架內(nèi),在每次請求處理完后,都執(zhí)行一次事件的分發(fā)和處理,這就是event loop了。很多語言中都有這種概念,如nodejs中的event loop,iOS中的run loop。
這是在「從I/O模型到Netty(一)」中提到過的EventLoop的概念,在Netty4中,則真正實(shí)現(xiàn)了這樣一個(gè)概念。在Netty4的類里赫然能看到EventLoop的接口,但Netty4里的EventLoop和其他語言中Runtime級別的EventLoop還是有很大的區(qū)別的,其更像是一個(gè)執(zhí)行預(yù)定義隊(duì)列中任務(wù)的線程(繼承自java.util.concurrent.ScheduledExecutorService
,看下圖中EventLoop的繼承結(jié)構(gòu)。

其中,io.netty.channel.SingleThreadEventLoop
比較重要,從它的名字就能看出來,它指的是單個(gè)線程的EventLoop
,在Netty4的事件模型中,每一個(gè)EventLoop都有一個(gè)分配的線程,所有的I/O操作(也會(huì)使用事件進(jìn)行傳遞)和事件的處理都是在這個(gè)線程中完成的。其執(zhí)行邏輯如下圖所示:
在本文之后的內(nèi)容中有時(shí)候會(huì)不區(qū)分「EventLoop、EventExecutor和線程」,「EventExecutorGroup和線程池」的概念

代碼4中是上圖中邏輯的實(shí)現(xiàn)代碼。
//代碼4
@Override
public void execute(Runnable task) {
if (task == null) {
throw new NullPointerException("task");
}
boolean inEventLoop = inEventLoop();
if (inEventLoop) {
addTask(task);
} else {
startThread();
addTask(task);
if (isShutdown() && removeTask(task)) {
reject();
}
}
if (!addTaskWakesUp && wakesUpForTask(task)) {
wakeup(inEventLoop);
}
}
四、線程模型
線程模型直接反應(yīng)了一個(gè)程序在運(yùn)行時(shí)是如何去「分配和執(zhí)行任務(wù)」的。對于Netty而言,其基本的線程模型可以理解為之前介紹到的Reactor模型,只是在其之上做了一些擴(kuò)展。比如說在服務(wù)端,最重要的I/O事件應(yīng)該算是連接請求(CONNECT)事件了,它直接關(guān)系到了服務(wù)端程序的吞吐量,所以在Netty的線程模型中就設(shè)計(jì)了一個(gè)單獨(dú)的線程去處理這個(gè)請求,其基本模型如下圖所示:
由于篇幅所限,本文討論的線程模型將只關(guān)注服務(wù)端,客戶端當(dāng)然也同樣重要。
Netty中的事件流
簡單來說,Netty中的管道(ChannelPipe)可以認(rèn)為就是一個(gè)Handler的容器,里邊存放了兩種EventHandler(io.netty.channel.ChannelInboundHandler
和io.netty.channel.ChannelOutboundHandler
)。一個(gè)網(wǎng)絡(luò)請求從建立連接開始到得到回復(fù)的過程,就是在這個(gè)管道中流入然后流出的過程,Netty的文檔中是這么描述管道的:
從Channel或者
ChannelHandlerContext
而來的I/O請求
|
+---------------------------------------------------+---------------+
| ChannelPipeline | |
| \|/ |
| +---------------------+ +-----------+----------+ |
| | Inbound Handler N | | Outbound Handler 1 | |
| +----------+----------+ +-----------+----------+ |
| /|\ | |
| | \|/ |
| +----------+----------+ +-----------+----------+ |
| | Inbound Handler N-1 | | Outbound Handler 2 | |
| +----------+----------+ +-----------+----------+ |
| /|\ . |
| . . |
| ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()|
| [ method call] [method call] |
| . . |
| . \|/ |
| +----------+----------+ +-----------+----------+ |
| | Inbound Handler 2 | | Outbound Handler M-1 | |
| +----------+----------+ +-----------+----------+ |
| /|\ | |
| | \|/ |
| +----------+----------+ +-----------+----------+ |
| | Inbound Handler 1 | | Outbound Handler M | |
| +----------+----------+ +-----------+----------+ |
| /|\ | |
+---------------+-----------------------------------+---------------+
| \|/
+---------------+-----------------------------------+---------------+
| [ Socket.read() ] [ Socket.write() ] |
+-------------------------------------------------------------------+
簡單說,就是一個(gè)事件在管道里的順序是從第一個(gè)Inbound的Handler開始,執(zhí)行到最后一個(gè),當(dāng)一個(gè)Outbound事件發(fā)生時(shí),它是從相反的方向執(zhí)行到第一個(gè)。
實(shí)際的實(shí)現(xiàn)是這樣的,管道可以被認(rèn)為是一個(gè)有序的Handler的序列(鏈表,見代碼5),當(dāng)一個(gè)Inbound事件發(fā)生時(shí),它會(huì)從序列的最頭部依次通過每一個(gè)Handler,如果這個(gè)Handler是Inbound類型,那么就被執(zhí)行,否則依次往后。當(dāng)一個(gè)Outbound事件發(fā)生時(shí),它會(huì)從這個(gè)序列的當(dāng)前位置開始執(zhí)行,判斷是否是Outbound類型,直至最頭部。
//代碼5
@Override
public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
final AbstractChannelHandlerContext newCtx;
synchronized (this) {
checkMultiplicity(handler);
newCtx = newContext(group, filterName(name, handler), handler);
addLast0(newCtx);
。。。省略一些別的代碼
}
}
。。。省略一些別的代碼
private void addLast0(AbstractChannelHandlerContext newCtx) {
AbstractChannelHandlerContext prev = tail.prev;
newCtx.prev = prev;
newCtx.next = tail;
prev.next = newCtx;
tail.prev = newCtx;
}
每一個(gè)事件往管道的更深處發(fā)送需要Handler自身顯式觸發(fā)。
Netty3中的管道
在Netty3里,Inbound和Outbound的概念分別叫Upstream和Downstream,從上一節(jié)的圖中能很清晰的看出來,一個(gè)往上,一個(gè)往下。
如果在上邊所說的某一個(gè)Handler中包含一個(gè)很耗時(shí)的操作,那么處理I/O的線程就會(huì)造成阻塞,導(dǎo)致這個(gè)線程遲遲不能被回收并用以處理新的請求,所以在Netty3中引入了除I/O線程池之外的另一個(gè)線程池來處理業(yè)務(wù)邏輯。用戶可以通過org.jboss.netty.handler.execution.ExecutionHandler
來實(shí)現(xiàn)自己的業(yè)務(wù)線程池,它同時(shí)實(shí)現(xiàn)了UpstreamHandler和DownstreamHandler。
ExecutionHandler的引入給Netty帶來很多問題,本來Netty一直秉持著I/O處理串行化(一個(gè)事件只被一個(gè)線程處理)的理念,但是在ExecutionHandler的場景下則會(huì)有多個(gè)線程參與到這個(gè)事件的處理中來,同時(shí)也增加了開發(fā)的復(fù)雜度,用戶需要關(guān)心額外的多線程編程的東西。
Netty3中還有一個(gè)接口org.jboss.netty.channel.ChannelSink
用來提供統(tǒng)一的「把Downstream寫入底層」的API,在Netty4中已經(jīng)看不到了。
Netty4的線程模型
我在自己的電腦上運(yùn)行Netty4中自帶的Echo的例子,使用VisualVM查看其線程列表,截圖如下:
然后依次啟動(dòng)10個(gè)client對這個(gè)server進(jìn)行連接,截圖如下:
本文中所有的講述都是基于NIO的,所以這里看到啟動(dòng)的線程池是io.netty.channel.nio.NioEventLoopGroup
的實(shí)例,其繼承了類io.netty.util.concurrent.DefaultThreadFactory
,Netty4中不再需要使用Java的線程池,所以線程的名稱和Netty3中有些不同,大致的規(guī)則是線程的名稱為<線程池的類名(第一個(gè)字母小寫)>-<線程池啟動(dòng)的順序>-<線程啟動(dòng)的順序>
。
可以看到Netty運(yùn)行起來之后有這樣幾個(gè)線程:
- 一些無關(guān)的線程:JDK的線程,網(wǎng)絡(luò)連接的線程,JMX的線程
- 1個(gè)Boss線程(nioEventLoopGroup-2-1)
- 8個(gè)Worker線程(nioEventLoopGroup-3-*)
一個(gè)小的細(xì)節(jié),從上圖可以看到Boss線程池是第二個(gè)被實(shí)例化的,其實(shí)還有一個(gè)線程池
GlobalEventExecutor
會(huì)被第一個(gè)實(shí)例化,它在Netty的整個(gè)生命周期都會(huì)存在。
在Netty4中線程是按照如下方式工作的:
- 對于每一個(gè)端口的監(jiān)聽,會(huì)有一個(gè)單獨(dú)的線程(Boss)去監(jiān)聽并處理其I/O事件。
- Boss線程為這個(gè)事件生成對應(yīng)的Channel,并綁定其對應(yīng)的Pipeline,然后交給Worker(childGroup)。
- Worker會(huì)將此Channel綁定到某個(gè)EventLoop(I/O線程)上,之后所有這個(gè)Channel上的事件默認(rèn)都要在此EventLoop上執(zhí)行。
- 當(dāng)事件需要執(zhí)行耗時(shí)的工作時(shí),為了不阻塞I/O線程,往往會(huì)自定義一個(gè)EventExecutorGroup(Netty4提供了
io.netty.util.concurrent.DefaultEventExecutorGroup
),將耗時(shí)的Handler放入其中執(zhí)行。 - 對于沒有指定EventExecutorGroup的Handler,將默認(rèn)指定為Channel上綁定的EventLoop。
其流程如下圖所示:
Netty3與Netty4的不同
Netty3與4相比,大致的思想都是一樣的,但是實(shí)現(xiàn)上有一些略微的不同,在Netty3.7源碼中的EchoServer執(zhí)行后其線程列表如下:
Netty3與Netty4不同的地方有:
- Netty3中ServerBootstrap的創(chuàng)建需要使用JDK的線程池,而Netty4封裝了線程池,增加了很多如EventLoop的概念,這點(diǎn)可以從線程的命名上看得出來。
- 由于#1的原因,導(dǎo)致了Pipeline中的Handler沒有被約束在某個(gè)線程內(nèi)執(zhí)行,會(huì)出現(xiàn)多線程同步的問題。
- 由于#1的原因,在Netty3中可以生成大量的業(yè)務(wù)線程來做Handler的處理,有時(shí)候看這樣做可以提升系統(tǒng)的性能,但是其實(shí)這樣做破壞了Netty只處理網(wǎng)絡(luò)I/O事件的設(shè)計(jì),整個(gè)Handler的執(zhí)行過程變得很復(fù)雜,增加了系統(tǒng)開發(fā)和維護(hù)的復(fù)雜度。
- Netty3中在Pipeline中切換線程可以使用
org.jboss.netty.handler.execution.ExecutionHandler
,而在新的線程模型中,Netty提供了io.netty.util.concurrent.DefaultEventExecutorGroup
來實(shí)現(xiàn)這種切換。
五、一些關(guān)于Netty的周邊
第一次看到Netty時(shí)想,WTH,它跟Jetty有什么關(guān)系,怎么長得這么像。
后來去逛了Netty的網(wǎng)站,看(xiang)了(xi)一(yue)看(du)最新的User Guide,看到了一段話讓我一下子樂了。
Some users might already have found other network application framework that claims to have the same advantage, and you might want to ask what makes Netty so different from them. The answer is the philosophy it is built on. Netty is designed to give you the most comfortable experience both in terms of the API and the implementation from the day one. It is not something tangible but you will realize that this philosophy will make your life much easier as you read this guide and play with Netty.
粗體的文字大意是說,「Netty的好,不能用言語來表達(dá),但是只要你去使用它,你能體會(huì)的到蘊(yùn)藏在其中的哲學(xué),它會(huì)讓你的生活更加容易。」這個(gè)X裝的我給。
- 因?yàn)橐獪?zhǔn)備這篇內(nèi)容,去搜了一下Netty的歷史,原來它最早是一個(gè)叫
Trustin Lee
的人寫的。然后用了十多年的積累才造就了今天Netty這樣一個(gè)開箱即用的基于事件驅(qū)動(dòng)的NIO網(wǎng)絡(luò)框架。