NIO的整體介紹和Demo

 BIO的步驟流程說明:

 

BIO代碼實現:

// 用於存儲讀取到的數據
// 用於存儲讀取到的數據
byte[] bytes = new byte[1024];
ServerSocket serverSocket= null;
try {
    serverSocket = new ServerSocket();
    serverSocket.bind(new InetSocketAddress(8080)) ; // 綁定監聽的端口
    for(;;){
        //線程阻塞,等待連接
        Socket socket = serverSocket.accept();
        // 如果有客戶端連接進來就往下走,如果沒有讀取到數據會堵塞
        InputStream input = socket.getInputStream();
        try{
            int read = input.read(bytes);
            String content = new String(bytes);
            System.out.println(content);
        }} catch (IOException e) {
            e.printStackTrace();
        }
    }
} catch (IOException e) {
    e.printStackTrace();
}

 客戶端:

try {
    Socket socket = new Socket("127.0.0.1",8080);
    OutputStream outputStream = socket.getOutputStream();
    outputStream.write("hello".getBytes());
} catch (IOException e) {
    e.printStackTrace();
}

 總結:

其中BIO可以通過採用多線程完成併發請求處理,accept方法處阻塞,不變,如果監聽到有客戶端連接進來,則開啓一個線程,並且將獲取到的和客戶端長連接交互的socket 作爲對象傳入子線程中,然後讓read方法的阻塞在子線程中出現,而主線程則繼續執行,然後循環到accept()方法中等待第二個客戶端線程的鏈接。

其中負責監聽的socket對象在創建之後會返回一個文件描述符,用於bind一個端口,這個bind調用的是操作系統的內核函數,調用listen 方法用於監聽,而accept方法則阻塞住不往下走,直到客戶端通過操作系統完成三次握手的過程之後accept會接受到這個客戶端,返回一個文件描述符(簡單理解就是和客戶端交互的socket對象),然後進入read方法,進行阻塞。可以通過開闢線程來執行read,而accept則循環監控連接;

缺點:線程頻繁創建和銷燬(線程棧消耗),上線文切換頻繁,不安全。無法解決單線程問題

NIO non-blocking IO(非阻塞IO)關鍵組成:

  1. channel 通道
  2. buffer 緩衝區
  3. selector 選擇器

1、channel通道:

Java NIO的通道類似流,但又有些不同,channel既可以從通道中讀取數據,又可以寫數據到通道。但流的讀寫通常是單向的。通道可以異步地讀寫。通道中的數據總是要先讀到一個Buffer,或者總是要從一個Buffer中寫入。正如上面所說,從通道讀取數據到緩衝區,從緩衝區寫入數據到通道。

通道的類型(參考右邊鏈接):http://ifeve.com/channels/

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

FileChannel 從文件中讀寫數據。

DatagramChannel 能通過UDP讀寫網絡中的數據。

SocketChannel 能通過TCP讀寫網絡中的數據。

ServerSocketChannel可以監聽新進來的TCP連接,像Web服務器那樣。對每一個新進來的連接都會創建一個SocketChannel。

以ServerSocketChannel爲例:首先先開啓ServerSocketChannel,調用open()方法,並且可以非阻塞的監聽新進連接,如果不設置爲非阻塞也會和BIO 一樣會放棄cpu的執行權,直到有新連接被監聽到纔會往下執行;該對象可產生一個用於監聽的ServerSocket對像,ServerSocket可以綁定監聽指定的端口;注意:FileChannel不能切換到非阻塞模式

// 開啓通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 獲取服務端監聽套接字
ServerSocket serverSocket = serverChannel.socket();
// 綁定監聽的端口 也可以直接用ServerSocketChannel 來監聽   
// serverChannel.bind(new InetSocketAddress(8080));
serverSocket.bind(new InetSocketAddress(8080));
// 設置通道爲非阻塞 accept()方法會立即返回,如果沒有監聽到新連接返回null,監聽到
//新連接則返回一個與新連接交互的SocketChannel通道
serverChannel.configureBlocking(false);

 

通道和緩衝區的數據交互

//將 Buffer 中數據寫入 Channel
channel.write(buff)
//從 Channel 讀取數據到 Buffer
channel.read(buff)

channel 註冊到選擇器上,register()如果已經註冊過的,更新對象和更新channel對什麼事件感興趣,註冊之後返回一個selectorKey,用於標識channel 和channel感興趣的事件,如果是有客戶端連接這個channel,並且事件是這個channel感興趣的 ,那麼操作系統會將這個連接分配給這個channel。關於底層應用的的共享內存啊、內核輪詢之類的可以自己去看馬士兵的NIO裏面介紹的挺多的。

2、buffer 緩衝區:

緩衝區本質上是一個內存塊,您可以在其中寫入數據,然後可以在以後再次讀取。該內存塊包裝在NIO Buffer對象中,數據從通道讀取到緩衝區,然後從緩衝區寫入通道,該對象提供了一組方法,可以更輕鬆地使用該內存塊。Java NIO 有以下Buffer類型:

  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

要獲取Buffer對象,您必須首先分配它。每個Buffer類都有一個allocate()執行此操作的方法

ByteBuffer buf = ByteBuffer.allocate(1024);

2.1、使用Buffer來讀取和寫入數據通常遵循以下四個步驟:

  1. 將數據寫入緩衝區
  2. 呼叫 buffer.flip()
  3. 從緩衝區讀取數據
  4. 調用buffer.clear()或buffer.compact()清除數據

2.1.1 將數據寫入緩衝區

數據寫入緩衝區有兩種方式:

int bytes = channel.read(buf); // 從通道讀取數據寫入緩衝池


buf.put("hello world".getByte());  // 在程序中向緩衝池添加數據

 

2.1.2 呼叫 buffer.flip()

buffer中的flip方法涉及到bufer中的Capacity,Position和Limit三個概念

  • 容量 (capacity)
  • 位置 (position)
  • 限制 (limit)

容量是在調用allocate(1024)時定義的容量,有興趣可以看看源碼:

public static ByteBuffer allocate(int capacity) {
    if (capacity < 0)
        throw new IllegalArgumentException();
    return new HeapByteBuffer(capacity, capacity);
}

// 傳入的都是容量所以說開始的時候cap = lim
HeapByteBuffer(int cap, int lim) {
    super(-1, 0, lim, cap, new byte[cap], 0);
}

// 調用上級
ByteBuffer(int mark, int pos, int lim, int cap,   // package-private
             byte[] hb, int offset)
{
    super(mark, pos, lim, cap);
    this.hb = hb;
    this.offset = offset;
}

// 容量賦值position的開始位置賦值爲0,標記設爲-1,mark是起輔助判斷作用。
Buffer(int mark, int pos, int lim, int cap) {       // package-private
    if (cap < 0)
        throw new IllegalArgumentException("Negative capacity: " + cap);
    this.capacity = cap;
    limit(lim);
    position(pos);
    if (mark >= 0) {
        if (mark > pos)
            throw new IllegalArgumentException("mark > position: ("
                                               + mark + " > " + pos + ")");
        this.mark = mark;
    }
}

// 最後會到這裏來
public final Buffer limit(int newLimit) {
    if ((newLimit > capacity) || (newLimit < 0))
        throw new IllegalArgumentException();
    limit = newLimit;
    if (position > limit) position = limit;
    if (mark > limit) mark = -1;
    return this;
}

所以初始化之後的值:limit = cap = 1024(我傳入的值) 而mark = -1 , position = 0 ;

讀取過程(下面鏈接講的挺清楚的):

https://blog.csdn.net/u013096088/article/details/78638245

讀取結束(消息長15):position 位於15;

調用flip()方法之後:

public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}

limit = 15 ; positon 重置;也就是說調用flip之後,讀寫指針指到緩存頭部,並且設置了最多隻能讀出之前寫入的數據長度(而不是整個緩存的容量大小)

簡答點描述就是:你將文件按一定的順序疊起來,然後領導突然說把已經疊好的按順序交給他,你只能把現在到了第幾個文件記住,然後把已經整理的交給領導。大概就這個意思吧,可能例子不夠恰當

2.1.3 從緩衝區讀取數據

int bytes = channel.write(buf); // 從緩衝池中向通道寫入數據 

 byte b = buffer.get(0);   // 從緩衝池中獲取數據

2.1.4 調用buffer.clear()或buffer.compact()清除數據

讀取所有數據後,需要清除緩衝區,以使其可以再次寫入。您可以通過兩種方式執行此操作:通過調用clear()或通過 compact()。該clear()方法清除整個緩衝區。該compact() 方法僅清除您已讀取的數據。任何未讀的數據都將移至緩衝區的開頭,並且現在將在未讀的數據之後將數據寫入緩衝區。

3、selector 選擇器

一個selector對象可以通過調用Selector.open()來創建,這個工廠方法會使用系統默認的selector provider來創建一個新的selector對象。或者我們還可以通過實現抽象類SelectorProvider自定義一個selector provider,然後調用它的openSelector()來創建,

例如:new SelectorProviderImpl().openSelector()

除非調用selector.close(),否則該selector將會一直保持打開狀態。

參考:https://blog.csdn.net/u014730165/article/details/85089085?depth_1-utm_source=distribute.pc_relevant_right.none-task-blog-BlogCommendFromBaidu-7&utm_source=distribute.pc_relevant_right.none-task-blog-BlogCommendFromBaidu-7

通過channel的register方法,將channel註冊到給定的selector中,並返回一個表示註冊關係的SelectionKey 對象。

SelectionKey key = channel.register(selector,
 SelectionKey.OP_ACCEPT|SelectionKey.OP_READ|SelectionKey.OP_WRITE|SelectionKey.OP_CONNECT);
// 爲了確保selector捕捉到信息(也就是有客戶端的行爲才響應)需要調用select方法
// 該方法屬於一個阻塞方法,即沒有內容不會往下執行,表示監聽的端口沒有人連接也沒有人讀寫
selector.select();  

第一個表示選擇器,參數二表示註冊到選擇器之後這個channel對什麼事件感興趣,SelectionKey抽象類裏定義了4中,分別:是:

  • OP_ACCEPT: 接收連接進行事件,表示服務器監聽到了客戶連接
  • OP_READ: 讀就緒事件,表示通道中已經有了可讀的數據
  • OP_WRITE: 寫就緒事件,表示已經可以向通道寫數據了
  • OP_CONNECT:表示客戶與服務器的連接已經建立成功

你可以獲取selector中的所有selector中的selectionKeys的集合:

 selectionKeys = selector.selectedKeys();
 // 獲取遍歷selectionKeys的迭代器,你通過這個key值的遍歷找出這個選擇器中你感興趣的事件進行業務操作。 
 iterator = selectionKeys.iterator();
 // 這個key你捕捉到了並且你處理了,那麼就應該將這個key移除這個集合,避免重複處理相同事件
iterator.remove(selectionKey);

太深的內容我自己理解也不到位,希望將自己能理順的知識做個總結,方便有需要的人蔘考;下面是我開發中用到的NIO例子;

 

完整的NIO 的Demo :

public class ServerCrawImpl implements IServerCraw {
    public ServerCrawImpl() {
    }

    @Override
    public void startup() {
        ServerSocketChannel serverChannel;
        Selector selector;
        try {
            // 開啓管道
            serverChannel = ServerSocketChannel.open();
            // 獲取服務端監聽socket
            ServerSocket serverSocket = serverChannel.socket();
            //綁定端口
            serverSocket.bind(new InetSocketAddress(8603));
            // 設置socket非阻塞
            serverChannel.configureBlocking(false);
            //創建Selector複用器,選擇器
            selector = Selector.open();
            //將多個Channel類型事件註冊到多路複用器   實現Selector管理Channel,其中的事件可以多個
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (Exception e) {
            log.debug(ExceptionUtil.getStackMsg(e));
            return;
        }
        while (true) {
            try {
                //如果selector管理的管道沒有客戶端請求,客戶端的讀,客戶端的寫的交互,則阻塞,返回0
                int eventCount = selector.select();
                if (eventCount == 0) {
                    continue;
                }
            } catch (Exception e) {
                log.debug(ExceptionUtil.getStackMsg(e));
                break;
            }
            // 獲取所有管道相關信息的keys集合
            Set<SelectionKey> keys = selector.selectedKeys();
            // 獲取遍歷keys集合的迭代器
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {
                // 獲取當前的channel對應 key
                SelectionKey key = iterator.next();
                // 移除,防止重複處理
                iterator.remove();
                //處理業務
                try {
                    // key存在並且得到的是連接請求
                    if (key.isValid() && key.isAcceptable()) {
                        // 獲取與客戶端交互的channel
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();
                        //接受客戶端的請求
                        SocketChannel client = server.accept();
                        //客戶端 服務端 全都設置爲 非阻塞
                        client.configureBlocking(false);
                        client.register(key.selector(), SelectionKey.OP_READ | SelectionKey.OP_WRITE, ByteBuffer.allocate(2048));
                    }
                    if (key.isValid() && key.isReadable()) {
                        SocketChannel client = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(3);
                        int res = 0;
                        try {
                            // 從緩存中讀取數據到通道中返回給客戶端,需要判斷一下客戶端收到的消息長度和自己發送的長度是否一致
                            res = client.read(buffer);
                        } catch (Exception e) {
                            client.close();
                            log.debug(ExceptionUtil.getStackMsg(e));
                            break;
                        }
                       //如果read()方法返回-1,說明客戶端關閉了連接,那麼客戶端已經接收到了與自己發送字節數相等的數據,可以安全地關閉
                        if (res == -1) {
                            client.close();
                        } else if (res == 0) {
                            continue;
                        } else if (res > 0) {
                            continue;
                        }
                    }
                    if (key.isValid() && key.isWritable()) {
                            try {
                                // 從緩衝池中獲取數據
                                // 業務操作
                            } catch (Exception e) {
                                log.debug("異常:" + ExceptionUtil.getStackMsg(e));
                            }
                        }
                    }
                } catch (Exception e) {
                    log.debug(ExceptionUtil.getStackMsg(e));
                }
            }
        }
    }
}

 

NIO 的弊端:客戶端連接就緒之後都會有文件描述符的read阻塞監聽,NIO 會重複調用read(),會曾加內核的壓力;

我也僅限會用,太深的東西也沒辦法理解透徹,有不對的歡迎指正。

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