溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Netty中的線程模型和實(shí)現(xiàn)Echo程序服務(wù)端

發(fā)布時(shí)間:2020-06-03 09:14:25 來(lái)源:億速云 閱讀:219 作者:Leah 欄目:編程語(yǔ)言

本文以Netty網(wǎng)絡(luò)編程框架為例,為大家分析網(wǎng)絡(luò)編程性能的瓶頸、Reactor 模式、Netty中的線程模型以及實(shí)現(xiàn)Echo程序服務(wù)端。閱讀完整文相信大家對(duì)Netty網(wǎng)絡(luò)編程框架有了一定的認(rèn)識(shí)。

Netty 是一個(gè)高性能的網(wǎng)絡(luò)框架,應(yīng)用非常普遍,目前在Java 領(lǐng)域,Netty 基本上成為網(wǎng)絡(luò)程序的標(biāo)配了,Netty 框架功能豐富,也非常復(fù)雜。今天主要分析Netty 框架中的線程模型,而線程模型直接影響著網(wǎng)絡(luò)程序的性能。

在介紹Netty 的線程模型之前,我們首先搞清楚網(wǎng)絡(luò)編程性能的瓶頸在哪里,然后再看Netty 的線程模型是如何解決這個(gè)問(wèn)題的。

網(wǎng)絡(luò)編程性能的瓶頸

傳統(tǒng)的BIO 編程模型里, 所有的read() 操作和 write() 操作都會(huì)阻塞當(dāng)前線程的, 如果客戶端和服務(wù)端已經(jīng)建立了一個(gè)連接,而遲遲不發(fā)送數(shù)據(jù),那么服務(wù)端的 read() 操作會(huì)一直阻塞, 所以使用BIO 模型, 一般都會(huì)為每個(gè)socket 分配一個(gè)獨(dú)立的線程,這樣就不會(huì)因?yàn)榫€程阻塞在一個(gè)socket 上而影響對(duì)其他socket 的讀寫(xiě)。

BIO 的線程模型如下圖所示:每個(gè)socket 對(duì)應(yīng)一個(gè)獨(dú)立的線程。為了避免頻繁創(chuàng)建消耗線程,可以采用線程池,但是socket 和線程之間的對(duì)應(yīng)關(guān)系不會(huì)變化。

BIO 這種線程模型,適用于socket 連接不是很多的場(chǎng)景。但是現(xiàn)在的互聯(lián)網(wǎng)場(chǎng)景,往往需要服務(wù)器能夠支撐十萬(wàn)甚至百萬(wàn)連接,而創(chuàng)建十萬(wàn)甚至百萬(wàn)連接顯然不現(xiàn)實(shí),所以BIO 線程模型無(wú)法解決百萬(wàn)連接的問(wèn)題。如果仔細(xì)觀察,你會(huì)發(fā)現(xiàn)互聯(lián)網(wǎng)場(chǎng)景中,雖然連接很多,但是每個(gè)連接的請(qǐng)求并不頻繁,所以線程大部分時(shí)間都在等待I/O 就緒,也就是說(shuō)線程大部分時(shí)間都阻塞在那里,這完全是浪費(fèi),如果我們能夠解決這個(gè)問(wèn)題,那就不需要這么多線程了。

順著這個(gè)思路,我們可以將線程的模型優(yōu)化為下圖這個(gè)樣子,用一個(gè)線程來(lái)處理多個(gè)連接,這樣利用率就上來(lái)了,同時(shí)所需要的線程數(shù)量也降下來(lái)了。可是使用 BIO 相關(guān)的 API 是無(wú)法實(shí)現(xiàn)的, 為什么呢?因?yàn)?BIO 相關(guān)的 socket 讀寫(xiě)操作都是阻塞式的,而一旦調(diào)用了阻塞式 API,在 I/O 就緒前,調(diào)用線程會(huì)一直阻塞,也就無(wú)法處理其他的 socket 連接了。

好在 Java 里還提供了非阻塞式(NIO)API, 利用非阻塞API 就能夠?qū)崿F(xiàn)一個(gè)線程處理多個(gè)連接了。 那具體如何實(shí)現(xiàn)呢?現(xiàn)在普遍采用的都是Reactor 模式, 包括Netty 的實(shí)現(xiàn),所以先讓我們了解以下 Reactor 模式。

Reactor 模式

下面是 Reactor 模式的類結(jié)構(gòu)圖,其中 Handle 指的是 I/O 句柄,在 Java 網(wǎng)絡(luò)編程里,它本質(zhì)上就是一個(gè)網(wǎng)絡(luò)連接。Event Handler 很容易理解,就是一個(gè)事件處理器,其中 handle_event() 方法處理 I/O 事件,也就是每個(gè) Event Handler 處理一個(gè) I/O Handle;get_handle() 方法可以返回這個(gè) I/O 的 Handle。Synchronous Event Demultiplexer 可以理解為操作系統(tǒng)提供的 I/O 多路復(fù)用 API,例如 POSIX 標(biāo)準(zhǔn)里的 select() 以及 Linux 里面的 epoll()。

Reactor 模式的核心自然是 Reactor 這個(gè)類,其中 register_handler() 和 remove_handler() 這兩個(gè)方法可以注冊(cè)和刪除一個(gè)事件處理器;handle_events() 方式是核心,也是 Reactor 模式的發(fā)動(dòng)機(jī),這個(gè)方法的核心邏輯如下:首先通過(guò)同步事件多路選擇器提供的 select() 方法監(jiān)聽(tīng)網(wǎng)絡(luò)事件,當(dāng)有網(wǎng)絡(luò)事件就緒后,就遍歷事件處理器來(lái)處理該網(wǎng)絡(luò)事件。由于網(wǎng)絡(luò)事件是源源不斷的,所以在主程序中啟動(dòng) Reactor 模式,需要以 while(true){} 的方式調(diào)用 handle_events() 方法。

void Reactor::handle_events(){
  //通過(guò)同步事件多路選擇器提供的
  //select()方法監(jiān)聽(tīng)網(wǎng)絡(luò)事件
  select(handlers);
  //處理網(wǎng)絡(luò)事件
  for(h in handlers){
    h.handle_event();
  }
}
// 在主程序中啟動(dòng)事件循環(huán)
while (true) {
  handle_events();  

Netty 中的線程模型

Netty 的實(shí)現(xiàn)雖然參考了 Reactor 模式,但是并沒(méi)有完全照搬,Netty 中最核心的概念是事件循環(huán)(EventLoop),其實(shí)也就是 Reactor 模式中的 Reactor,負(fù)責(zé)監(jiān)聽(tīng)網(wǎng)絡(luò)事件并調(diào)用事件處理器進(jìn)行處理。在 4.x 版本的 Netty 中,網(wǎng)絡(luò)連接和 EventLoop 是穩(wěn)定的多對(duì) 1 關(guān)系,而 EventLoop 和 Java 線程是 1 對(duì) 1 關(guān)系,這里的穩(wěn)定指的是關(guān)系一旦確定就不再發(fā)生變化。也就是說(shuō)一個(gè)網(wǎng)絡(luò)連接只會(huì)對(duì)應(yīng)唯一的一個(gè) EventLoop,而一個(gè) EventLoop 也只會(huì)對(duì)應(yīng)到一個(gè) Java 線程,所以一個(gè)網(wǎng)絡(luò)連接只會(huì)對(duì)應(yīng)到一個(gè) Java 線程。

一個(gè)網(wǎng)絡(luò)連接對(duì)應(yīng)到一個(gè) Java 線程上,有什么好處呢?最大的好處就是對(duì)于一個(gè)網(wǎng)絡(luò)連接的事件處理是單線程的,這樣就避免了各種并發(fā)問(wèn)題。

Netty 中的線程模型可以參考下圖,這個(gè)圖和前面我們提到的理想的線程模型圖非常相似,核心目標(biāo)都是用一個(gè)線程處理多個(gè)網(wǎng)絡(luò)連接。

Netty 中還有一個(gè)核心概念是 EventLoopGroup,顧名思義,一個(gè) EventLoopGroup 由一組 EventLoop 組成。實(shí)際使用中,一般都會(huì)創(chuàng)建兩個(gè) EventLoopGroup,一個(gè)稱為 bossGroup,一個(gè)稱為 workerGroup。為什么會(huì)有兩個(gè) EventLoopGroup 呢?

這個(gè)和 socket 處理網(wǎng)絡(luò)請(qǐng)求的機(jī)制有關(guān),socket 處理 TCP 網(wǎng)絡(luò)連接請(qǐng)求,是在一個(gè)獨(dú)立的 socket 中,每當(dāng)有一個(gè) TCP 連接成功建立,都會(huì)創(chuàng)建一個(gè)新的 socket,之后對(duì) TCP 連接的讀寫(xiě)都是由新創(chuàng)建處理的 socket 完成的。也就是說(shuō)處理 TCP 連接請(qǐng)求和讀寫(xiě)請(qǐng)求是通過(guò)兩個(gè)不同的 socket 完成的。上面我們?cè)谟懻摼W(wǎng)絡(luò)請(qǐng)求的時(shí)候,為了簡(jiǎn)化模型,只是討論了讀寫(xiě)請(qǐng)求,而沒(méi)有討論連接請(qǐng)求。

在 Netty 中,bossGroup 就用來(lái)處理連接請(qǐng)求的,而 workerGroup 是用來(lái)處理讀寫(xiě)請(qǐng)求的。bossGroup 處理完連接請(qǐng)求后,會(huì)將這個(gè)連接提交給 workerGroup 來(lái)處理, workerGroup 里面有多個(gè) EventLoop,那新的連接會(huì)交給哪個(gè) EventLoop 來(lái)處理呢?這就需要一個(gè)負(fù)載均衡算法,Netty 中目前使用的是輪詢算法。

用 Netty 實(shí)現(xiàn) Echo 程序服務(wù)端

下面的示例代碼基于 Netty 實(shí)現(xiàn)了 echo 程序服務(wù)端:首先創(chuàng)建了一個(gè)事件處理器(等同于 Reactor 模式中的事件處理器),然后創(chuàng)建了 bossGroup 和 workerGroup,再之后創(chuàng)建并初始化了 ServerBootstrap,代碼還是很簡(jiǎn)單的,不過(guò)有兩個(gè)地方需要注意一下。

第一個(gè),如果 NettybossGroup 只監(jiān)聽(tīng)一個(gè)端口,那 bossGroup 只需要 1 個(gè) EventLoop 就可以了,多了純屬浪費(fèi)。

第二個(gè),默認(rèn)情況下,Netty 會(huì)創(chuàng)建“2*CPU 核數(shù)”個(gè) EventLoop,由于網(wǎng)絡(luò)連接與 EventLoop 有穩(wěn)定的關(guān)系,所以事件處理器在處理網(wǎng)絡(luò)事件的時(shí)候是不能有阻塞操作的,否則很容易導(dǎo)致請(qǐng)求大面積超時(shí)。如果實(shí)在無(wú)法避免使用阻塞操作,那可以通過(guò)線程池來(lái)異步處理。

//事件處理器
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服務(wù)端端口  
  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();
  }
}

看完上述內(nèi)容,你們對(duì)Netty網(wǎng)絡(luò)編程框架有進(jìn)一步的了解嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI