JAVA NIO程式設計入門(二)

木木匠發表於2019-02-23

一、回顧

上一篇文章 JAVA NIO程式設計入門(一)我們學習了NIO程式設計的基礎知識,並通過一個小demo實戰幫助瞭解NIO程式設計的channel,buffer等概念。本文會繼續學習JAVA NIO程式設計,並通過一個小示例來幫助理解相關知識,通過本文你將可以學習到

  • buffer的聚集和分散(Scatter/Gather)
  • SocketChannel和ServerSocketChannel的使用
  • 選擇器的使用

二、什麼是聚集和分散(Scatter/Gather)

  • 分散(scatter)從Channel中讀取是指在讀操作時將讀取的資料寫入多個buffer中。因此,Channel將從Channel中讀取的資料“分散(scatter)”到多個Buffer中。
  • 聚集(gather)寫入Channel是指在寫操作時將多個buffer的資料寫入同一個Channel,因此,Channel 將多個Buffer中的資料“聚集(gather)”後傳送到Channel。

分散(Scatter)示意圖

JAVA NIO程式設計入門(二)

從通道填充buffer,必須填充完前一個buffer才會填充後面的buffer,這也意味著不能動態調整每個buffer的接受大小。

聚集(Gather)示意圖

JAVA NIO程式設計入門(二)

聚集和分散是相反的形式,從buffer寫入資料到通道,只會寫入buffer的positon位置到limit位置的內容,也就是意味著可以動態的寫入內容到通道中。

三、選擇器

什麼是選擇器

Selector(選擇器)是Java NIO中能夠檢測多個NIO通道,並能夠知道通道是否為諸如讀寫事件做好準備的元件。這樣,一個單獨的執行緒可以管理多個channel,從而管理多個網路連線,提高效率。

為什麼要用選擇器

使用了選擇器就可以用一個執行緒管理多個channel,如果多個channel由多個執行緒管理,執行緒之前的切換是消耗資源的,而單個執行緒就避免了執行緒之間切換的消耗。

選擇器常用方法

方法名 功能
register(Selector sel, int ops) 向選擇器註冊通道,並且可以選擇註冊指定的事件,目前事件分為4種;1.Connect,2.Accept,3.Read,4.Write,一個通道可以註冊多個事件
select() 阻塞到至少有一個通道在你註冊的事件上就緒了
selectNow() 不會阻塞,不管什麼通道就緒都立刻返回
select(long timeout) 和select()一樣,除了最長會阻塞timeout毫秒(引數)
selectedKeys() 一旦呼叫了select()方法,並且返回值表明有一個或更多個通道就緒了,然後可以通過呼叫selector的selectedKeys()方法,訪問“已選擇鍵集(selected key set)”中的就緒通道
wakeUp() 可以使呼叫select()阻塞的物件返回,不阻塞。
close() 用完Selector後呼叫其close()方法會關閉該Selector,且使註冊到該Selector上的所有SelectionKey例項無效。通道本身並不會關閉

四、實戰

實戰需求說明

編碼客戶端和服務端,服務端可以接受客戶端的請求,並返回一個報文,客戶端接受報文並解析輸出。

服務端程式碼

 try {
            //建立一個服socket並開啟
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            //監聽繫結8090埠
            serverSocketChannel.socket().bind(new InetSocketAddress(8090));
            //設定為非阻塞模式
            serverSocketChannel.configureBlocking(false);
            while(true){
            //獲取請求連線
                SocketChannel socketChannel = serverSocketChannel.accept();
                if (socketChannel!=null){
                    ByteBuffer buf1 = ByteBuffer.allocate(1024);
                    socketChannel.read(buf1);
                    buf1.flip();
                    if(buf1.hasRemaining())
                        System.out.println(">>>服務端收到資料:"+new String(buf1.array()));
                    buf1.clear();
                //構造返回的報文,分為頭部和主體,實際情況可以構造複雜的報文協議,這裡只演示,不做特殊設計。
                    ByteBuffer header = ByteBuffer.allocate(6);
                    header.put("[head]".getBytes());
                    ByteBuffer body   = ByteBuffer.allocate(1024);
                    body.put("i am body!".getBytes());
                    header.flip();
                    body.flip();
                    ByteBuffer[] bufferArray = { header, body };
                    socketChannel.write(bufferArray);

                    socketChannel.close();
                }else{
                    Thread.sleep(1000);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
複製程式碼

服務端selector(選擇器版本)

 try {
            //開啟選擇器
            Selector selector = Selector.open();
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.socket().bind(new InetSocketAddress(8090));
            serverSocketChannel.configureBlocking(false);
            //向通道註冊選擇器,並且註冊接受事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            while (true) {
                //獲取已經準備好的通道數量
                int readyChannels = selector.selectNow();
                //如果沒準備好,重試
                if (readyChannels == 0) continue;
                //獲取準備好的通道中的事件集合
                Set selectedKeys = selector.selectedKeys();
                Iterator keyIterator = selectedKeys.iterator();
                while (keyIterator.hasNext()) {
                    SelectionKey key = (SelectionKey) keyIterator.next();
                    if (key.isAcceptable()) {
                        //在自己註冊的事件中寫業務邏輯,
                         //我這裡註冊的是accept事件,
                         //這部分邏輯和上面非選擇器服務端程式碼一樣。
                        ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) key.channel();
                        SocketChannel socketChannel = serverSocketChannel1.accept();
                        ByteBuffer buf1 = ByteBuffer.allocate(1024);
                        socketChannel.read(buf1);
                        buf1.flip();
                        if (buf1.hasRemaining())
                            System.out.println(">>>服務端收到資料:" + new String(buf1.array()));
                        buf1.clear();

                        ByteBuffer header = ByteBuffer.allocate(6);
                        header.put("[head]".getBytes());
                        ByteBuffer body = ByteBuffer.allocate(1024);
                        body.put("i am body!".getBytes());
                        header.flip();
                        body.flip();
                        ByteBuffer[] bufferArray = {header, body};
                        socketChannel.write(bufferArray);

                        socketChannel.close();
                    } else if (key.isConnectable()) {
                    } else if (key.isReadable()) {
                    } else if (key.isWritable()) {

                    }
                    //注意每次迭代末尾的keyIterator.remove()呼叫。
                    //Selector不會自己從已選擇鍵集中移除SelectionKey例項。必須在處理完通道時自己移除。
                    //下次該通道變成就緒時,Selector會再次將其放入已選擇鍵集中
                    keyIterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
複製程式碼

客戶端程式碼

 try {
            //開啟socket連線,連線本地8090埠,也就是服務端
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.connect(new InetSocketAddress("127.0.0.1", 8090));
            //請求服務端,傳送請求
            ByteBuffer buf1 = ByteBuffer.allocate(1024);
            buf1.put("來著客戶端的請求".getBytes());
            buf1.flip();
            if (buf1.hasRemaining())
                socketChannel.write(buf1);
            buf1.clear();
            //接受服務端的返回,構造接受緩衝區,我們定義頭6個位元組為頭部,後續其他位元組為主體內容。
            ByteBuffer header = ByteBuffer.allocate(6);
            ByteBuffer body   = ByteBuffer.allocate(1024);
            ByteBuffer[] bufferArray = { header, body };

            socketChannel.read(bufferArray);
            header.flip();
            body.flip();
            if (header.hasRemaining())
                System.out.println(">>>客戶端接收頭部資料:" + new String(header.array()));
            if (body.hasRemaining())
                System.out.println(">>>客戶端接收body資料:" + new String(body.array()));
            header.clear();
            body.clear();


            socketChannel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
複製程式碼

執行結果

服務端:

JAVA NIO程式設計入門(二)

客戶端:

JAVA NIO程式設計入門(二)

這裡給出了服務端程式碼的兩個版本,一個是非選擇器的版本,一個是選擇器的版本。檢視最後執行結果,發現客戶端根據雙方約定的協議格式,正確解析到了頭部和body的內容,其實這也是聚集和分散最主要的作用和應用場景,在網路互動中,進行協議報文格式的定義和實現。後續學完NIO程式設計入門後我們最後進行總結性的實戰,編寫一個RPC的demo框架,實現分散式系統的遠端呼叫,有興趣的同學可以關注筆者和後續的文章。

參考

JAVA NIO

推薦閱讀

Java鎖之ReentrantLock(一)

Java鎖之ReentrantLock(二)

Java鎖之ReentrantReadWriteLock

JAVA NIO程式設計入門(一)

相關文章