NIO詳解(四):NIO編程

1. NIO類庫簡介

1.1 緩衝區Buffer

Buffer是一個對象,它包含了一些要寫入或者要讀出的數據。在NIO類庫中加入Buffer對象,體現了新庫和原來I/O的一個重要區別。在NIO庫中,所有的數據都是用緩衝區處理的。在讀取數據時,它是直接讀取到緩衝區中的;在寫入到緩衝區時。任何時候訪問NIO中的數據,都是通過緩衝區進行操作。

緩衝區實質上是一個數組。通常它是一個字節數據(ByteBuffer),也可以使用其他種類的數組。但是一個緩衝區不僅僅是一個數組,緩衝區還提供了對數據的結構化訪問以及維護讀寫位置(limit)等信息。

  • ByteBuffer:字節緩衝區
  • CharBuffer:字符緩衝區
  • ShortBuffer:短整形緩衝區
  • IntBuffer:整形緩衝區
  • LongBuffer:長整形緩衝區
  • FloatBuffer:浮點型緩衝區
  • DoubleBuffer:雙精度浮點型緩衝區

緩衝區的繼承關係如下:

在這裏插入圖片描述

1.2 通道Channel

Channel是一個通道,它就像自來水管道一樣,網絡數據通過Channel讀取和寫入。通道與流不同之處在於通道它是雙向的,流只是在一個方向上移動(一個流必須是InputStream或者OutputStream的子類),而通道可以用於讀、寫或者二者同時進行。因爲Channel是全雙工的,所以它可以比流更加映射底層操作系統地API。從類圖中可以看出,實際上Channel可以分爲兩大類:用於網絡讀寫的SelectabaleChannel和用於文件操作的FileChannel。ServerSocketChannel是一個可以監聽新進來的TCP連接的通道,就像標準IO中的ServerSocket一樣。

在這裏插入圖片描述

1.3 多路複用器Selector

Select會不斷地輪詢註冊在其上的Channel,如果某個Channel上面發生讀或者寫事件,這個Channel就處於就緒狀態,會被Selector輪詢出來,然後通過SelectionKey可以獲取就緒Channel集合,進行後續的I/O操作。一個多路複用器Selector可以同時輪詢多個Channel,由於JDK使用了epoll()代替傳統的select實現,所以它並沒有最大連接句柄1024/2048的輪詢,就可以接入成千上萬的客戶端。

2. NIO服務端序列圖

在這裏插入圖片描述

一。 打開ServerSocketChannel,用於監聽客戶端的連接。

ServerSocketChannel servChannel=ServerSocketChannel.open();

二。綁定監聽端口,設置連接爲非阻塞狀態。

servChannel.configureBlocking(false);
servChannel.socket().bind(new InetSocketAddress(port), 1024);

三。創建Reactor線程,創建多路複用器並啓動線程。

Selector selector = Selector.open();

四。將ServerSocketChannel註冊到Reactor線程的多路複用器Selector上,監聽ACCEPT事件

servChannel.register(selector, SelectionKey.OP_ACCEPT);

五。多路複用器在線程run方法的無線循環體內輪詢準備就緒的Key。

while (!stop) {
	    try {
		selector.select(1000);
		Set<SelectionKey> selectedKeys = selector.selectedKeys();
		Iterator<SelectionKey> it = selectedKeys.iterator();
		SelectionKey key = null;
		while (it.hasNext()) {
		    key = it.next();
		    it.remove();
		    try {
			handleInput(key);
		    } catch (Exception e) {
			if (key != null) {
			    key.cancel();
			    if (key.channel() != null)
				key.channel().close();
			}
		    }
		}
	    } catch (Throwable t) {
		t.printStackTrace();
	    }
	}

六。多路複用器監聽到有新的客戶端接入,處理新的接入請求,完成TCP三次握手,建立物理鏈路。

 if (key.isAcceptable()) {
		// Accept the new connection
		ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
		SocketChannel sc = ssc.accept();
}

七。設置客戶端鏈路爲非阻塞模式

sc.configureBlocking(false);

八。將接入的客戶端連接註冊到Reactor線程的多路複用器上,監聽讀操作,讀取客戶端發送的網絡消息。

sc.register(selector, SelectionKey.OP_READ);

九。異步讀取客戶端請求消息到緩衝區。

 if (key.isReadable()) {
		// Read the data
		SocketChannel sc = (SocketChannel) key.channel();
		ByteBuffer readBuffer = ByteBuffer.allocate(1024);
		int readBytes = sc.read(readBuffer);
        .....
 }

十。對ByteBuffer進行編碼解碼,如果有半包消息指針reset,繼續讀取後續的報文,將解碼成功的消息封裝成Task,投遞到業務線程池中,進行業務邏輯編排。

Object message=null;
while(buffer.hasRemain()){
    bytebuffer.mark();
    Object message=decode(byteBuffer);
    if(message==null){
        byteBufer.reset();
        break;
    }
    messageList.add(message);
}
if(!bytebuffer.hasRemain()){
    byteBuffer.clear();
}else
    byteBuffer.compact();

if(messageList!=null & !messageList.isEmpty()){
    for(Obbject messageE:messagList){
        handlerTask(messageE)
    }
}

十一。將POJO對象encode成ByteBuffer,調用SocketChannel的異步write接口,將消息異步發送給客戶端。

socketChannel.write(buffer).

3. NIO客戶端序列圖

在這裏插入圖片描述

一。打開SocketChannel,綁定客戶端本地地址。

SocketChannel clientChannel = SocketChannel.open();

二。設置SocketChannel爲非阻塞模式,同時設置客戶端連接的TCP參數。

socketChannel.configureBlocking(false);
socketChannel.socket().setReuseAddress(true);

三。異步連接服務器。判斷是否連接成功,如果連接成功,則直接註冊讀取狀態到多路複用器中,如果當前沒有連接成功,則向Reactor的多路複用器註冊OP_CONNECT狀態爲,監聽服務器端的TCP ACK應答。

// 如果直接連接成功,則註冊到多路複用器上,發送請求消息,讀應答
	if (socketChannel.connect(new InetSocketAddress(host, port))) {
	    socketChannel.register(selector, SelectionKey.OP_READ);
	    doWrite(socketChannel);
	} else
	    socketChannel.register(selector, SelectionKey.OP_CONNECT);

四。創建Reactor線程,創建多路複用器並啓動線程。

Selector selector = Selector.open();

五。多路複用器在線程run方法的無線循環體內輪詢準備就緒的Key。

while (!stop) {
	    try {
		selector.select(1000);
		Set<SelectionKey> selectedKeys = selector.selectedKeys();
		Iterator<SelectionKey> it = selectedKeys.iterator();
		SelectionKey key = null;
		while (it.hasNext()) {
		    key = it.next();
		    it.remove();
		    try {
			handleInput(key);
		    } catch (Exception e) {
			if (key != null) {
			    key.cancel();
			    if (key.channel() != null)
				key.channel().close();
			}
		    }
		}
	    } catch (Throwable t) {
		t.printStackTrace();
	    }
	}

六。接受connect事件處理。判斷連接結果,如果連接成功,註冊連接事件到多路複用器。註冊讀事件到多路複用器中。
```java
if (key.isConnectable()) {
		if (sc.finishConnect()) {
		    sc.register(selector, SelectionKey.OP_READ);
		    doWrite(sc);
		} else
		    System.exit(1);// 連接失敗,進程退出
	    }

七。異步讀取客戶端請求消息到緩衝區。

 if (key.isReadable()) {
		// Read the data
		SocketChannel sc = (SocketChannel) key.channel();
		ByteBuffer readBuffer = ByteBuffer.allocate(1024);
		int readBytes = sc.read(readBuffer);
        .....
 }

八。對ByteBuffer進行編碼解碼,如果有半包消息指針reset,繼續讀取後續的報文,將解碼成功的消息封裝成Task,投遞到業務線程池中,進行業務邏輯編排。

Object message=null;
while(buffer.hasRemain()){
    bytebuffer.mark();
    Object message=decode(byteBuffer);
    if(message==null){
        byteBufer.reset();
        break;
    }
    messageList.add(message);
}
if(!bytebuffer.hasRemain()){
    byteBuffer.clear();
}else
    byteBuffer.compact();

if(messageList!=null & !messageList.isEmpty()){
    for(Obbject messageE:messagList){
        handlerTask(messageE)
    }
}

九。將POJO對象encode成ByteBuffer,調用SocketChannel的異步write接口,將消息異步發送給客戶端。

socketChannel.write(buffer).

4. 總結

通過源碼分析,我們發現NIO編程的難度確實比同步阻塞BIO的大很多,我們的NIO程序中還沒有考慮“半包讀”和“半包寫”,如果加上這些,代碼會更加複雜。使用NIO編程的優點如下:

  • 客戶端發起連接的操作是異步的,可以通過多路複用器註冊OP_CONNECT等待後續結果,不需要像之前的客戶端那樣被同步阻塞。
  • SocketChannel的讀寫操作是異步的,如果沒有可讀寫的數據它不會等待,直接返回,這樣I/O通信線程就可以處理其他鏈路,不需要同步等待這個鏈路可用。
  • 線程模型的優化:由於JDK的Selector在Linux等主流操作系統上通過epoll實現,它沒有連接句柄的限制。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章