Tomcat架構解析(六)-----BIO、NIO、NIO2、APR


  對於應用服務器來說,性能是非常重要的,基本可以說決定着這款應用服務器的未來。通常從軟件角度來說,應用服務器性能包括如下幾個方面:

  1、請求處理的並發程度,當前主流服務器均采用異步的方式處理客戶端的請求;

  2、減少網絡傳輸的數據量,提高網絡利用率;

  3、降低新建網絡鏈接的開銷,以實現鏈接在多個請求之間的復用;

  4、選擇合適的I/O方式,例如NIO等。

 

 一、阻塞與非阻塞、同步與異步

 

                                       ------同步:發出一個調用時,沒有得到結果之前,該調用不返回,由調用者主動等待調用結果。
                                      |
 關注的是消息通信機制---------------------|
                                      |
                                       ------異步:調用發出之后,調用直接返回,此時不會拿到返回結果。被調用者通過狀態通知調用者或回調函數處理這個調用。

 

                                       ------阻塞:調用結果返回之前,當前線程會被掛起。
                                      |
 關注的是程序在等待調用結果時的狀態---------|
                                      |
                                       ------非阻塞:調用返回結果之前,當前線程不會被掛起。

 

 

  二、BIO

  概念:bio基於流,是同步阻塞IO模式,數據的讀取寫入必須阻塞在一個線程內等待其完成。這里使用那個經典的燒開水例子,這里假設一個燒開水的場景,有一排水壺在燒開水,BIO的工作模式就是, 叫一個線程停留在一個水壺那,直到這個水壺燒開,才去處理下一      個水壺。但是實際上線程在等待水壺燒開的時間段什么都沒有做。不知道io操作中什么時候有數據可讀,所以一直是阻塞的模式。

  缺點:當並發數達到一定量時,並且服務端需要一定的時間去處理請求時,例如1-2s,這時需要開啟非常多的線程數,並且這些線程啥事不干,都是等着請求返回,大大浪費了系統資源,而且在線程切換上下文的過程中,也會浪費很多的資源

  BIO是阻塞式I/O,通過socket在客戶端與服務端建立雙向鏈接以實現通信,主要步驟如下:

  •   a、服務端監聽某個端口是否有鏈接請求;
  •   b、客戶端向服務端發出鏈接請求;
  •   c、服務端向客戶端返回accept()消息,此時鏈接成功;
  •   d、客戶端和服務端通過send()、write()等方法與對方通信;
  •   e、關閉鏈接

 

  eg:簡單的網絡通信如下:

  服務端:

  

  

  客戶端:

  

  

  這種簡單的示例只支持一個客戶端鏈接到一個服務端,現實情況是N個客戶端鏈接到服務端。Tomcat是這么實現的:

  

  三、NIO

  概念:bio的性能是相對較差的,在NIO中,基於塊的概念,可以在不編寫本地代碼的情況下利用底層優化。

  NIO結構圖:

 來個復雜點的:

 selectionKey則是用來描述相關事件。 

 

  1、通道(channel)

  

  2、緩沖區(buffer)

  

  3、選擇器(selector)

  

  簡單的NIO示例:

  服務端:NIOServer

  

package com.ty.server;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
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;
import java.util.Set;

/**
 * 服務端
 */
public class NIOServer {

    /**
     * 定義服務端的selector,
     * 主要作用:
     * 1、將各種事件注冊到selector中,selector監控各種事件的發生,例如accept、read等
     * 2、將不同事件分配到不同的channel
     */
    private Selector selector;

    //服務端初始化
    public void init() throws IOException {
        //創建一個selector對象
        this.selector = Selector.open();
        //創建serverSocketChanel對象
        ServerSocketChannel serverSocketChanel = ServerSocketChannel.open();
        //設置為非阻塞
        serverSocketChanel.configureBlocking(false);
        //通過serverSocketChannel對象獲取serverSocket
        ServerSocket serverSocket = serverSocketChanel.socket();
        //綁定端口
        InetSocketAddress address = new InetSocketAddress(8080);
        serverSocket.bind(address);
        //注冊accept事件到selector中,accept用於獲取客戶端請求
        serverSocketChanel.register(selector, SelectionKey.OP_ACCEPT);
    }

    //服務端啟動服務
    public void start() throws IOException {
        //這地方只做一個最簡單的示例,不考慮服務端stop
        while(true) {
            /**
             * selector監控客戶端是否有對應事件發生,例如accept、read等等。
             * 注:此方法是阻塞的,當客戶端一直沒有事件觸發,線程一直掛起,直到至少有一事件觸發,走后續流程
             */
            selector.select();

            //獲取該selector監控到的所有觸發的事件
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            //拿到迭代器,循環所有監控到的事件
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while(iterator.hasNext()) {
                //事件用SelectionKey描述,主要包括connect、accept、read、write事件
                SelectionKey selectionKey = iterator.next();
                //每種事件只處理一次,避免重復處理
                iterator.remove();
                if(selectionKey.isAcceptable()) {
                    accept(selectionKey);
                }
                if(selectionKey.isReadable()) {
                    read(selectionKey);
                }
            }
        }
    }

    private void accept(SelectionKey selectionKey) throws IOException {
        /**
         * 從selectionkey中獲取serverSocketChannel。
         * ServerSocketChannel 是一個可以監聽新進來的TCP連接的通道, 就像標准IO中的ServerSocket一樣
         */
        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
        //serverSocketChannel監聽到新連接后,會創建socketChannel。獲取socketChannel
        SocketChannel socketChannel = serverSocketChannel.accept();
        //設置為非阻塞
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ);
    }

    private void read(SelectionKey selectionKey) throws IOException {
        /**
         * SocketChannel是一個連接到TCP網絡套接字的通道,就像標准IO中的socket
         * 創建方式:可以通過以下2種方式創建SocketChannel
         * 1、打開一個SocketChannel並連接到互聯網上的某台服務器。
         * 2、一個新連接到達ServerSocketChannel時,會創建一個SocketChannel。
         */
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        //創建讀取緩沖區
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //通過socketChannel.read()獲取客戶端的請求數據
        socketChannel.read(byteBuffer);
        String request = new String(byteBuffer.array()).trim();
        System.out.println("客戶端發送的請求為:" + request);
        //將一個數組包裝成ByteBuffer
        ByteBuffer outBuffer = ByteBuffer.wrap("請求收到啦!".getBytes());
        //數據發送到客戶端
        socketChannel.write(outBuffer);
    }

    public static void main(String[] args) throws IOException {
        NIOServer server = new NIOServer();
        server.init();
        server.start();
    }
}

  客戶端:NIOClient

  

package com.ty.client;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
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;

public class NIOClient {

    private Selector selector;

    private BufferedReader clientInput = new BufferedReader(new InputStreamReader(System.in));

    public void init() throws IOException {
        //創建selector
        this.selector = Selector.open();
        //創建SocketChannel
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
        //注冊connect事件
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
    }

    public void start() throws IOException {
        while(true) {
            selector.select();
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while(iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                iterator.remove();
                if(selectionKey.isConnectable()) {
                    connect(selectionKey);
                }
                if(selectionKey.isReadable()) {
                    read(selectionKey);
                }
            }
        }
    }

    public void connect(SelectionKey selectionKey) throws IOException {
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        //如果客戶端正在鏈接
        if(socketChannel.isConnectionPending()) {
            //如果客戶端已經鏈接成功
            if(socketChannel.finishConnect()) {
                socketChannel.configureBlocking(false);
                //鏈接成功后自然要獲取服務端的返回,因此注冊read事件
                socketChannel.register(selector, SelectionKey.OP_READ);
                String request = clientInput.readLine();
                //數據發送到服務端
                socketChannel.write(ByteBuffer.wrap(request.getBytes()));
            }else {
                //事件未注冊成功,取消掉
                selectionKey.cancel();
            }
        }
    }

    public void read(SelectionKey selectionKey) throws IOException {
        //socketChannel與服務端的對應,雙方友好建立一個通道
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        socketChannel.read(byteBuffer);
        System.out.println("服務端響應為:" + new String(byteBuffer.array()).trim());
        String request = clientInput.readLine();
        socketChannel.write(ByteBuffer.wrap(request.getBytes()));
    }

    public static void main(String[] args) throws IOException {
        NIOClient client = new NIOClient();
        client.init();
        client.start();
    }
}

測試結果:

服務端:

 

客戶端:

 這樣通過NIO,客戶端與服務端即可正常通信。

  

Tomcat中的NIO實現:

 

  

 

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM