1. 程式人生 > >NIO實現非阻塞Socket程式設計

NIO實現非阻塞Socket程式設計

前言
基於阿里面試時,面試官問我,我做的聊天專案裡,考慮過效能沒有,是怎麼解決程式卡頓現象的,針對客戶端,當在傳送檔案時,如果卡頓,怎麼辦,同時想聊天,當時程式我是基於多執行緒實現的,在客戶端裡,聊天時啟動一個執行緒,傳送檔案時,啟動另一個執行緒,所以在客戶端並不會影響,面試官就說知道了,但是,針對伺服器的效能優化,我做的不好,針對每一個客戶端開啟執行緒,造成伺服器壓力過大,因為現在沒有上線,所以不會面對這個問題。
博文地址:http://blog.csdn.net/u011958281/article/details/74556377

問題
網路通訊是基於阻塞式API的——即當程式執行輸入,輸出操作以後,在這些操作返回之前會一直阻塞執行緒,所以伺服器端必須為每個客戶端提供一個獨立的執行緒來進行處理。當伺服器端需要同時處理大量客戶端時,這種做法會導致效能下降,使用NIO API則可以讓伺服器端使用一個或者有限的幾個執行緒來同時處理連線到伺服器端的所有客戶端。

解決
補充知識:新IO和傳統IO有相同的目的,都是用於進行輸入/輸出,但是新的IO使用了不同的方式來處理輸入流,新的IO採用記憶體對映的方式來處理輸入輸出,新IO將檔案或檔案的一部分割槽域對映到記憶體中,這樣就可以此昂訪問記憶體一樣來訪問檔案了,通過這種方式來進行輸入輸出比傳統的輸入輸出要快很多。

Channel(通道)和Buffer(緩衝)是新IO中的兩個核心物件,與傳統的InputStream和OutputStream最大的區別就是。它提供一個map()方法,可以直接將一塊資料對映到記憶體中,如果說傳統的IO是針對於面向流的處理,則新IO是面向塊的處理。

  • Buffer
    從內部結構來看,Buffer就像一個數組,它可以儲存多個型別相同的資料,Buffer是一個抽象類,最常用的子類就是ByteBuffer,它可以在底層位元組陣列上進行get/set操作,還有CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,DoubleBuffer.
  • Channel
    程式不能直接訪問Channel中的資料,包括讀取,寫入都不行,Channel只能與Buffer進行互動,也就是說,如果要從Channel取一些資料,必須先用Buffer從Channel中取出一些資料,然後讓程式從Buffer中取出這些資料。

    1. Pipe.SkinChannel和Pipe.SourceChannel用於支援執行緒之間通訊的管道
    2. ServerSocketChannel,SocketChannel是用於支援TCP通訊的Channel
    3. DatagramChannel則是用於支援UDP網路通訊的

JAVA的NIO為非阻塞式Socket通訊提供了下面幾個特殊類:

  • Selector:所有希望採用通過非阻塞式方式通訊的Channel都應該註冊到Selector物件,可以通過呼叫此類的Open()靜態方法來建立Selector例項。
    Selector可以同時監聽多個SelectorChannel的IO狀態,是非阻塞式IO的核心

僅用單個執行緒來處理多個Channels的好處是,只需要更少的執行緒來處理通道。事實上,可以只用一個執行緒處理所有的通道。對於作業系統來說,執行緒之間上下文切換的開銷很大,而且每個執行緒都要佔用系統的一些資源(如記憶體)。因此,使用的執行緒越少越好。

Selector的建立
通過呼叫Selector.open()方法建立一個Selector,如下:

Selector selector = Selector.open();

向Selector註冊通道

為了將Channel和Selector配合使用,必須將channel註冊到selector上。通過SelectableChannel.register()方法來實現,如下:

channel.configureBlocking(false);
SelectionKey key = channel.register(selector,
Selectionkey.OP_READ);

與Selector一起使用時,Channel必須處於非阻塞模式下。這意味著不能將FileChannel與Selector一起使用,因為FileChannel不能切換到非阻塞模式。而套接字通道都可以。

注意register()方法的第二個引數。這是一個“interest集合”,意思是在通過Selector監聽Channel時對什麼事件感興趣。可以監聽四種不同型別的事件:

Connect
Accept
Read
Write

通道觸發了一個事件意思是該事件已經就緒。所以,某個channel成功連線到另一個伺服器稱為“連線就緒”。一個server socket channel準備好接收新進入的連線稱為“接收就緒”。一個有資料可讀的通道可以說是“讀就緒”。等待寫資料的通道可以說是“寫就緒”。

這四種事件用SelectionKey的四個常量來表示:

SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE

如果你對不止一種事件感興趣,那麼可以用“位或”操作符將常量連線起來,如下:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

這裡寫圖片描述

伺服器上所有的Channel(包括ServerSocketChannel和SocketChannel)都需要向Selector註冊,而該Selector則負責監視其中的Socket的IO狀態,當其中任意一個或幾個Channel具有可用的IO操作時,該Selector的select()方法將會返回大於0整數,該整數就表示有幾個Channel具有IO操作,並提供了secletorKeys()返回selectionKeys集合,正是通過這些Selcetor的例項的select()方法,可以知道當前有多少channel是否有需要的IO操作。