高併發網絡編程之NIO非阻塞網絡編程

NIO非阻塞網絡編程

始於Java1.4,提供了新的JAVA IO操作非阻塞API。用意是替代Java IO和Java Networking相關的API。

三個核心組件:

  • Buffer緩衝區
  • Channel通道
  • Selector選擇器

Buffer緩衝區

緩衝區本質上是一個可以寫入數據的內存塊(類似數組),然後可以再次讀取。此內存塊包含在NIO Buffer對象中,該對象提供了一組方法,可以更輕鬆地使用內存塊。

相比較直接對數組的操作,Buffer API更加容易操作和管理。

使用Buffer進行數據寫入和讀取,需要進行如下四個步驟:

  1. 將數據寫入緩衝區
  2. 調用buffer.flip(),轉換爲讀取模式
  3. 緩衝區讀取數據
  4. 調用buffer.clear()或buffer.compact()清除緩衝區

Buffer工作原理:

Buffer三個重要屬性:

  • capacity容量:作爲一個內存塊,Buffer具有一定的固定大小,也稱爲容量。
  • position位置:寫入模式時代表寫數據的位置。讀取模式時代表讀取數據的位置。
  • limit限制:寫入模式,限制等於buffer的容量。讀取模式下,limit等於寫入的數據量。

在這裏插入圖片描述

public class BufferDemo {
    public static void main(String[] args) {
        // 構建一個byte字節緩衝區,容量是4
        ByteBuffer byteBuffer = ByteBuffer.allocate(4);
        // 默認寫入模式,查看三個重要的指標
        System.out.println(String.format("初始化:capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(),
                byteBuffer.position(), byteBuffer.limit()));
        // 寫入2字節的數據
        byteBuffer.put((byte) 1);
        byteBuffer.put((byte) 2);
        byteBuffer.put((byte) 3);
        // 再看數據
        System.out.println(String.format("寫入3字節後,capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(),
                byteBuffer.position(), byteBuffer.limit()));

        // 轉換爲讀取模式(不調用flip方法,也是可以讀取數據的,但是position記錄讀取的位置不對)
        System.out.println("#######開始讀取");
        byteBuffer.flip();
        byte a = byteBuffer.get();
        System.out.println(a);
        byte b = byteBuffer.get();
        System.out.println(b);
        System.out.println(String.format("讀取2字節數據後,capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(),
                byteBuffer.position(), byteBuffer.limit()));

        // 繼續寫入3字節,此時讀模式下,limit=3,position=2.繼續寫入只能覆蓋寫入一條數據
        // clear()方法清除整個緩衝區。compact()方法僅清除已閱讀的數據。轉爲寫入模式
        byteBuffer.compact(); // buffer : 1 , 3
        byteBuffer.put((byte) 3);
        byteBuffer.put((byte) 4);
        byteBuffer.put((byte) 5);
        System.out.println(String.format("最終的情況,capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(),
                byteBuffer.position(), byteBuffer.limit()));

        // rewind() 重置position爲0
        // mark() 標記position的位置
        // reset() 重置position爲上次mark()標記的位置

    }
}

ByteBuffer內存類型

ByteBuffer爲性能關鍵型代碼提供了直接內存(direct堆外)和非直接內存(heap堆)兩種實現。

堆外內存獲取方式:

ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(noBytes);

好處:

  1. 進行網絡IO或者文件IO時比heapBuffer少一次拷貝。(file/socket ---- OS memory ---- jvm heap (可以用賣股票的方式來理解:先將股票轉化爲現金,再用現金進行交易))GC會移動對象內存,在寫file或socket的過程中,JVM的實現中,會先把數據複製到堆外,再進行寫入。
  2. GC範圍之外,降低GC壓力,但實現了自動管理。DirectByteBuffer中有一個Cleaner對象(PhantomReference),Cleaner被GC前會執行celan方法,觸發DirectByteBuffer中定義的Deallocator

建議:

  1. 性能確實可觀的時候採取使用;分配給大型、長壽命;(網絡傳輸、文件讀寫場景)
  2. 通過虛擬機參數MaxDirectMemorySize限制大小,防止耗盡整個機器的內存;
public class DirectBufferDemo {
	public static void main(String[] args) {
		// 構建一個byte字節緩衝區,容量是4
		ByteBuffer byteBuffer = ByteBuffer.allocateDirect(4);
		// 默認寫入模式,查看三個重要的指標
		System.out.println(String.format("初始化:capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(),
				byteBuffer.position(), byteBuffer.limit()));
		// 寫入2字節的數據
		byteBuffer.put((byte) 1);
		byteBuffer.put((byte) 2);
		byteBuffer.put((byte) 3);
		// 再看數據
		System.out.println(String.format("寫入3字節後,capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(),
				byteBuffer.position(), byteBuffer.limit()));

		// 轉換爲讀取模式(不調用flip方法,也是可以讀取數據的,但是position記錄讀取的位置不對)
		System.out.println("#######開始讀取");
		byteBuffer.flip();
		byte a = byteBuffer.get();
		System.out.println(a);
		byte b = byteBuffer.get();
		System.out.println(b);
		System.out.println(String.format("讀取2字節數據後,capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(),
				byteBuffer.position(), byteBuffer.limit()));

		// 繼續寫入3字節,此時讀模式下,limit=3,position=2.繼續寫入只能覆蓋寫入一條數據
		// clear()方法清除整個緩衝區。compact()方法僅清除已閱讀的數據。轉爲寫入模式
		byteBuffer.compact();
		byteBuffer.put((byte) 3);
		byteBuffer.put((byte) 4);
		byteBuffer.put((byte) 5);
		System.out.println(String.format("最終的情況,capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(),
				byteBuffer.position(), byteBuffer.limit()));
		byteBuffer.array();
		// rewind() 重置position爲0
		// mark() 標記position的位置
		// reset() 重置position爲上次mark()標記的位置
	}
}

Channel通道

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Gqtrene3-1589444120550)(images/NIO與BIO通信區別.png)]

和標準IO Stream操作的區別:

在一個通道內進行讀取和寫入

stream通常是單向的(input或output)

可以非阻塞讀取和寫入通道

通道始終讀取或寫入緩衝區

Channel的API涵蓋了UDP/TCP網絡和文件IO

FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel

SocketChannel

SocketChannel用於建立TCP網絡連接,類似java.net.Socket。有兩種創建socketChannel形式:

  1. 客戶端主動發起和服務器的連接。
  2. 服務端獲取的新連接。
    在這裏插入圖片描述

注意:

**write寫:**write()在尚未寫入任何內容是就可能返回了。需要在循環中調用write()。

**read讀:**read()方法可能直接返回而根本不讀取任何數據,根據返回的int值判斷讀取了多少字節。

ServerSocketChannel

ServerSocketChannel可以監聽新建的TCP連接通道,類似serverSocket。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-He9xMnqd-1589444120553)(images/ServerSocketChannel.png)]

**serverSocketChannel.accept():**如果該通道處於非阻塞模式,那麼如果沒有掛起的連接,該方法將立即返回null。必須檢查返回的SocketChannel是否爲null。

Selector選擇器

Selector是一個Java NIO組件,可以檢查一個或多個NIO通道,並確定哪些通道已準備好進行讀取或寫入。實現單個線程可以管理多個通道,從而管理多個網絡連接。

一個線程使用Selector監聽多個channel的不同事件:

四個事件分別對應selectionKey四個常量。

  1. Connect連接(SelectionKey.OP_CONNECT)
  2. Accept準備就緒(OP_ACCEPT)
  3. Read讀取(OP_READ)
  4. Write寫入(OP_WRITE)

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-EuOWCSUU-1589444120554)(images/Selector工作流程.png)]

核心概念:

實現一個線程處理多個通道的核心概念理解:事件驅動機制。

非阻塞的網絡通道下,開發者通過Selector註冊對於通道感興趣的事件類型,線程通過監聽事件來觸發相應的代碼執行。(拓展:更底層是操作系統的多路複用機制)

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-MAgdiQ0n-1589444120555)(images/Selector核心概念.png)]

NIO對比BIO

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-cUN5LhL3-1589444120556)(images/BIO線程模型.png)]

  • 阻塞IO,線程等待時間長
  • 一個線程負責一個連接處理
  • 線程多且利用率低

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Y61abJq6-1589444120556)(images/NIO線程模型.png)]

  • 非阻塞IO,線程利用率更高
  • 一個線程處理多個連接事件
  • 性能更強大

如果程序需要支撐大量的連接,使用NIO是最好的方式。

Tomcat8中,已經完全去除BIO相關的網絡處理代碼,默認採用NIO進行網絡處理。

Reactor網絡模型

網絡模型:

應用於多線程高併發場景、海量請求,充分利用CPU多核的性能

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-vxJF6Mt4-1589444120556)(images/Ractor模式.png)]

/**
 * NIO selector 多路複用reactor線程模型
 */
public class NIOServerV3 {
    /** 處理業務操作的線程 */
    private static ExecutorService workPool = Executors.newCachedThreadPool();

    /**
     * 封裝了selector.select()等事件輪詢的代碼
     */
    abstract class ReactorThread extends Thread {

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

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

        private ReactorThread() 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 {
            // 爲什麼register要以任務提交的形式,讓reactor線程去處理?
            // 因爲線程在執行channel註冊到selector的過程中,會和調用selector.select()方法的線程爭用同一把鎖
            // 而select()方法實在eventLoop中通過while循環調用的,爭搶的可能性很高,爲了讓register能更快的執行,就放到同一個線程來處理
            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、創建多個線程 - accept處理reactor線程 (accept線程)
    private ReactorThread[] mainReactorThreads = new ReactorThread[1];
    // 2、創建多個線程 - io處理reactor線程  (I/O線程)
    private ReactorThread[] subReactorThreads = new ReactorThread[8];

    /**
     * 初始化線程組
     */
    private void newGroup() throws IOException {
        // 創建IO線程,負責處理客戶端連接以後socketChannel的IO讀寫
        for (int i = 0; i < subReactorThreads.length; i++) {
            subReactorThreads[i] = new ReactorThread() {
                @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(Thread.currentThread().getName() + "收到數據,來自:" + ch.getRemoteAddress());

                    // TODO 業務操作 數據庫、接口...
                    workPool.submit(() -> {
                    });

                    // 響應結果 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);
                    }
                }
            };
        }

        // 創建mainReactor線程, 只負責處理serverSocketChannel
        for (int i = 0; i < mainReactorThreads.length; i++) {
            mainReactorThreads[i] = new ReactorThread() {
                AtomicInteger incr = new AtomicInteger(0);

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


    }

    /**
     * 初始化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(mainReactorThreads.length);
        mainReactorThreads[index].doStart();
        SelectionKey selectionKey = mainReactorThreads[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(); // 1、 創建main和sub兩組線程
        nioServerV3.initAndRegister(); // 2、 創建serverSocketChannel,註冊到mainReactor線程上的selector上
        nioServerV3.bind(); // 3、 爲serverSocketChannel綁定端口
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章