概覽
最近弄幾篇NIO基礎相關的內容,用於Netty源碼解析使用。因爲沒有這些知識就產生不了問題,也就無法深入一個成熟的網絡IO框架源碼進行學習。
NIO三大核心組件:
1,Channel
2,Buffer
3,Selector
先概述一下三者的概念和之間的關係,再逐個瞭解組件的API打個基礎。
對於IO通信,必然需要連接起來才能通信,Channel可以理解成一個連接連端的通道,有了通道就可以讀寫數據了,Buffer是和Channel交互的讀寫數據的組件,讀操作就是從Channel中的數據讀到Buffer上,寫就是把數據從Buffer上寫到Channel上。在通信的流程中,會有很多個通道產生IO事件需要處理,Selector可以理解成由它來監聽全部的通道上發生的事件,然後我們通過Selector可以獲得我們感興趣的IO事件,然後進行不同的操作。
Channel
這裏就涉及兩個Channel類型:
-
SocketServerChannel
監聽TCP連接請求,爲每個監聽到的請求,創建一個SocketChannel套接字通道。
-
SocketChannel
用於Socket套接字TCP連接的數據讀寫。
先進行連接,然後讀寫數據,所以對於被連接的一端,是用SocketServerChannel來監聽連接請求的,監聽到後就創建好SocketChannel,我們可以對SocketChannel進行監聽,進行讀寫操作。這個流程在實現代碼中體現,可以先記住這個意思就行。
接下去對關鍵的API進行解釋:
ServerSocketChannel#open
開啓一個服務端的Socket Channel。
ServerSocketChannel#bind(SocketAddress)
Server端作爲接受連接的一方,怎麼說也要先綁定一個端口吧,否則人家想連接你都找不到地址。所以執行open開啓一個Channel後一般就執行bind綁定到一個本地地址(SocketAddress)。這樣客戶端往這個地址連接的時候,服務端的Channel才能接受到。
AbstractSelectableChannel#configureBlocking
這個方法由SocketServerChannel
和SocketChannel
的抽象父類提供,用於設置Channel是阻塞還是非阻塞的。這個設置關係到channel的一些方法是否阻塞的特性,在後面的會被涉及。
ServerSocketChannel#accept
開啓一個Channel,綁定好了地址,如果有客戶端來連了,就用這個方法來接受並返回一個SocketChannel
,和前面概述的內容對應到了,非常的順利成章,這個方法就涉及前面設置的是否阻塞,如果設置的阻塞,那麼這個方法就會阻塞直到有客戶端來連接,如果設置的是非阻塞,那麼就看此時有沒有新連接,如果有就返回一個SocketChannel
,否則返回null。
SelectableChannel#register(Selector, int)
Selector前面介紹過是用來監聽Channel的,作爲被監聽者,需要有一個註冊的動作,並且在註冊的時候告訴監聽者監聽什麼內容,這個內容在SelectionKey中列舉,一共也就四個:
- OP_READ 讀就緒
- OP_WRITE 寫就緒
- OP_CONNECT 連接操作
- OP_ACCEPT 接受連接
需要理解這些Key表示的是一種就緒的事件,不是事件本身,比如我們監聽OP_ACCEPT,那麼在後續詢問Selector的時候,Selector告訴我們的答案是有一個連接已經過來可以接受這個連接了,所以接下去要做的是去接受這個連接。
SocketChannel#read(ByteBuffer)
從SocketChannel讀取數據到ByteBuffer。
SocketChannel.connect(SocketAddress remote)
向遠程地址發起連接。作爲客戶端連接服務端使用。
SocketChannel.finishConnect()
判斷前面connect方法是否結束。
Buffer
JDK實現了各種類型的Buffer:
IntBuffer, CharBuffer, FloatBuffer, DoubleBuffer, ShortBuffer, LongBuffer, ByteBuffer
重點關注ByteBuffer就可以了,我們知道它是和Chennel交互數據的,可以稍微瞭解一下它的結構:
內部有一個byte[]
的數據塊,讀寫數據就是操作這個數組,初始化的時候會確定一個容量,用capacity
來標識,然後使用position
來表示目前讀寫的位置,比如在讀的時候從0開始遞增。還有一個limit
字段表示讀寫的最大上限。Buffer有讀寫模式,會有切換模式的操作,切換模式後position
和limit
定義是調整的,所以值也會調整。
後面將仔細分析Buffer的實現原理。
ByteBuffer#allocate
分配一個新的字節緩衝區,初始化操作
ByteBuffer#put(byte)
put方法用於寫數據,position會隨着寫入的數據遞增。
ByteBuffer#get()
get方法用於從Buffer獲取數據
Buffer#flip
反轉操作,也就是切換模式,具體操作是把limit設置成當前的position,position設置成0。
Buffer#clear
清理操作,position設置成0,limit設置成capacity。
Selector
Selector 用於監控Channel上的就緒事件,這個能力是實現多路複用的IO模型的關鍵。我們可以在Channel通過register方法註冊到Selector上的時候傳入感興趣的就緒事件,然後通過select方法檢測是否有就緒事件。
Selector#select
選擇出就緒事件,返回數量,大於0表示有就緒事件。無參數的方法是阻塞一直到可以返回數量或者被weakup或線程被中斷,我們可以通過重載的方法(select(long timeout)
)中參數調整這個方法的阻塞時間,也可以選擇selectNow()
執行一次選擇操作,立即返回。
Selector#wakeup
效果就是把前面select方法阻塞還未返回的操作喚醒立即返回。如果此時沒有調用select的那麼下次的調用立即返回。
Selector#selectedKeys
獲得事件就緒的SelectionKey類型集合,我們可以通過SelectionKey獲得事件就緒的Channel,然後就可以對這些Channel進行相應的讀寫等操作。
SelectionKey#interestOps(int)
設置監聽的事件,這個方法可以調整對應的SocketChannel監聽的感興趣事件。
SelectionKey#interestOps()
獲取SelectionKey的就緒事件興趣集,一般先調用這個方法在調用前面的方法調整。
SelectionKey#attach(Object ob)
綁定一個對象
SelectionKey#attachment()
返回前面綁定的對象
入門代碼
先代碼入個門,我們已經瞭解了一些NIO的核心組件的關鍵Api,那麼我們爲了實現一個網絡通信的功能,應該組裝起這些API呢?先實現一個監聽端口,能夠獲得連接,並且從連接讀取數據這樣一個基本的功能代碼。
服務端代碼:
public class NioServer {
public static void start() throws IOException {
// 開啓選擇器
Selector selector = Selector.open();
// 開啓server socket channel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 設置成非阻塞模式
serverSocketChannel.configureBlocking(false);
// server 監聽端口綁定
serverSocketChannel.bind(new InetSocketAddress(12022));
// channel 註冊到選擇器上,IO事件爲Accept
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// select 操作 阻塞等待Accept狀態就緒
while (selector.select() > 0) {
// 獲取全部就緒的selectedKeys
Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
// 遍歷selectedKeys
while (selectionKeys.hasNext()) {
SelectionKey selectionKey = selectionKeys.next();
// 就緒狀態爲Accept
if (selectionKey.isAcceptable()) {
// 接受一個連接 得到一個SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
// 設置成非阻塞模式
socketChannel.configureBlocking(false);
// 把SocketChannel 註冊到Selector 感興趣的事件是讀事件
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
// 分配一個新的字節緩衝區
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 從Channel中讀取數據到Buffer
while ((socketChannel.read(byteBuffer)) > 0) {
// 翻轉Buffer
byteBuffer.flip();
// 清理Buffer
byteBuffer.clear();
}
String readStr = new String(byteBuffer.array());
System.out.print("" + readStr);
socketChannel.close();
}
selectionKeys.remove();
}
// serverSocketChannel.close();
}
}
public static void main(String[] args) throws IOException {
start();
}
客戶端代碼:
public static void start() throws IOException {
InetSocketAddress inetSocketAddress = new InetSocketAddress(12022);
// 開啓一個Socket Channel 並且連接遠程地址
SocketChannel socketChannel = SocketChannel.open();
// 設置爲非阻塞
socketChannel.configureBlocking(false);
// 連接遠程地址
socketChannel.connect(inetSocketAddress);
// 等待連接成功
while (!socketChannel.finishConnect()) {
}
// 分配Buffer空間
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 寫入Buffer
byteBuffer.put("hello world".getBytes());
// 切換成讀模式
byteBuffer.flip();
// 把Buffer 寫入Socket Channel
socketChannel.write(byteBuffer);
// 關閉寫連接
socketChannel.shutdownOutput();
// 關閉socket Channel
socketChannel.close();
}
public static void main(String[] args) throws IOException {
start();
}
參考: