揭開網絡編程的神祕面紗2(BIO、偽異步IO、NIO、AIO)

從上文 中我們知道,不管是BIO模型還是偽異步IO模型,服務端都需要創建一定量的線程來處理客戶端Socket的請求,也就是說在任何時候都可能有大量線程處於休眠狀態,只是等待輸入或者輸出數據就緒;而且由於需要為每個線程的調用棧分配內存。這些都是一種資源的浪費。即使jvm在物理上有很大的資源,可以支持非常大數量的線程,但是線程上下文切換所帶來的開銷也會給我們帶來麻煩。

揭開網絡編程的神秘面紗2(BIO、偽異步IO、NIO、AIO)

所以BIO模型和偽異步IO模型只適用於中小數量的客戶端請求。如果需要支撐高併發的數據,我們需要選擇NIO或者AIO模型。

由於NIO涉及到Buffer、Channel、Selector等基礎知識,為了便於理解,我們舉個例子。小JIA要結婚了,需要給親朋好友發請柬,這個時候小JIA需要一一拜訪,每次拿一張請柬給一個親朋好友,發送完成後,再回家再取一張請柬發給另外一個親朋好友,直到所有的好友都拿到請柬。這就是普通的Socket模式,來一個請求ServerSocket就進行處理,處理完再繼續接受請求。發到一半的時候小JIA發現還有幾百個親朋好友沒發,照這個速度發下去,得到猴年馬月,說不定婚禮都過了,請柬還沒發完。這個時候小JIA開始動員親朋好友來幫忙發請柬。小JIA通知了幾個親戚和朋友,讓他們過來拿請柬幫忙分發,小JIA的父母幫忙分揀請柬。這就是NioSocket,Buffer就是請柬,而Channel就是幫忙分發請柬的親戚朋友,而小JIA的父母充當了Selector的職責,負責請柬的分揀。這樣的處理方式極大的提高了IO的效率。小JIA再也不用擔心親戚朋友沒有準時收到請柬而錯過婚禮了。

NioSocket提供ServerSocketChannel和SocketChannel,分別對應ServerSocket和Socket。接下來我們來介紹NIO的幾個基礎知識。

Channel(通道)

Channel就像水管一樣,是一個通道。通道與流的不同之處在於通道是雙向的,可以用來讀、寫或同時讀寫操作。因為是雙向的,所以通道可以比流更好地反應底層操作系統的真實情況。

揭開網絡編程的神秘面紗2(BIO、偽異步IO、NIO、AIO)

Channel主要分為兩大類:SelectableChannel(用戶網絡讀寫)和FileChannel(用於文件操作)。ServerSocketChannel和SocketChannel都是SelectableChannel的子類。

Buffer(緩衝區)

Channel提供從文件、網絡讀取數據的通道,可是讀取或寫入數據都必須經過Buffer。Buffer實際上是一個數組,並提供對數據結構化訪問以及維護讀寫位置等信息。

揭開網絡編程的神秘面紗2(BIO、偽異步IO、NIO、AIO)

具體的緩存區有:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer和DoubleBuffer。它們實現了共同的接口:Buffer。

Selector(多路複用選擇器)

Selector一般稱為選擇器,當然也可以叫做多路複用器。它是Java NIO核心組件中的一個,用於檢查一個或者多個NIO Channel(通道)的狀態是否處於可讀、可寫。

揭開網絡編程的神秘面紗2(BIO、偽異步IO、NIO、AIO)

一個Selector可以同時輪詢多個Channel,因為jdk使用了epoll()代替傳統的select實現,所以沒有最大連接句柄1024/2048的限制。也就是一個線程負責Selector的輪詢,可以管理成千上萬個網絡客戶端連接。

NIO通信模型

NIO提供了一個全新底層I/O模型。採用面向塊的概念,IO操作以大的數據塊為單位進行操作,而不是一個個字節或字符進行操作,因此性能有了很大的提高。

//創建選擇器
selector = Selector.open();
//打開監聽通道
serverChannel = ServerSocketChannel.open();
//如果為 true,則此通道將被置於阻塞模式;如果為 false,則此通道將被置於非阻塞模式
serverChannel.configureBlocking(false);//開啟非阻塞模式
//綁定端口 backlog設為1024
serverChannel.socket().bind(new InetSocketAddress(port),1024);
//監聽客戶端連接請求
serverChannel.register(selector, SelectionKey.OP_ACCEPT);

1、初始化多路複用器selector、serverSocketChannel通道、設置通道的模式為非阻塞、註冊channel到selector上,並監聽accept請求;

Set keys = selector.selectedKeys();
Iterator it = keys.iterator();

2、啟動Server服務器,循環selectedKeys,當有channel準備好時就處理,否則一直循環。

3、selectedKey保存了當前請求的Channel和Selector,並提供了不同的操作類型。共有四種:

  • SelectionKey.OP_ACCEPT //請求操作
  • SelectionKey.OP_CONNECT //鏈接操作
  • SelectionKey.OP_READ //讀操作
  • SelectionKey.OP_WRITE //寫操作
//處理新接入的請求消息
if(key.isAcceptable()){
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
\t //通過ServerSocketChannel的accept創建SocketChannel實例
\t //完成該操作意味著完成TCP三次握手,TCP物理鏈路正式建立
\t SocketChannel sc = ssc.accept();
\t //設置為非阻塞的
\t sc.configureBlocking(false);
\t //註冊為讀
\t sc.register(selector, SelectionKey.OP_READ);
}

3.1 只有在register方法中註冊了相應的操作,Selector才會關心相應類型操作的請求。比如我們在上面的代碼中註冊了SelectionKey.OP_ACCEPT請求操作,那麼Seletor就關心新的客戶端接入,如果監聽到新的客戶端接入,就處理新的接入請求,完成TCP三次握手,建立物理鏈路。並把ServerSocketChannel註冊為SelectionKey.OP_READ讀操作。

//讀消息
if(key.isReadable()){
\t SocketChannel sc = (SocketChannel) key.channel();
\t //創建ByteBuffer,並開闢一個1M的緩衝區
\t ByteBuffer buffer = ByteBuffer.allocate(1024);
\t //讀取請求碼流,返回讀取到的字節數
\t int readBytes = sc.read(buffer);

\t //讀取到字節,對字節進行編解碼
\t if(readBytes>0){
}//鏈路已經關閉,釋放資源
\t else if(readBytes<0){
\t key.cancel();
\t sc.close();
\t }
}

3.2 當把新接入的客戶端連接註冊到Selector上,開始監聽都操作,讀取客戶端發送的網絡消息,把讀取到的消息放到緩衝區,然後進行服務端數據處理操作。

由於jdk的selector在linux等主流操作系統上通過epoll實現,它沒有鏈接句柄數的限制,意味著一個selector可以連接成千上萬個客戶端,而性能不會隨著客戶端連接數的增長而線性下降,因此,它適合做高性能、高負載的網絡服務器。

AIO通信模型

AIO是異步IO的縮寫,雖然NIO在網絡操作中,提供了非阻塞的方法,但是NIO的IO行為還是同步的。對於NIO來說,我們的業務線程是在IO操作準備好時,得到通知,接著由這個線程自行進行IO操作,IO操作本身是同步的。

//創建服務端通道
channel = AsynchronousServerSocketChannel.open();
//綁定端口

channel.bind(new InetSocketAddress(port));
//用於接收客戶端的連接
channel.accept(this,new AcceptHandler());

1、與NIO不同,當進行讀寫操作時,只須直接調用API的read或write方法即可。這兩種方法均為異步的,對於讀操作而言,當有流可讀取時,操作系統會將可讀的流傳入read方法的緩衝區,並通知應用程序;對於寫操作而言,當操作系統將write方法傳遞的流寫入完畢時,操作系統主動通知應用程序。 即可以理解為,read/write方法都是異步的,完成後會主動調用回調函數。在JDK1.7中,這部分內容被稱作NIO.2,主要在Java.nio.channels包下增加了下面四個異步通道:

  • AsynchronousSocketChannel
  • AsynchronousServerSocketChannel
  • AsynchronousFileChannel
  • AsynchronousDatagramChannel

在AIO socket編程中,服務端通道是AsynchronousServerSocketChannel,這個類提供了一個open()靜態工廠,一個bind()方法用於綁定服務端IP地址(還有端口號),另外還提供了accept()用於接收用戶連接請求。

public class AcceptHandler implements CompletionHandler{
\t@Override
\tpublic void completed(AsynchronousSocketChannel channel,AsyncServerHandler serverHandler) {
\t\tserverHandler.channel.accept(serverHandler, this);
\t\t//創建新的Buffer
\t\tByteBuffer buffer = ByteBuffer.allocate(1024);
\t\t//異步讀 第三個參數為接收消息回調的業務Handler

\t\tchannel.read(buffer, buffer, new ReadHandler(channel));
\t}
\t
\t@Override
\tpublic void failed(Throwable exc, AsyncServerHandler serverHandler) {
\t\texc.printStackTrace();
\t}

}

2、發出一個事件(accept read write等)之後要指定時間處理類(回調函數),AIO中的事件處理類是CompletionHandler,這個接口定義了兩個方法,分別在一部操作和成功時被回調。

  • void completed(V result, A attachment);
  • void failed(Throwable exc, A attachment);

所以對於AIO來說,它不是在IO準備好時再通知線程,而是在IO操作已經完成後,再通知線程發出通知。因此AIO是不會阻塞的,此時我們的業務邏輯將變成一個回調函數,等待IO操作完成後,由系統自動觸發。

總結

BIO適用於連接數目比較小且固定的架構,這種方式對服務器資源要求比較高。但程序簡單易理解。

NIO適用於連接數目多且連接比較短(輕操作)的架構,比如聊天服務器,編程複雜。

AIO適用於連接數目多且連接比較長(重操作)的架構,比如相冊服務器,編程比較複雜。


分享到:


相關文章: