netty初步使用2

Netty提供異步的、事件驅動的網絡應用程序框架和工具,用以快速開發高性能、高可靠性的網絡服務器和客戶端程序[官方定義],整體來看其包含了以下內容:1.提供了豐富的協議編解碼支持,2.實現自有的buffer系統,減少複製所帶來的消耗,3.整套channel的實現,4.基於事件的過程流轉以及完整的網絡事件響應與擴展,5.豐富的example。本文並不對Netty實際使用中可能出現的問題做分析,只是從代碼角度分析它的架構以及實現上的一些關鍵細節。

首先來看下最如何使用Netty(其自帶example很好展示了使用),Netty普通使用一般是通過BootStrap來啓動,BootStrap主要分爲兩類:1.面向連接(TCP)的BootStrap(ClientBootStrap和ServerBootstrap),2.非面向連接(UDP) 的(ConnectionlessBootstrap)。

Netty整體架構很清晰的分成2個部分,ChannelFactory 和ChannelPipelineFactory,前者主要生產網絡通信相關的Channel實例和ChannelSink實例,Netty提供的 ChannelFactory實現基本能夠滿足絕大部分用戶的需求,當然你也可以定製自己的ChannelFactory,後者主要關注於具體傳輸數據的處理,同時也包括其他方面的內容,比如異常處理等等,只要是你希望的,你都可以往裏添加相應的handler,一般 ChannelPipelineFactory由用戶自己實現,因爲傳輸數據的處理及其他操作和業務關聯比較緊密,需要自定義處理的handler。

現在,使用Netty的步驟實際上已經非常明確了,比如面向連接的Netty服務端客戶端使用,第一步:實例化一個BootStrap,並且通過構造方法指定一個ChannelFactory實現,第二步:向bootstrap實例註冊一個自己實現的ChannelPipelineFactory,第三步:如果是服務器端,bootstrap.bind(new InetSocketAddress(port)),然後等待客戶端來連接,如果是客戶端,bootstrap.connect(new InetSocketAddress(host,port))取得一個future,這個時候Netty會去連接遠程主機,在連接完成後,會發起類型爲 CONNECTED的ChannelStateEvent,並且開始在你自定義的Pipeline裏面流轉,如果你註冊的handler有這個事件的響應方法的話那麼就會調用到這個方法。在此之後就是數據的傳輸了。下面是一個簡單客戶端的代碼解讀。

// 實例化一個客戶端Bootstrap實例,其中NioClientSocketChannelFactory實例由Netty提供
Java代碼  收藏代碼
  1. ClientBootstrap bootstrap = new ClientBootstrap(    
  2.   
  3.         new NioClientSocketChannelFactory(    
  4.   
  5.                 Executors.newCachedThreadPool(),    
  6.   
  7.                 Executors.newCachedThreadPool()));    
  8.   
  9.   
  10.   
  11. // 設置PipelineFactory,由客戶端自己實現    
  12.   
  13. bootstrap.setPipelineFactory(new FactorialClientPipelineFactory(count));    
  14.   
  15.   
  16.   
  17. //向目標地址發起一個連接    
  18.   
  19. ChannelFuture connectFuture =    
  20.   
  21.     bootstrap.connect(new InetSocketAddress(host, port));    
  22.   
  23.   
  24.   
  25. // 等待鏈接成功,成功後發起的connected事件將會使handler開始發送信息並且等待messageRecive,當然這只是示例。    
  26.   
  27. Channel channel = connectFuture.awaitUninterruptibly().getChannel();    
  28.   
  29.   
  30.   
  31. // 得到用戶自定義的handler    
  32.   
  33. FactorialClientHandler handler =    
  34.   
  35.     (FactorialClientHandler) channel.getPipeline().getLast();    
  36.   
  37.   
  38.   
  39. // 從handler裏面取數據並且打印,這裏需要注意的是,handler.getFactorial使用了從結果隊列result take數據的阻塞方法,而結果隊列會在messageRecieve事件發生時被填充接收回來的數據    
  40.   
  41. System.err.format(    
  42.   
  43.         "Factorial of %,d is: %,d", count, handler.getFactorial());   
 

Netty提供了NIO與BIO(OIO)兩種模式處理這些邏輯,其中NIO主要通過一個BOSS線程處理等待鏈接的接入,若干個WORKER線程(從worker線程池中挑選一個賦給Channel實例,因爲Channel實例持有真正的 java網絡對象)接過BOSS線程遞交過來的CHANNEL進行數據讀寫並且觸發相應事件傳遞給pipeline進行數據處理,而BIO(OIO)方式服務器端雖然還是通過一個BOSS線程來處理等待鏈接的接入,但是客戶端是由主線程直接connect,另外寫數據C/S兩端都是直接主線程寫,而數據讀操作是通過一個WORKER 線程BLOCK方式讀取(一直等待,直到讀到數據,除非channel關閉)。

網絡動作歸結到最簡單就是服務器端bind->accept->read->write,客戶端 connect->read->write,一般bind或者connect後會有多次read、write。這種特性導致,bind,accept與read,write的線程分離,connect與read、write線程分離,這樣做的好處就是無論是服務器端還是客戶端吞吐量將有效增大,以便充分利用機器的處理能力,而不是卡在網絡連接上,不過一旦機器處理能力充分利用後,這種方式反而可能會因爲過於頻繁的線程切換導致性能損失而得不償失,並且這種處理模型複雜度比較高。

採用什麼樣的網絡事件響應處理機制對於網絡吞吐量是非常重要的,Netty採用的是標準的SEDA(Staged Event-Driven Architecture)架構[http://en.wikipedia.org/wiki/ Staged_event-driven_architecture],其所設計的事件類型,代表了網絡交互的各個階段,並且在每個階段發生時,觸發相應事件交給初始化時生成的pipeline實例進行處理。事件處理都是通過Channels類的靜態方法調用開始的,將事件、channel傳遞給 channel持有的Pipeline進行處理,Channels類幾乎所有方法都爲靜態,提供一種Proxy的效果(整個工程裏無論何時何地都可以調用其靜態方法觸發固定的事件流轉,但其本身並不關注具體的處理流程)。

Channels部分事件流轉靜態方法
1.fireChannelOpen 2.fireChannelBound 3.fireChannelConnected 4.fireMessageReceived 5.fireWriteComplete 6.fireChannelInterestChanged
7.fireChannelDisconnected 8.fireChannelUnbound 9.fireChannelClosed 10.fireExceptionCaught 11.fireChildChannelStateChanged

Netty提供了全面而又豐富的網絡事件類型,其將java中的網絡事件分爲了兩種類型Upstream和Downstream。一般來說,Upstream類型的事件主要是由網絡底層反饋給Netty的,比如messageReceived,channelConnected等事件,而Downstream類型的事件是由框架自己發起的,比如bind,write,connect,close等事件。
netty event

Netty的Upstream和Downstream網絡事件類型特性也使一個Handler分爲了3種類型,專門處理Upstream,專門處理Downstream,同時處理Upstream,Downstream。實現方式是某個具體Handler通過繼承ChannelUpstreamHandler和ChannelDownstreamHandler類來進行區分。PipeLine在Downstream或者Upstream類型的網絡事件發生時,會調用匹配事件類型的Handler響應這種調用。ChannelPipeline維持有所有handler有序鏈表,並且由handler自身控制是否繼續流轉到下一個handler(ctx.sendDownstream(e),這樣設計有個好處就是隨時終止流轉,業務目的達到無需繼續流轉到下一個handler)。下面的代碼是取得下一個處理Downstream事件的處理器。

Java代碼  收藏代碼
  1. DefaultChannelHandlerContext realCtx = ctx;    
  2.   
  3. while (!realCtx.canHandleUpstream()) {    
  4.   
  5.     realCtx = realCtx.next;    
  6.   
  7.     if (realCtx == null) {    
  8.   
  9.         return null;    
  10.   
  11.     }    
  12.   
  13. }    
  14.   
  15.      
  16.   
  17. return realCtx;   
 

如果是一個網絡會話最末端的事件,比如messageRecieve,那麼可能在某個handler裏面就直接結束整個會話,並把數據交給上層應用,但是如果是網絡會話的中途事件,比如connect事件,那麼當觸發connect事件時,經過pipeline流轉,最終會到達掛載pipeline最底下的ChannelSink實例中,這類實例主要作用就是發送請求和接收請求,以及數據的讀寫操作。
Netty SINK

NIO方式ChannelSink一般會有1個BOSS實例(implements Runnable),以及若干個worker實例(不設置默認爲cpu cores*2個worker),這在前面已經提起過,BOSS線程在客戶端類型的ChannelSink和服務器端類型的ChannelSink觸發條件不一樣,客戶端類型的BOSS線程是在發生connect事件時啓動,主要監聽connect是否成功,如果成功,將啓動一個worker線程,將connected的channel交給這個線程繼續下面的工作,而服務器端的BOSS線程是發生在bind事件時啓動,它的工作也相對比較簡單,對於channel.socket().accept()進來的請求向Nioworker進行工作分配即可。這裏需要提到的是,Server端ChannelSink實現比較特別,無論是NioServerSocketPipelineSink 還是OioServerSocketPipelineSink的eventSunk方法實現都將channel分爲 ServerSocketChannel和SocketChannel分開處理。這主要原因是Boss線程accept()一個新的連接生成一個 SocketChannel交給Worker進行數據接收。

public void eventSunk(
           ChannelPipeline pipeline, ChannelEvent e) throws Exception {
       Channel channel = e.getChannel();
       if (channel instanceof NioServerSocketChannel) {
           handleServerSocket(e);
       } else if (channel instanceof NioSocketChannel) {
           handleAcceptedSocket(e);
       }
   }
NioWorker worker = nextWorker();
               worker.register(new NioAcceptedSocketChannel(
                       channel.getFactory(), pipeline, channel,
                       NioServerSocketPipelineSink.this, acceptedSocket,
                       worker, currentThread), null);

另外兩者實例化時都會走一遍如下流程:

Java代碼  收藏代碼
  1. setConnected();    
  2.   
  3.       fireChannelOpen(this);    
  4.   
  5.       fireChannelBound(this, getLocalAddress());    
  6.   
  7.       fireChannelConnected(this, getRemoteAddress());    
 

而對應的ChannelSink裏面的處理代碼就不同於ServerSocketChannel了,因爲走的是 handleAcceptedSocket(e)這一塊代碼,從默認實現代碼來說,實例化調用 fireChannelOpen(this);fireChannelBound(this,getLocalAddress());fireChannelConnected(this,getRemoteAddress())沒有什麼意義,但是對於自己實現的ChannelSink有着特殊意義。具體的用途我沒去了解,但是可以讓用戶插手Server accept連接到準備讀寫數據這一個過程的處理。

Java代碼  收藏代碼
  1. switch (state) {    
  2.   
  3.           case OPEN:    
  4.   
  5.               if (Boolean.FALSE.equals(value)) {    
  6.   
  7.                   channel.worker.close(channel, future);    
  8.   
  9.               }    
  10.   
  11.               break;    
  12.   
  13.           case BOUND:    
  14.   
  15.           case CONNECTED:    
  16.   
  17.               if (value == null) {    
  18.   
  19.                   channel.worker.close(channel, future);    
  20.   
  21.               }    
  22.   
  23.               break;    
  24.   
  25.           case INTEREST_OPS:    
  26.   
  27.               channel.worker.setInterestOps(channel, future, ((Integer) value).intValue());    
  28.   
  29.               break;    
  30.   
  31.           }    
 

Netty提供了大量的handler來處理網絡數據,但是大部分是CODEC相關的,以便支持多種協議,下面一個圖繪製了現階段Netty提供的Handlers(紅色部分不完全)
Netty HANDLER_smal

Netty實現封裝實現了自己的一套ByteBuffer系統,這個ByteBuffer系統對外統一的接口就是ChannelBuffer,這個接口從整體上來說定義了兩類方法,一種是類似getXXX(int index…),setXXX(int index…)需要指定開始操作buffer的起始位置,簡單點來說就是直接操作底層buffer,並不用到Netty特有的高可重用性buffer特性,所以Netty內部對於這類方法調用非常少,另外一種是類似readXXX(),writeXXX()不需要指定位置的buffer操作,這類方法實現放在了AbstractChannelBuffer,其主要的特性就是維持buffer的位置信息,包括readerIndex,writerIndex,以及回溯作用的markedReaderIndex和markedWriterIndex,當用戶調用readXXX()或者writeXXX()方法時,AbstractChannelBuffer會根據維護的readerIndex,writerIndex計算出讀取位置,然後調用繼承自己的ChannelBuffer的getXXX(int index…)或者setXXX(int index…)方法返回結果,這類方法在Netty內部被大量調用,因爲這個特性最大的好處就是很方便地重用buffer而不必去費心費力維護index或者新建大量的ByteBuffer。

另外WrappedChannelBuffer接口提供的是對ChannelBuffer的代理,他的用途說白了就是重用底層buffer,但是會轉換一些buffer的角色,比如原本是讀寫皆可 ,wrap成ReadOnlyChannelBuffer,那麼整個buffer只能使用readXXX()或者getXXX()方法,也就是隻讀,然後底層的buffer還是原來那個,再如一個已經進行過讀寫的ChannelBuffer被wrap成TruncatedChannelBuffer,那麼新的buffer將會忽略掉被wrap的buffer內數據,並且可以指定新的writeIndex,相當於slice功能。
Netty BUFFER

Netty實現了自己的一套完整Channel系統,這個channel說實在也是對java 網絡做了一層封裝,加上了SEDA特性(基於事件響應,異步,多線程等)。其最終的網絡通信還是依靠底下的java網絡api。提到異步,不得不提到Netty的Future系統,從channel的定義來說,write,bind,connect,disconnect,unbind,close,甚至包括setInterestOps等方法都會返回一個channelFuture,這這些方法調用都會觸發相關網絡事件,並且在pipeline中流轉。Channel很多方法調用基本上不會馬上就執行到最底層,而是觸發事件,在pipeline中走一圈,最後纔在channelsink中執行相關操作,如果涉及網絡操作,那麼最終調用會回到Channel中,也就是serversocketchannel,socketchannel,serversocket,socket等java原生網絡api的調用,而這些實例就是jboss實現的channel所持有的(部分channel)。
Netty CHANNEL_small

Netty新版本出現了一個特性zero-copy,這個機制可以使文件內容直接傳輸到相應channel上而不需要通過cpu參與,也就少了一次內存複製。Netty內部ChunkedFile 和 FileRegion 構成了non zero-copy 和zero-copy兩種形式的文件內容傳輸機制,前者需要CPU參與,後者根據操作系統是否支持zero-copy將文件數據傳輸到特定channel,如果操作系統支持,不需要cpu參與,從而少了一次內存複製。ChunkedFile主要使用file的read,readFully等API,而FileRegion使用FileChannel的transferTo API,2者實現並不複雜。Zero-copy的特性還是得看操作系統的,本身代碼沒有很大的特別之處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章