java nio 基礎用法

傳統的io模型問題: 

在傳統的IO模型中,每個連接創建成功之後都需要一個線程來維護,每個線程包含一個while死循環,那麼1w個連接對應1w個線程,繼而1w個while死循環,這就帶來如下幾個問題:

  1. 線程資源受限:線程是操作系統中非常寶貴的資源,同一時刻有大量的線程處於阻塞狀態是非常嚴重的資源浪費,操作系統耗不起
  2. 線程切換效率低下:單機cpu核數固定,線程爆炸之後操作系統頻繁進行線程切換,應用性能急劇下降。
  3. 除了以上兩個問題,IO編程中,我們看到數據讀寫是以字節流爲單位,效率不高。

NIO編程模型

NIO編程模型中,新來一個連接不再創建一個新的線程,而是可以把這條連接直接綁定到某個固定的線程,然後這條連接所有的讀寫都由這個線程來負責,那麼他是怎麼做到的?我們用一幅圖來對比一下IO與NIO

如上圖所示,IO模型中,一個連接來了,會創建一個線程,對應一個while死循環,死循環的目的就是不斷監測這條連接上是否有數據可以讀,大多數情況下,1w個連接裏面同一時刻只有少量的連接有數據可讀,因此,很多個while死循環都白白浪費掉了,因爲讀不出啥數據。

而在NIO模型中,他把這麼多while死循環變成一個死循環,這個死循環由一個線程控制,那麼他又是如何做到一個線程,一個while死循環就能監測1w個連接是否有數據可讀的呢?
這就是NIO模型中selector的作用,一條連接來了之後,現在不創建一個while死循環去監聽是否有數據可讀了,而是直接把這條連接註冊到selector上,然後,通過檢查這個selector,就可以批量監測出有數據可讀的連接,進而讀取數據,下面我再舉個非常簡單的生活中的例子說明IO與NIO的區別。

在一家幼兒園裏,小朋友有上廁所的需求,小朋友都太小以至於你要問他要不要上廁所,他纔會告訴你。幼兒園一共有100個小朋友,有兩種方案可以解決小朋友上廁所的問題:

  1. 每個小朋友配一個老師。每個老師隔段時間詢問小朋友是否要上廁所,如果要上,就領他去廁所,100個小朋友就需要100個老師來詢問,並且每個小朋友上廁所的時候都需要一個老師領着他去上,這就是IO模型,一個連接對應一個線程。
  2. 所有的小朋友都配同一個老師。這個老師隔段時間詢問所有的小朋友是否有人要上廁所,然後每一時刻把所有要上廁所的小朋友批量領到廁所,這就是NIO模型,所有小朋友都註冊到同一個老師,對應的就是所有的連接都註冊到一個線程,然後批量輪詢。

這就是NIO模型解決線程資源受限的方案,實際開發過程中,我們會開多個線程,每個線程都管理着一批連接,相對於IO模型中一個線程管理一條連接,消耗的線程資源大幅減少

由於NIO模型中線程數量大大降低,線程切換效率因此也大幅度提高

NIO解決這個問題的方式是數據讀寫不再以字節爲單位,而是以字節塊爲單位。IO模型中,每次都是從操作系統底層一個字節一個字節地讀取數據,而NIO維護一個緩衝區,每次可以從這個緩衝區裏面讀取一塊的數據,
這就好比一盤美味的豆子放在你面前,你用筷子一個個夾(每次一個),肯定不如要勺子挖着吃(每次一批)效率來得高。

 

一、Buffer demo

public class BufferTest {
    public static void main(String[] args) {
        //靜態方法常見 buffer
        IntBuffer buf = IntBuffer.allocate(10);
        int[] array = new int[]{3, 5, 1};

        //put一個數組到buffer中,使用put方式將
        // buf.put(array);

        //使用wrap方式會直接更改原數組
        buf = buf.wrap(array);

        //IntBuffer.wrap(array, 0, 2);

        buf.put(0, 7);

        int length = buf.limit();
        for (int i = 0; i < length; i++) {
            System.out.print(buf.get(i));
        }

        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i]);
        }

        System.out.println(buf);
        /**
         *   limit = position;
         *   position = 0;
         */
        buf.flip();
        /**
         *   position = 0;
         *   limit = capacity;
         */
        buf.clear();
        System.out.println(buf);
        //創建一個新的字節緩衝區,共享此緩衝區的內容
        IntBuffer newBuffer = buf.duplicate();
        System.out.println(newBuffer);
    }

}

二、FileChannel demo

public class FileChannelTest {


    public static void testFileChannel() throws IOException {
        RandomAccessFile aFile = new RandomAccessFile("D:/nio-data.txt", "rw");
        FileChannel channel = aFile.getChannel();
        //分配一個新的緩衝區
        ByteBuffer allocate = ByteBuffer.allocate(48);
        int bytesRead = channel.read(allocate);
        while (bytesRead != -1) {
            System.out.println("Read " + bytesRead);
            allocate.flip();
            while (allocate.hasRemaining()) {
                System.out.print((char) allocate.get());
            }
            allocate.clear();
            bytesRead = channel.read(allocate);
        }
        aFile.close();
    }


    public static void fileChannelDemo() throws IOException {

        //定義一個byteBuffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        FileChannel inputChannel = new FileInputStream("D:/nio-data.txt").getChannel();

        FileChannel outputChannel = new FileOutputStream("D:/nio-data.txt", true).getChannel();

        //讀取數據
        byteBuffer.clear();
        int len = inputChannel.read(byteBuffer);

        System.out.println(new String(byteBuffer.array(), "UTF-8"));
        System.out.println(new String(byteBuffer.array(), 0, len, "UTF-8"));

        ByteBuffer byteBuffer2 = ByteBuffer.wrap("奧會計師八度空間".getBytes());

        outputChannel.write(byteBuffer2);

        outputChannel.close();
        inputChannel.close();
    }


    public static void main(String[] args) {
        try {
            FileChannelTest.fileChannelDemo();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 三、不使用 選擇器 selector 的 ServerSocketChannel 和 SocketChannel  的demo

服務端:

public class NioChannelServer {

    private ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

    //獲取一個intBuffer視圖,操作視圖的同時原緩衝區也會改變
    private IntBuffer intBuffer = byteBuffer.asIntBuffer();

    private SocketChannel socketChannel = null;

    private ServerSocketChannel serverSocketChannel = null;


    /**
     * 打開服務端的通道
     *
     * @throws Exception
     */
    public void openChannel() throws Exception {
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(8888));
        System.out.println("服務端通道已經打開");
    }

    /**
     * 等待新的連接
     *
     * @throws Exception
     */
    public void waitReqConn() throws Exception {

        while (true) {
            socketChannel = serverSocketChannel.accept();
            if (null != socketChannel) {
                System.out.println("新的連接加入!");
            }
            //處理請求
            processReq();
            socketChannel.close();

        }
    }

    private void processReq() throws IOException {
        System.out.println("開始讀取和處理客戶端數據。。");
        byteBuffer.clear();
        socketChannel.read(byteBuffer);
        int result = intBuffer.get(0) + intBuffer.get(1);
        byteBuffer.flip();
        byteBuffer.clear();
        //修改視圖,byteBuffer也會變化
        intBuffer.put(0, result);
        socketChannel.write(byteBuffer);
        System.out.println("讀取處理完成");

    }

    public void start() {
        try {
            //打開通道
            openChannel();
            //等待客戶端連接
            waitReqConn();
            socketChannel.close();
            System.out.println("服務端處理完畢");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args){
       new NioChannelServer().start();
    }

}

客戶端:

public class NioChannelClient {

    private SocketChannel socketChannel = null;

    private ByteBuffer buff = ByteBuffer.allocate(8);

    private IntBuffer intBuffer = buff.asIntBuffer();


    public SocketChannel connect() throws IOException {
        return SocketChannel.open(new InetSocketAddress("127.0.0.1", 8888));
    }


    public int getSum(int a, int b) {
        int result = 0;
        try {
            socketChannel = connect();
            sendRequest(a, b);
            result = receiveResult();
        } catch (Exception e) {
         e.printStackTrace();
        }
        return result;
    }

    private  int  receiveResult() throws IOException {
        buff.clear();
        socketChannel.read(buff);
        return intBuffer.get(0);
    }

    private void sendRequest(int a, int b) throws IOException {
        buff.clear();
        intBuffer.put(0,a);
        intBuffer.put(1,b);
        socketChannel.write(buff);
        System.out.println("客戶端發送請求 ("+a+"+"+b+")");
    }


    
    public static void main(String[] args){

        Random random = new Random();

        for (int i = 0; i <10 ; i++) {
            int result = new NioChannelClient().getSum(random.nextInt(100),random.nextInt(100));
            System.out.println(result);
        }
    }
}

 

四、使用 selector 方式 實現 ServerSocketChannel  和 SocketChannel

選擇器(Selector) 是 SelectableChannle 對象的多路複用器,Selector 可以同時監控多個 SelectableChannel 的 IO 狀況,也就是說,利用 Selector可使一個單獨的線程管理多個 Channel,selector 是非阻塞 IO 的核心。

選擇器(Selector)的應用:

當通道使用register(Selector sel, int ops)方法將通道註冊選擇器時,選擇器對通道事件進行監聽,通過第二個參數指定監聽的事件類型。

其中可監聽的事件類型包括以下:

  讀 : SelectionKey.OP_READ (1)

  寫 : SelectionKey.OP_WRITE (4)

  連接 : SelectionKey.OP_CONNECT (8)

  接收 : SelectionKey.OP_ACCEPT (16)

如果需要監聽多個事件是:

  int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE ; //表示同時監聽讀寫操作

服務端:

public class SelectorServer {
    private Selector selector = null;
    private ServerSocketChannel serverSocketChannel = null;
    private int keys = 0;

    public void initServer() throws IOException {
        selector = Selector.open();
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 8888));
        serverSocketChannel.configureBlocking(false);
        //服務端通道註冊accept事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    }

    private void listen() throws IOException {
        System.out.println("服務端已經啓動");
        while (true) {
            //讓通道選擇器至少選擇一個通道
            keys = this.selector.select();
            System.out.println(keys);
            Iterator<SelectionKey> itor = this.selector.selectedKeys().iterator();

            if (keys > 0) {
                //進行輪詢
                while (itor.hasNext()) {
                    try{
                    SelectionKey key = itor.next();
                    if (key.isAcceptable()) {
                        //serverSocketChannel = (ServerSocketChannel) key.channel();
                        //獲取和客戶端連接的服務端渠道
                        SocketChannel channel = serverSocketChannel.accept();
                        channel.configureBlocking(false);
                        channel.write(ByteBuffer.wrap("hello".getBytes()));
                        //還需要讀取客戶端發過來的數據,所以需要註冊一個讀取數據的事件
                        channel.register(selector, SelectionKey.OP_READ);
                    } else if (key.isReadable()) {
                        read(key);
                    }
                    }finally {
                        //處理完一個key,就刪除,防止重複處理
                        itor.remove();
                    }
                }
            } else {
                System.out.println("select finished without any keys");
            }

        }


    }

    private void read(SelectionKey key) throws IOException {
        SocketChannel socketChannel = (SocketChannel) key.channel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        int len = socketChannel.read(byteBuffer);
        String msg = new String(byteBuffer.array(), 0, len);
        System.out.println("服務端接收到的消息是" + msg);
    }


    public void start() {
        try {
            initServer();
            listen();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new SelectorServer().start();
    }


}

客戶端:

 

public class SelectorClient {
    private Selector selector;

    private ByteBuffer outBuffer = ByteBuffer.allocate(1024);
    private ByteBuffer inputBuffer = ByteBuffer.allocate(1024);

    private int keys = 0;

    private SocketChannel socketChannel = null;

    public void initClient() throws IOException {
        selector = Selector.open();

        socketChannel = SocketChannel.open();
        //客戶端通道配置爲非阻塞
        socketChannel.configureBlocking(false);
        //連接服務端
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 8888));
        //註冊客戶端連接服務器的事件
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
    }


    private void listen() throws IOException {
        while (true) {
            keys = this.selector.select();
            System.out.println(keys);
            if (keys > 0) {
                Iterator<SelectionKey> iter = this.selector.selectedKeys().iterator();

                while (iter.hasNext()) {
                    try{
                    SelectionKey key = iter.next();
                    if (key.isConnectable()) {
                        SocketChannel channel = (SocketChannel) key.channel();
                        if (channel.isConnectionPending()) {
                            channel.finishConnect();
                            System.out.println("完成連接");
                        }
                        //連接完成之後,肯定還要做其它的事情,比如寫
                        channel.register(selector, SelectionKey.OP_WRITE);

                    } else if (key.isWritable()) {
                        SocketChannel channel = (SocketChannel) key.channel();
                        outBuffer.clear();
                        System.out.println("客戶端正在寫數據。。");

                        //從控制檯寫消息
                        Scanner scanner = new Scanner(System.in);
                        while (true) {
                            String msg = scanner.next();
                            channel.write(ByteBuffer.wrap(msg.getBytes()));
                            if("end".equals(msg)) {
                                break;
                            }
                        }
                        channel.register(selector, SelectionKey.OP_READ);
                        System.out.println("客戶端寫數據完成。。。");
                    } else if (key.isReadable()) {
                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        inputBuffer.clear();
                        int len = socketChannel.read(inputBuffer);
                        System.out.println("讀取服務端發送的消息:" + new String(inputBuffer.array()));
                    }
                    }finally{
                     iter.remove();
                    }
                }
            } else {
                System.out.println("select finished without any keys");
            }
        }

    }


    public void start() {
        try {
            initClient();
            listen();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args){
       new SelectorClient().start();
    }


}

nio的非阻塞是對於網絡通道來說的,需要使用Channel.configureBlocking(false)來設置通道爲非阻塞的,如果沒設置,默認是阻塞的。 

 

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