Java網絡編程由淺入深三 一文了解非阻塞通信

本文詳細介紹組成非阻塞通信的幾大類:BufferChannelSelectorSelectionKey

非阻塞通信的流程

  1. ServerSocketChannel通過open方法獲取ServerSocketChannel,通過ServerSocketChannel設置爲非阻塞模式,再通過ServerSocketChannel獲取socket,綁定服務進程監聽端口。服務啓動成功。
  2. 然後就是非阻塞通信的精髓了,Selector通過靜態的open()方法獲取到Selector,然後ServerSocketChannel註冊Selection.OP_ACCEPT事件到Selector上。
  3. Selector就會監控事件發生,Selector通過select()監控已發生的SelectionKey對象的數目,通過selectKeys()方法返回對應的selectionKey對象集合。遍歷該集合得到相應的selectionKey對象,通過該對象的channel()方法獲取關聯的ServerSocketChannel對象, 通過selector()方法就可以獲取關聯的Selector對象,並移除相應的selectionKey。
  4. 通過上面獲取的ServerSocketChannel執行accept()方法獲取SocketChannel,再通過SocketChannel設置爲非阻塞模式,在將SocketChannel註冊到上面創建的Selector上,註冊SelectionKey.OP_READ |SelectionKey.OP_WRITE 事件。
  5. Selector將在監控對應上面綁定的事件,監控到對應的事件的話執行讀和寫的操作。

示例代碼:

上面描述了服務端非阻塞方式通信的一個流程,下面通過具體代碼實現:

/**
 * 非阻塞模式
 * 
 */
public class EchoServer2 {
    private Selector selector = null;
    private ServerSocketChannel serverSocketChannel = null;
    private int port = 8001;
    private Charset charset = Charset.forName("UTF-8");

    public EchoServer2() throws IOException {
        selector = Selector.open();
        serverSocketChannel = ServerSocketChannel.open();
        //服務器重啓的時候,重用端口
        serverSocketChannel.socket().setReuseAddress(true);
        //設置非阻塞模式
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.socket().bind(new InetSocketAddress(port));
        System.out.println("服務器啓動成功");
    }

    /**
     * 服務方法
     */
    public void service() throws IOException {
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (selector.select() > 0) {
            Set readyKes = selector.selectedKeys();
            Iterator it = readyKes.iterator();
            while (it.hasNext()) {
                SelectionKey key = null;
                try {
                    key = (SelectionKey) it.next();
                    it.remove();
                    if (key.isAcceptable()) {
                        System.out.println("連接事件");
                        //連接事件
                        ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                        SocketChannel socketChannel = ssc.accept();
                        System.out.println("接收到客戶連接,來自:" + socketChannel.socket().getInetAddress() +
                                " : " + socketChannel.socket().getPort());
                        socketChannel.configureBlocking(false);
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        socketChannel.register(selector, SelectionKey.OP_READ |
                                SelectionKey.OP_WRITE, buffer);
                    } else if (key.isReadable()) {
                        //接收數據
                        receive(key);
                    } else if (key.isWritable()) {
                        //發送數據
                        send(key);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                    try {
                        if (key != null) {
                            key.cancel();
                            key.channel().close();
                        }
                    }catch (IOException ex){
                        ex.printStackTrace();
                    }
                }
            }
        }
    }

    private void send(SelectionKey key) throws IOException {
        ByteBuffer buffer = (ByteBuffer) key.attachment();

        SocketChannel channel = (SocketChannel) key.channel();
        buffer.flip(); //把極限設置爲位置,把位置設置爲0
        String data = decode(buffer);
        if (data.indexOf("\r\n") == -1) {
            return;
        }
        String outputData = data.substring(0, data.indexOf("\n") + 1);
        System.out.println("請求數據:" + outputData);

        ByteBuffer outputBuffer = encode("echo:" + outputData);
        while (outputBuffer.hasRemaining()) {
            channel.write(outputBuffer);
        }
        ByteBuffer temp = encode(outputData);
        buffer.position(temp.limit());
        buffer.compact();

        if (outputData.equals("bye\r\n")) {
            key.cancel();
            channel.close();
            System.out.println("關閉與客戶的連接");
        }
    }

    private String decode(ByteBuffer buffer) {
        CharBuffer charBuffer = charset.decode(buffer);
        return charBuffer.toString();
    }

    private ByteBuffer encode(String s) {
        return charset.encode(s);
    }


    private void receive(SelectionKey key) throws IOException {
        ByteBuffer buffer = (ByteBuffer) key.attachment();

        SocketChannel socketChannel = (SocketChannel) key.channel();
        ByteBuffer readBuff = ByteBuffer.allocate(32);
        socketChannel.read(readBuff);
        readBuff.flip();

        buffer.limit(buffer.capacity());
        buffer.put(readBuff);
    }

    public static void main(String[] args) throws IOException {
        new EchoServer2().service();
    }
}

/**
 * 創建非阻塞客戶端
 * 
 */
public class EchoClient2 {

    private SocketChannel socketChannel;
    private int port = 8001;
    private Selector selector;
    private ByteBuffer sendBuffer = ByteBuffer.allocate(1024);
    private ByteBuffer receiveBuffer = ByteBuffer.allocate(1024);
    private Charset charset = Charset.forName("UTF-8");

    public EchoClient2() throws IOException {
        socketChannel = SocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress(InetAddress.getLocalHost(), port);
        socketChannel.connect(inetSocketAddress);//
        socketChannel.configureBlocking(false);//設置爲非阻塞模式
        System.out.println("與服務器連接成功");
        selector = Selector.open();
    }

    public static void main(String[] args) throws IOException {
        final EchoClient2 client = new EchoClient2();
        Thread receiver = new Thread(new Runnable() {
            @Override
            public void run() {
                client.receiveFromUser();
            }
        });
        receiver.start();
        client.talk();
    }

    private void receiveFromUser() {
        try {
            System.out.println("請輸入數據:");
            BufferedReader localReader = new BufferedReader(new InputStreamReader(System.in));
            String msg = null;
            while ((msg = localReader.readLine()) != null) {
                System.out.println("用戶輸入的數據:" + msg);
                synchronized (sendBuffer) {
                    sendBuffer.put(encode(msg + "\r\n"));
                }
                if (msg.equalsIgnoreCase("bye")) {
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private ByteBuffer encode(String s) {
        return charset.encode(s);
    }

    private void talk() throws IOException {
        socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
        while (selector.select() > 0) {
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> it = keys.iterator();
            while (it.hasNext()) {
                SelectionKey key = null;
                try {
                    key = it.next();
                    it.remove();
                    if (key.isReadable()) {
                        //System.out.println("讀事件");
                        //讀事件
                        receive(key);
                    }
                    if (key.isWritable()) {
                       // System.out.println("寫事件");
                        //寫事件
                        send(key);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                    if (key != null) {
                        key.cancel();
                        key.channel().close();
                    }
                }
            }
        }
    }

    private void send(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        synchronized (sendBuffer) {
            sendBuffer.flip();//把極限設爲位置,把位置設爲零
            channel.write(sendBuffer);
            sendBuffer.compact();//刪除已經發送的數據。
        }
    }

    private void receive(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        channel.read(receiveBuffer);
        receiveBuffer.flip();//將limit的值設置爲position的值,將position的值設置爲0
        String receiveData = decode(receiveBuffer);
        if (receiveData.indexOf("\n") == -1) {
            return;
        }
        String outputData = receiveData.substring(0, receiveData.indexOf("\n") + 1);
        System.out.println("響應數據:" + outputData);

        if (outputData.equalsIgnoreCase("echo:bye\r\n")) {
            key.cancel();
            socketChannel.close();
            ;
            System.out.println("關閉與服務器的連接");
            selector.close();
            System.exit(0);
        }

        ByteBuffer temp = encode(outputData);
        receiveBuffer.position(temp.limit());
        receiveBuffer.compact();//刪除已經打印的數據
    }

    private String decode(ByteBuffer receiveBuffer) {
        CharBuffer buffer = charset.decode(receiveBuffer);
        return buffer.toString();
    }
}

實現非阻塞通信的方式

  • 緩衝區
  • 通道
  • Selector

    緩衝區

作用:減少物理讀寫次數,減少內存創建和銷燬次數。 緩衝區的屬性:capacity(最大容量)、limit(實際容量)、position(當前位置)。PS:其他地方是翻譯成capacity(容量)、limit(極限)、position位置),我個人覺得翻譯成上面的更好理解,爲啥通過下面的方法解析和圖解就可明白。當然最好通過英文表達這樣最清楚。
三個屬性的關係爲:capacity≥limit≥position≥0

圖解關係如下:
這裏寫圖片描述
緩衝區類結構:
java.nio.ByteBuffer類是一個抽象類,不能被實例化。但是提供了8個具體的實現類,其中最基本的的緩衝區是ByteBuffer,它存放的數據單元是字節。
這裏寫圖片描述

常用方法:

clear():把limit設置爲capacity,再把位置設爲0
flip():把limit設置爲position,再把位置設置爲0。
rewind():不改變limit,把位置設爲0。
allocate():創建一個緩衝中,方法參數指定緩衝區大小
compact():將緩衝區的當前位置和界限之間的字節(如果有)複製到緩衝區的開始處。

測試上述方法:
測試clear()方法

    @Test
    public void testClear() {
        //創建一個10chars大小的緩衝區,默認情況下limit和capacity是相等的
        CharBuffer buffer = CharBuffer.allocate(10);
        System.out.println("創建默認情況");
        printBufferInfo(buffer);
        buffer.limit(8);//修改limit的值
        System.out.println("修改limit後");
        printBufferInfo(buffer);
        // clear():把limit設置爲capacity,再把位置設爲0
        buffer.clear();
        System.out.println("執行clear()方法後");
        printBufferInfo(buffer);
    }

執行結果如下:
這裏寫圖片描述

測試flip()方法:

    @Test
    public void testFlip() {
        CharBuffer buffer = CharBuffer.allocate(10);
        System.out.println("創建默認情況");
        printBufferInfo(buffer);
        //put的方法會修改position的值
        buffer.put('H');
        buffer.put('E');
        buffer.put('L');
        buffer.put('L');
        buffer.put('O');
        System.out.println("調用put方法後:");
        printBufferInfo(buffer);
        //flip():把limit設置爲position,再把位置設置爲0。
        buffer.flip();
        System.out.println("調用flip方法後:");
        printBufferInfo(buffer);
    }

執行結果如下:
這裏寫圖片描述

測試rewind()方法

        @Test
    public void testRewind() {
        CharBuffer buffer = CharBuffer.allocate(10);
        System.out.println("創建默認情況");
        printBufferInfo(buffer);
        //put的方法會修改position的值
        buffer.put('H');
        buffer.put('E');
        buffer.put('L');
        buffer.put('L');
        buffer.put('O');
        buffer.limit(8);
        System.out.println("調用put、limit方法後:");
        printBufferInfo(buffer);
        //rewind():不改變limit,把位置設爲0。
        buffer.rewind();
        System.out.println("調用rewind方法後:");
        printBufferInfo(buffer);
    }

執行結果如下:
這裏寫圖片描述

測試compact()方法

    @Test
    public void testCompact(){
        CharBuffer buffer = CharBuffer.allocate(10);
        System.out.println("創建默認情況");
        printBufferInfo(buffer);
        //put的方法會修改position的值
        buffer.put('H');
        buffer.put('E');
        buffer.put('L');
        buffer.put('L');
        buffer.put('O');
        buffer.limit(8);//修改limit的值
        System.out.println("調用put和limit方法後:");
        printBufferInfo(buffer);
        System.out.println("調用compact方法後:");
        //將緩衝區的當前位置和界限之間的字節(如果有)複製到緩衝區的開始處。
        buffer.compact();
        printBufferInfo(buffer);
    }

這裏寫圖片描述

這是JDK中介紹該方法的作用:

將緩衝區的當前位置和界限之間的字節(如果有)複製到緩衝區的開始處。即將索引 p = position() 處的字節複製到索引 0 處,將索引 p + 1 處的字節複製到索引 1 處,依此類推,直到將索引 limit() - 1 處的字節複製到索引 n = limit() - 1 - p 處。然後將緩衝區的位置設置爲 n+1,並將其界限設置爲其容量。如果已定義了標記,則丟棄它。

官方表示的太難理解了:

將緩衝區的當前位置和界限之間的字節(如果有)複製到緩衝區的開始處。並將limit(實際容量)設置爲 capacity(最大容量)。執行compact()方法前,limit的值是:8,position的值是:5。按照上面描述的執行完compact()後,position的值計算方式是:n+1;n=limit-1-p;所有n=8-1-5=2,最後position的值爲:2+1=3。和程序運行的結果一致。
可以在這種情況:從緩衝區寫入數據之後調用此方法,以防寫入不完整。

buf.clear();          // Prepare buffer for use
  while (in.read(buf) >= 0 || buf.position != 0) {
     buf.flip();
     out.write(buf);
     buf.compact();    // In case of partial write
 }

如果out.write()方法沒有將緩存中的數據讀取完,這個時候的position位置指向的是剩餘數據的位置。達到防止寫入不完整。

通道

作用: 連接緩衝區與數據源或數據目的地。

常用類:

Channel
接口有下面兩個子接口ReadableByteChannel和WritableByteChannel和一個抽象實現類SelectableChannel。
在ReadableByteChannel接口中申明瞭read(ByteBuffer
dst)方法。在WritableByteChannel接口中申明瞭write(ByteBuffer[]
srcs):方法。SelectableChannel抽象類中主要方法,configureBlocking(boolean
block)、register();方法。 ByteChannel
接口繼承了ReadableChannel和WritableChannel。所以ByteChannel具有讀和寫的功能。

ServerSocketChannel繼承了SelectableChannel類抽象類,所以SocketChannel具有設置是否是阻塞模式、向selector註冊事件功能。

SocketChannel也繼承了SelectableChannel類還實現ByteChannel接口,所以SocketChannel具有設置是否是阻塞模式、向selector註冊事件、從緩衝區讀寫數據的功能。

通過類圖展現:
這裏寫圖片描述

Selector類:

作用:只要ServerSocketChannel及SocketChannel向Selector註冊了特定的事件,Selector就會監聽這些事件的發生。

流程:
Selector通過靜態的open()方法創建一個Selector對象,SelectableChannel類向Selector註冊了特定的事件。Selector就會監控這些事件發生,Selector通過select()監控已發生的SelectionKey對象的數目,通過selectKeys()方法返回對應的selectionKey對象集合。遍歷該集合得到相應的selectionKey對象,通過該對象的channel()方法獲取關聯的SelectableChannel對象,
通過selector()方法就可以獲取關聯的Selector對象。

Note:
當Selector的select()方法還有一個重載方式:select(long timeout)。並且該方法採用阻塞的工作方式,如果相關事件的selectionKey對象的數目一個也沒有,就進入阻塞狀態。知道出現以下情況之一,才從select()方法中返回。

  • 至少有一個SelectionKey的相關事件已經發生。
  • 其他線程調用了Selector的wakeup()方法,導致執行select()方法的線程立即返回。
  • 當前執行的select()方法的線程被中斷。
  • 超出了等待時間。僅限調用select(long timeout)方法時出現。如果沒有設置超時時間,則永遠不會超時。

Selector類有兩個非常重要的方法: 靜態方法open(),這是Selector的靜態工廠方法,創建一個Selector對象。
selectedKeys()方法返回被Selector捕獲的SelectionKey的集合。

SelectionKey類

作用:
ServerSocketChannel或SocketChannel通過register()方法向Selector註冊事件時,register()方法會創建一個SelectionKey對象,該對象是用來跟蹤註冊事件的句柄。在SelectionKey對象的有效期間,Selector會一直監控與SelectionKey對象相關的事件,如果事件發生,就會把SelectionKey對象添加到Selected-keys集合中。

SelectionKey中定義的事件: 定義了4種事件:
1、SelectionKey.OP_ACCEPT:接收連接就緒事件,表示服務器監聽到了客戶連接,服務器可以接收這個連接了。常量值爲16.
2、SelectionKey.OP_CONNECT:連接就緒事件,表示客戶與服務器的連接已經建立成功。常量值爲8.
3、SelectionKey.OP_READ:讀就緒事件,表示通道中已經有了可讀數據可以執行讀操作。常量值爲1.
4、SelectionKey.OP_WRITE:寫就緒事件,表示已經可以向通道寫數據了。常量值爲4.

常用方法:
channel()方法:返回與它關聯的SelectedChannel(包括ServerSocketChannel和SocketChannel)。
selector()方法:返回與它關聯的Selector對象。
它們之間的關係如下:
這裏寫圖片描述


歡迎關注微信公衆號 在路上的coder 每天分享優秀的Java技術文章,還有學習視頻分享!
掃描二維碼關注:這裏寫圖片描述

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