Netty 是一個高性能的網絡框架,應用很是廣泛,目前在Java 領域,Netty 基本上成爲網絡程序的標配了,Netty 框架功能豐富,也很是複雜。今天主要分析Netty 框架中的線程模型,而線程模型直接影響着網絡程序的性能。web
在介紹Netty 的線程模型以前,咱們首先搞清楚網絡編程性能的瓶頸在哪裏,而後再看Netty 的線程模型是如何解決這個問題的。算法
傳統的BIO 編程模型裏, 全部的read() 操做和 write() 操做都會阻塞當前線程的, 若是客戶端和服務端已經創建了一個鏈接,而遲遲不發送數據,那麼服務端的 read() 操做會一直阻塞, 因此使用BIO 模型, 通常都會爲每一個socket 分配一個獨立的線程,這樣就不會由於線程阻塞在一個socket 上而影響對其餘socket 的讀寫。編程
BIO 的線程模型以下圖所示:每一個socket 對應一個獨立的線程。爲了不頻繁建立消耗線程,能夠採用線程池,可是socket 和線程之間的對應關係不會變化。bash
BIO 這種線程模型,適用於socket 鏈接不是不少的場景。可是如今的互聯網場景,每每須要服務器可以支撐十萬甚至百萬鏈接,而建立十萬甚至百萬鏈接顯然不現實,因此BIO 線程模型沒法解決百萬鏈接的問題。若是仔細觀察,你會發現互聯網場景中,雖然鏈接不少,可是每一個鏈接的請求並不頻繁,因此線程大部分時間都在等待I/O 就緒,也就是說線程大部分時間都阻塞在那裏,這徹底是浪費,若是咱們可以解決這個問題,那就不須要這麼多線程了。服務器
順着這個思路,咱們能夠將線程的模型優化爲下圖這個樣子,用一個線程來處理多個鏈接,這樣利用率就上來了,同時所須要的線程數量也降下來了。但是使用 BIO 相關的 API 是沒法實現的, 爲何呢?由於 BIO 相關的 socket 讀寫操做都是阻塞式的,而一旦調用了阻塞式 API,在 I/O 就緒前,調用線程會一直阻塞,也就沒法處理其餘的 socket 鏈接了。網絡
好在 Java 裏還提供了非阻塞式(NIO)API, 利用非阻塞API 就可以實現一個線程處理多個鏈接了。 那具體如何實現呢?如今廣泛採用的都是Reactor 模式, 包括Netty 的實現,因此先讓咱們瞭解如下 Reactor 模式。多線程
下面是 Reactor 模式的類結構圖,其中 Handle 指的是 I/O 句柄,在 Java 網絡編程裏,它本質上就是一個網絡鏈接。Event Handler 很容易理解,就是一個事件處理器,其中 handle_event() 方法處理 I/O 事件,也就是每一個 Event Handler 處理一個 I/O Handle;get_handle() 方法能夠返回這個 I/O 的 Handle。Synchronous Event Demultiplexer 能夠理解爲操做系統提供的 I/O 多路複用 API,例如 POSIX 標準裏的 select() 以及 Linux 裏面的 epoll()。併發
Reactor 模式的核心天然是 Reactor 這個類,其中 register_handler() 和 remove_handler() 這兩個方法能夠註冊和刪除一個事件處理器;handle_events() 方式是核心,也是 Reactor 模式的發動機,這個方法的核心邏輯以下:首先經過同步事件多路選擇器提供的 select() 方法監聽網絡事件,當有網絡事件就緒後,就遍歷事件處理器來處理該網絡事件。因爲網絡事件是源源不斷的,因此在主程序中啓動 Reactor 模式,須要以 while(true){} 的方式調用 handle_events() 方法。負載均衡
void Reactor::handle_events(){
//經過同步事件多路選擇器提供的
//select()方法監聽網絡事件
select(handlers);
//處理網絡事件
for(h in handlers){
h.handle_event();
}
}
// 在主程序中啓動事件循環
while (true) {
handle_events();
複製代碼
Netty 的實現雖然參考了 Reactor 模式,可是並無徹底照搬,Netty 中最核心的概念是事件循環(EventLoop),其實也就是 Reactor 模式中的 Reactor,負責監聽網絡事件並調用事件處理器進行處理。在 4.x 版本的 Netty 中,網絡鏈接和 EventLoop 是穩定的多對 1 關係,而 EventLoop 和 Java 線程是 1 對 1 關係,這裏的穩定指的是關係一旦肯定就再也不發生變化。也就是說一個網絡鏈接只會對應惟一的一個 EventLoop,而一個 EventLoop 也只會對應到一個 Java 線程,因此一個網絡鏈接只會對應到一個 Java 線程。框架
一個網絡鏈接對應到一個 Java 線程上,有什麼好處呢?最大的好處就是對於一個網絡鏈接的事件處理是單線程的,這樣就避免了各類併發問題。
Netty 中的線程模型能夠參考下圖,這個圖和前面咱們提到的理想的線程模型圖很是類似,核心目標都是用一個線程處理多個網絡鏈接。
Netty 中還有一個核心概念是 EventLoopGroup,顧名思義,一個 EventLoopGroup 由一組 EventLoop 組成。實際使用中,通常都會建立兩個 EventLoopGroup,一個稱爲 bossGroup,一個稱爲 workerGroup。爲何會有兩個 EventLoopGroup 呢?
這個和 socket 處理網絡請求的機制有關,socket 處理 TCP 網絡鏈接請求,是在一個獨立的 socket 中,每當有一個 TCP 鏈接成功創建,都會建立一個新的 socket,以後對 TCP 鏈接的讀寫都是由新建立處理的 socket 完成的。也就是說處理 TCP 鏈接請求和讀寫請求是經過兩個不一樣的 socket 完成的。上面咱們在討論網絡請求的時候,爲了簡化模型,只是討論了讀寫請求,而沒有討論鏈接請求。
在 Netty 中,bossGroup 就用來處理鏈接請求的,而 workerGroup 是用來處理讀寫請求的。bossGroup 處理完鏈接請求後,會將這個鏈接提交給 workerGroup 來處理, workerGroup 裏面有多個 EventLoop,那新的鏈接會交給哪一個 EventLoop 來處理呢?這就須要一個負載均衡算法,Netty 中目前使用的是輪詢算法。
下面的示例代碼基於 Netty 實現了 echo 程序服務端:首先建立了一個事件處理器(等同於 Reactor 模式中的事件處理器),而後建立了 bossGroup 和 workerGroup,再以後建立並初始化了 ServerBootstrap,代碼仍是很簡單的,不過有兩個地方須要注意一下。
第一個,若是 NettybossGroup 只監聽一個端口,那 bossGroup 只須要 1 個 EventLoop 就能夠了,多了純屬浪費。
第二個,默認狀況下,Netty 會建立「2*CPU 核數」個 EventLoop,因爲網絡鏈接與 EventLoop 有穩定的關係,因此事件處理器在處理網絡事件的時候是不能有阻塞操做的,不然很容易致使請求大面積超時。若是實在沒法避免使用阻塞操做,那能夠經過線程池來異步處理。
//事件處理器
final EchoServerHandler serverHandler
= new EchoServerHandler();
//boss線程組
EventLoopGroup bossGroup
= new NioEventLoopGroup(1);
//worker線程組
EventLoopGroup workerGroup
= new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch){
ch.pipeline().addLast(serverHandler);
}
});
//bind服務端端口
ChannelFuture f = b.bind(9090).sync();
f.channel().closeFuture().sync();
} finally {
//終止工做線程組
workerGroup.shutdownGracefully();
//終止boss線程組
bossGroup.shutdownGracefully();
}
//socket鏈接處理器
class EchoServerHandler extends
ChannelInboundHandlerAdapter {
//處理讀事件
@Override
public void channelRead(
ChannelHandlerContext ctx, Object msg){
ctx.write(msg);
}
//處理讀完成事件
@Override
public void channelReadComplete(
ChannelHandlerContext ctx){
ctx.flush();
}
//處理異常事件
@Override
public void exceptionCaught(
ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
複製代碼
Netty 是一個款優秀的網絡編程框架,性能很是好,爲了實現高性能的目標,Netty 作了不少優化,例如優化了 ByteBuffer、支持零拷貝等等,和併發編程相關的就是它的線程模型了。Netty 的線程模型設計得很精巧,每一個網絡鏈接都關聯到了一個線程上,這樣作的好處是:對於一個網絡鏈接,讀寫操做都是單線程執行的,從而避免了併發程序的各類問題。