基於Java NIO Selector的多路複用IO模型(同步非阻塞)及IO不可中斷等待狀態線程的改寫

上篇基於Java Socket實現同步非阻塞通信中展示了非阻塞的聊天示例,ServerSocket#accept接收連接後,會創建一個不斷輪訓是否有讀寫數據的線程。不斷輪訓是很消耗CPU資源的,本篇基於Java NIO Selector的多路複用IO模型,將解決這一問題。
方法:Selector可監聽Channel的OP_ACCEPT、OP_CONNECT、OP_READ、OP_WRITE狀態,然後分發給Handler處理器。關聯了Socketd的SocketChannel可以使用configureBlocking方法將自己設置爲非阻塞狀態。
示例代碼使用Selector來監聽SocketChannel中是否有數據可讀、ServerSocketChannel是否接收的新的連接請求。該檢測是阻塞的,當有事件可處理時才通知處理器去處理。因爲是聊天示例,所以需要對每個網絡連接關聯一個阻塞的發消息線程,平常掛起,有輸入時發送消息,但是對每個連接都不斷地輪訓Socket中是否有數據了。

多路複用IO模型代碼
Server.java: IO模型的主要部分。如果Channel在向Selector註冊時附加了Buffer,當buffer中數據沒有處理完的話,下次select()還會選中該SocketChannel。如果事情處理完了,可以取消註冊,即使Selector中沒有註冊Channel,select()方法還是會阻塞的。
Client.java : 客戶端是否阻塞都行,爲了練習NIO,我將客戶端寫成了使用Selector、非阻塞的形式。
ConsoleThread.java 發送消息的線程類。

IO不可中斷等待狀態線程的改寫

結束通信時需要關閉SocketChannel和發消息線程。如果在發消息線程中使用了BufferedReader#readLine()方法,當該方法阻塞時,是不被外部控制的,即使發消息線程調用interrupt方法,該線程也不會被中斷結束。
在這裏插入圖片描述
因此,需要將讀取操作改爲掛起可中斷的IO讀形式:先用不可阻塞的BufferedReader#ready()方法判斷數據是否準備好,若無則使用Thread.sleep(1000)方法將線程掛起1秒,否則讀取數據。詳細寫法見ConsoleThread.java代碼片段。
在這裏插入圖片描述

附I/O複用模型圖
在這裏插入圖片描述

package syncnonblocking;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class Server {
    private static final int BUF_SIZE = 256;
    public static void main(String[] args) {
        try (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
             Selector selector = Selector.open();){
            serverSocketChannel.configureBlocking(false); // non blocking
            serverSocketChannel.socket().bind(new InetSocketAddress(13579));
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            while(selector.select() > 0) {  // blocking method. when a client request for accept, selector.selector() > 1 is true
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while(iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    iterator.remove();
                    switch (key.readyOps()) {
                        case SelectionKey.OP_ACCEPT:  // accept from connection
                            ServerSocketChannel clientChannel = (ServerSocketChannel) key.channel();
                            SocketChannel socketChannel = clientChannel.accept();
                            socketChannel.configureBlocking(false);
                            // 如果註冊時添加了ByteBuffer,只要buf不空,OP_READ會被一直調用
                            ConsoleThread thread = new ConsoleThread(socketChannel);
                            thread.start();
                            socketChannel.register(selector, SelectionKey.OP_READ, thread);
                            break;
                        case SelectionKey.OP_READ:  // read from a channel
                            ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE);
                            SocketChannel channel = (SocketChannel) key.channel();
                            channel.read(buffer);
                            buffer.flip();
                            byte[] tmp = new byte[buffer.limit() - buffer.position()];
                            buffer.get(tmp);
                            // buffer.clear();
                            String msg = new String(tmp);
                            if (msg.equals("exit")) {
                                channel.write(ByteBuffer.wrap("exit".getBytes()));
                                ((ConsoleThread)key.attachment()).interrupt();
                                key.cancel();
                            } else {
                                System.out.println(msg);
                            }
                            break;
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

package syncnonblocking;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class Client {
    private static final int BUF_SIZE = 1024 * 2;
    public static void main(String[] args) {
        ByteBuffer buf = ByteBuffer.allocate(BUF_SIZE);
        try (SocketChannel clientChannel = SocketChannel.open();
             Selector selector = Selector.open();) {
            clientChannel.connect(new InetSocketAddress("127.0.0.1", 13579));
            clientChannel.configureBlocking(false);
            ConsoleThread thread = new ConsoleThread(clientChannel);;
            clientChannel.register(selector, SelectionKey.OP_READ, thread);
            thread.start();
            boolean flag = true;
            // 客戶端沒有必要使用Selector,我是爲了練習而使用的。或者P2P通信時可以這樣寫
            while(flag && selector.select() > 0) { // 不要先select()再flag,因爲select會阻塞flag的判斷
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = keys.iterator();
                while(iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    iterator.remove();
                    if (key.isReadable()) {
                        SocketChannel readChannel = (SocketChannel) key.channel();
                        buf.clear();
                        readChannel.read(buf);
                        buf.flip();
                        byte[] tmp = new byte[buf.limit()];
                        buf.get(tmp);
                        buf.clear();
                        String msg = new String(tmp);
                        if (msg.equals("exit")) {
                            readChannel.write(ByteBuffer.wrap("exit".getBytes()));
                            ((ConsoleThread)key.attachment()).interrupt();
                            key.cancel();
                            flag = false;
                        } else {
                            System.out.println(msg);
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

package syncnonblocking;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class ConsoleThread extends Thread {
    private SocketChannel socketChannel;
    public ConsoleThread(SocketChannel socketChannel) {
        this.socketChannel = socketChannel;
    }

    @Override
    public void run() {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));) {
            while (!isInterrupted()) {
                if (reader.ready()) {
                    String msg = reader.readLine();
                    ByteBuffer buf = ByteBuffer.wrap(msg.getBytes());
                    socketChannel.write(buf);
                } else {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        break;
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        // System.out.println("ConsoleThread is over.");
    }
}


順帶擼一遍Java NIO包含什麼
  • 通道Channel: FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel、AsynchronousFileChannel
  • 緩衝Buffer:ByteBuffer, CharBuffer, ShortBuffer, IntBuffer, FloatBuffer, LongBuffer, DoubleBuffer, MappedByteBUffer。有時需要注意數據的的大小端問題。
  • 選擇器Selector:open、select方法、SelectionKey。
  • Pipe管道
  • Paths path: get, normalize
  • Files: exies, createDirectory, copy, move, move, walkFileTree
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章