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實現,它沒有連接句柄的限制。