NIO基礎-Netty系列-1

概覽

最近弄幾篇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

這個方法由SocketServerChannelSocketChannel的抽象父類提供,用於設置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有讀寫模式,會有切換模式的操作,切換模式後positionlimit定義是調整的,所以值也會調整。

後面將仔細分析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();
}

參考:

https://www.cnblogs.com/yungyu16/p/13065194.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章