NIO的介紹(與BIO的區別及使用模型)

阻塞與同步


1、阻塞和非阻塞
阻塞和非阻塞是進程在訪問數據的時候,數據是否準備就緒的一種處理方式,當數據沒有準備的時候一直等待,直到有數據返回,否則一直等待在那裏。

非阻塞:當我們的進程訪問我們的數據緩衝區的時候,如果數據沒有準備好則直接返回,不會等待。如果數據已經準備好,也直接返回。

如果用一個燒水例子解釋這個問題:

阻塞:就相當於水壺放在放到火上,立等水開才能知道水是否開了。
非堵塞:就相當於把水壺放在火上,隨時去看,都知道水開還是沒開而不是要等到水開了,才能告知結論。如果採用這種方案就是,可以看電視時不時去看,水是否開了。(同步阻塞)

NIO同步非堵塞IO:
當此此線程中沒有數據返回的時候,可以先處理其他線程的任務,不用卡在此線程一直等待數據返回。

2、同步(Synchronization)和異步(Async)的方式

同步和異步都是基於應用程序私操作系統處理IO事件所採用的方式,比如同步:是應用程序要直接參與IO讀寫的操作。異步:所有的IO讀寫交給搡作系統去處理,應用程序只需要等待通知。

如果用一個燒水例子解釋這個問題:

同步:就相當於水壺放在放到火上,水開的時候並不會通知水開了,而需要一個線程去看水是否開了。
異步:當水開了,會通知我們水已經開了。

異步非阻塞IO:
獲取數據的時候,無論是否有數據都立即返回,並且在內核在緩衝區裏填入了數據之後自動調用內存拷貝。

BIO代碼演示


BIO客戶端:

public class BIOClient {
	private static Charset charset = Charset.forName("UTF-8");

	public static void main(String[] args) throws Exception {
	//開啓一個8080端口的線程
		Socket s = new Socket("localhost", 8080);
		OutputStream out = s.getOutputStream();

		Scanner scanner = new Scanner(System.in);
		System.out.println("請輸入:");
		String msg = scanner.nextLine();
		out.write(msg.getBytes(charset));
		scanner.close();
		s.close();
	}

}

BIO客戶端:

bio由於Socket request = serverSocket.accept(); 獲取連接和
InputStream inputStream = request.getInputStream(); 從連接獲取流是阻塞的故,當有一個請求,只能創建一個線程去處理,線程一直阻塞利用率並不高。創建的線程過多導致服務器的新能下降明顯。

public class BIOServer {

    private static ExecutorService threadPool = new ThreadPoolExecutor(0, Integer.MAX_VALUE,
            60L, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>());

    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("服務器啓動成功");
        while (!serverSocket.isClosed()) {
            //獲取socket 連接數阻塞的
            Socket request = serverSocket.accept();
            System.out.println("收到新連接 : " + request.toString());
            threadPool.execute(() -> {
                try {
                    // 接收數據、打印(從連接中獲取數據同樣是阻塞的)
                    InputStream inputStream = request.getInputStream();
                    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
                    String msg;
                    while ((msg = reader.readLine()) != null) {
                        if (msg.length() == 0) {
                            break;
                        }
                        System.out.println(msg);
                    }

                    System.out.println("收到數據,來自:"+ request.toString());
                    // 響應結果 200
                    OutputStream outputStream = request.getOutputStream();
                    outputStream.write("HTTP/1.1 200 OK\r\n".getBytes());
                    outputStream.write("Content-Length: 11\r\n\r\n".getBytes());
                    outputStream.write("Hello World".getBytes());
                    outputStream.flush();
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        request.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        serverSocket.close();
    }
}

產生結果:
在這裏插入圖片描述

NIO代碼演示


1、緩衝區Buffer

緩衝區實際上是一個容器對象,可以看做一個功能增強的數組(有位置、固定容量、標記),在NIO庫中,所有數據都是用緩衝區處理的。在讀取數據時,它是直接讀到緩衝區中的; 在寫入數據時,它也是寫入到緩衝區中的;而在面向流I/O系統中,所有數據都是直接寫入或者直接將數據讀取到Stream對象中。

容量(capacity):
緩衝區能夠容納的數據元素的最大數量。這一容量在緩衝區創建時被設定,並且永遠不能被改變
上界(limit):
緩衝區的第一個不能被讀或寫的元素。或者說,緩衝區中現存元素的計數
位置(position):
下一個要被讀或寫的元素的索引。位置會自動由相應的 get( )和 put( )函數更新
標記(mark):
下一個要被讀或寫的元素的索引。位置會自動由相應的 get( )和 put( )函數更新一個備忘位置。

2、通道Channel

通道是一個對象,通過它可以讀取和寫入數據,當然了所有數據都通過Buffer對象來處理。我們永遠不會將字節直接寫入通道中,相反是將數據寫入包含一個或者多個字節的緩衝區。同樣不會直接從通道中讀取字節,而是將數據從通道讀入緩衝區,再從緩衝區獲取這個字節。

在這裏插入圖片描述

3、選擇器

SelectableChannel 可被註冊到 Selector 對象上,同時可以指定對那個選擇器而言,哪種操作是感興趣的,當發生的事件和該事件所發生的具體的SelectableChannel,以獲得客戶端發送過來的數據。一個通道可以被註冊到多個選擇器上,但對每個選擇器而言,只能被註冊一次,通道在被註冊到一個選擇器上之前,必須先設置爲非阻塞模式,通過調用通道的configureBlocking(false)方法即可。這意味着不能將FileChannel與Selector一起使用,因爲FileChannel不能切換到非阻塞模式,而套接字通道都可以。

在nio的代碼寫的過程中有很多需要注意的地方:
https://www.cnblogs.com/pingh/p/3224990.html

下面設置關於通過nio來演示客戶端與服務端的傳輸

客戶端

public class NIOClient {

    public static void main(String[] args) throws Exception {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
        while (!socketChannel.finishConnect()) {
            // 沒連接上,則一直等待
            Thread.yield();
        }
        Scanner scanner = new Scanner(System.in);
        System.out.println("請輸入:");
        // 發送內容
        String msg = scanner.nextLine();
        ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
        while (buffer.hasRemaining()) {
            socketChannel.write(buffer);
        }
        // 讀取響應
        System.out.println("收到服務端響應:");
        ByteBuffer requestBuffer = ByteBuffer.allocate(1024);

        while (socketChannel.isOpen() && socketChannel.read(requestBuffer) != -1) {
            // 長連接情況下,需要手動判斷數據有沒有讀取結束 (此處做一個簡單的判斷: 超過0字節就認爲請求結束了)
            if (requestBuffer.position() > 0){
                break;
            }
        }
        //nio 切換模式
        requestBuffer.flip();
        byte[] content = new byte[requestBuffer.limit()];
        requestBuffer.get(content);
        System.out.println(new String(content));
        scanner.close();
        socketChannel.close();
    }

}

服務端:
此處一個selector監聽所有事件,演示

public class NIOServerV2 {

    public static void main(String[] args) throws Exception {
        // 1. 創建網絡服務端ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 設置爲非阻塞模式
        serverSocketChannel.configureBlocking(false);

        // 2. 構建一個Selector選擇器,並且將channel註冊上去
        Selector selector = Selector.open();
        // 將serverSocketChannel註冊到selector
        SelectionKey selectionKey = serverSocketChannel.register(selector, 0, serverSocketChannel);
        // 對serverSocketChannel上面的accept事件感興趣(serverSocketChannel只能支持accept操作)
        selectionKey.interestOps(SelectionKey.OP_ACCEPT);
        // 3. 綁定端口
        serverSocketChannel.socket().bind(new InetSocketAddress(8080));

        System.out.println("啓動成功");

        while (true) {
            // 用下面輪詢事件的方式.select方法有阻塞效果,直到有事件通知纔會有返回
            selector.select();
            // 獲取事件
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            // 遍歷查詢結果
            Iterator<SelectionKey> iter = selectionKeys.iterator();
            while (iter.hasNext()) {
                // 被封裝的查詢結果
                SelectionKey key = iter.next();
                iter.remove();
                // 關注 Read 和 Accept兩個事件
                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key.attachment();
                    // 將拿到的客戶端連接通道,註冊到selector上面
                    SocketChannel clientSocketChannel = server.accept();
                    clientSocketChannel.configureBlocking(false);
                    clientSocketChannel.register(selector, SelectionKey.OP_READ, clientSocketChannel);
                    System.out.println("收到新連接 : " + clientSocketChannel.getRemoteAddress());
                }

                if (key.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel) key.attachment();
                    try {
                        ByteBuffer requestBuffer = ByteBuffer.allocate(1024);
                        while (socketChannel.isOpen() && socketChannel.read(requestBuffer) != -1) {
                            // 長連接情況下,需要手動判斷數據有沒有讀取結束 (此處做一個簡單的判斷: 超過0字節就認爲請求結束了)
                            if (requestBuffer.position() > 0) break;
                        }
                        // 如果沒數據了, 則不繼續後面的處理
                        if(requestBuffer.position() == 0) continue;
                        //切換模式
                        requestBuffer.flip();
                        byte[] content = new byte[requestBuffer.limit()];
                        requestBuffer.get(content);
                        System.out.println(new String(content));
                        System.out.println("收到數據,來自:" + socketChannel.getRemoteAddress());

                        // 響應結果 200
                        String response = "HTTP/1.1 200 OK\r\n" +
                                "Content-Length: 11\r\n\r\n" +
                                "Hello World";
                        ByteBuffer buffer = ByteBuffer.wrap(response.getBytes());
                        while (buffer.hasRemaining()) {
                            socketChannel.write(buffer);
                        }
                    } catch (IOException e) {
                        // e.printStackTrace();
                        // 取消事件訂閱
                        key.cancel(); 
                    }
                }
            }
            selector.selectNow();
        }
     
    }
}

運行結果:
在這裏插入圖片描述

NIO多路複用模型演示


在這裏插入圖片描述

一個selector監聽所有事件,個線程處理所有請求事件,這裏通過多路複用來展示這個過程,這也是netty採用的方式,只是則是netty的簡化版本。

/**
 * NIO selector 多路複用reactor線程模型
 */
public class NIOServerV3 {
    abstract class EventLoop extends Thread {

        Selector selector;
        LinkedBlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<>();

        /**
         * Selector監聽到有事件後,調用這個方法
         */
        public abstract void handler(SelectableChannel channel) throws Exception;

        private EventLoop() throws IOException {
            selector = Selector.open();
        }

        volatile boolean running = false;

        @Override
        public void run() {
            // 輪詢Selector事件
            while (running) {
                try {
                    // 執行隊列中的任務
                    Runnable task;
                    while ((task = taskQueue.poll()) != null) {
                        task.run();
                    }
                    selector.select(1000);

                    // 獲取查詢結果
                    Set<SelectionKey> selected = selector.selectedKeys();
                    // 遍歷查詢結果
                    Iterator<SelectionKey> iter = selected.iterator();
                    while (iter.hasNext()) {
                        // 被封裝的查詢結果
                        SelectionKey key = iter.next();
                        iter.remove();
                        int readyOps = key.readyOps();
                        // 關注 Read 和 Accept兩個事件
                        if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
                            try {
                                SelectableChannel channel = (SelectableChannel) key.attachment();
                                channel.configureBlocking(false);
                                handler(channel);
                                if (!channel.isOpen()) {
                                    key.cancel(); // 如果關閉了,就取消這個KEY的訂閱
                                }
                            } catch (Exception ex) {
                                key.cancel(); // 如果有異常,就取消這個KEY的訂閱
                            }
                        }
                    }
                    selector.selectNow();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }


        private SelectionKey register(SelectableChannel channel) throws Exception {
            FutureTask<SelectionKey> futureTask = new FutureTask<>(() -> channel.register(selector, 0, channel));
            taskQueue.add(futureTask);
            return futureTask.get();
        }

        private void doStart() {
            if (!running) {
                running = true;
                start();
            }
        }
    }

    private ServerSocketChannel serverSocketChannel;
    // 1、創建多個線程 - acceptor
    private EventLoop[] bossThreads = new EventLoop[1];
    // 2、創建多個線程 - io
    private EventLoop[] workThreads = new EventLoop[1];

    /**
     * 初始化線程組
     */
    private void newGroup() throws IOException {
        // 創建Boss線程, 只負責處理serverSocketChannel
        for (int i = 0; i < bossThreads.length; i++) {
            bossThreads[i] = new EventLoop() {
                AtomicInteger incr = new AtomicInteger(0);

                @Override
                public void handler(SelectableChannel channel) throws Exception {
                    // 只做請求分發,不做具體的數據讀取
                    ServerSocketChannel ch = (ServerSocketChannel) channel;
                    SocketChannel socketChannel = ch.accept();
                    socketChannel.configureBlocking(false);
                    // 收到連接建立的通知之後,分發給work線程繼續去讀取數據
                    int index = incr.getAndIncrement() % workThreads.length;
                    EventLoop workEventLoop = workThreads[index];
                    workEventLoop.doStart();
                    SelectionKey selectionKey = workEventLoop.register(socketChannel);
                    selectionKey.interestOps(SelectionKey.OP_READ);
                    System.out.println("收到新連接 : " + socketChannel.getRemoteAddress());
                }
            };
        }

        // 創建IO線程,負責處理客戶端連接以後socketChannel的IO讀寫
        for (int i = 0; i < workThreads.length; i++) {
            workThreads[i] = new EventLoop() {
                @Override
                public void handler(SelectableChannel channel) throws IOException {
                    // work線程只負責處理IO處理,不處理accept事件
                    SocketChannel ch = (SocketChannel) channel;
                    ByteBuffer requestBuffer = ByteBuffer.allocate(1024);
                    while (ch.isOpen() && ch.read(requestBuffer) != -1) {
                        // 長連接情況下,需要手動判斷數據有沒有讀取結束 (此處做一個簡單的判斷: 超過0字節就認爲請求結束了)
                        if (requestBuffer.position() > 0){
                            break;
                        }
                    }
                    // 如果沒數據了, 則不繼續後面的處理
                    if (requestBuffer.position() == 0){
                        return;
                    }
                    requestBuffer.flip();
                    byte[] content = new byte[requestBuffer.limit()];
                    requestBuffer.get(content);
                    System.out.println(new String(content));
                    System.out.println("收到數據,來自:" + ch.getRemoteAddress());

                    // 響應結果 200
                    String response = "HTTP/1.1 200 OK\r\n" +
                            "Content-Length: 11\r\n\r\n" +
                            "Hello World";
                    ByteBuffer buffer = ByteBuffer.wrap(response.getBytes());
                    while (buffer.hasRemaining()) {
                        ch.write(buffer);
                    }
                }
            };
        }
    }

    /**
     * 初始化channel,並且綁定一個eventLoop線程
     *
     * @throws IOException IO異常
     */
    private void initAndRegister() throws Exception {
        // 1、 創建ServerSocketChannel
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        // 2、 將serverSocketChannel註冊到selector
        int index = new Random().nextInt(bossThreads.length);
        bossThreads[index].doStart();
        SelectionKey selectionKey = bossThreads[index].register(serverSocketChannel);
        selectionKey.interestOps(SelectionKey.OP_ACCEPT);
    }

    /**
     * 綁定端口
     *
     * @throws IOException IO異常
     */
    private void bind() throws IOException {
        //  1、 正式綁定端口,對外服務
        serverSocketChannel.bind(new InetSocketAddress(8080));
        System.out.println("啓動完成,端口8080");
    }

    public static void main(String[] args) throws Exception {
        NIOServerV3 nioServerV3 = new NIOServerV3();
        nioServerV3.newGroup();
        nioServerV3.initAndRegister();
        nioServerV3.bind();
        int read = System.in.read();
        System.out.println(read);
    }

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