不會吧!做了這麼久開發還有不會NIO的,看阿里大佬怎麼用的吧

前言

  • 在將NIO之前,我們必須要了解一下Java的IO部分知識。
  • BIO(Blocking IO)
  • 阻塞IO,在Java中主要就是通過ServerSocket.accept()實現的。
  • NIO(Non-Blocking IO)
  • 非阻塞IO,在Java主要是通過NIOSocketChannel + Seletor實現的。
  • AIO(Asyc IO)
  • 異步IO,目前不做學習。

BIO

簡單實現服務器和客戶端

<code>package net.io;

import net.ByteUtil;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

//NIO(NonBlocking IO)非阻塞IO
//通過一個事件監聽器,吧這些客戶端的連接保存起來,如果有時間發生再去處理,沒時間發生不處理
public class Server {
    public Server(int port) {
        try {
            //創建服務器端,監聽端口port
            ServerSocket serverSocket = new ServerSocket(port);
            //對客戶端進行一個監聽操作,如果有連接過來,就將連接返回(socket)-----阻塞方法
            while (true) {
                //監聽,阻塞方法
                Socket socket = serverSocket.accept();
                //每個服務器和客戶端的通信都是針對與socket進行操作
                System.out.println("客戶端" + socket.getInetAddress());
                InputStream inputStream = socket.getInputStream();
                ObjectInputStream ois = new ObjectInputStream(inputStream);
                //獲取客戶端發送的message
                Object get = ois.readObject();
                System.out.println("接收到的消息為:"  + get);

                //服務器需要給客戶端進行一個回應
                OutputStream outputStream = socket.getOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(outputStream);
                String message = "客戶端你好,我是服務器端";
                //我這裡寫了,不代表發送了,知識寫到了輸出流的緩衝區
                oos.writeObject(message);
                //發送並清空
                oos.flush();
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new Server(7000);
    }
}

/<code>
<code>package net.io;

import net.ByteUtil;

import java.io.*;
import java.net.Socket;

public class Client {
    public Client(int port){
        try {
            Socket socket = new Socket("localhost",port);

            //inputStream是輸入流,從外面接收信息
            //outpurStream是輸出流, 往外面輸出信息
            OutputStream outputStream = socket.getOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(outputStream);
            //發送的信息
            String message = "服務器你好,我是客戶端";
            //我這裡寫了,不代表發送了,知識寫到了輸出流的緩衝區
            oos.writeObject(message);
            //發送並清空
            oos.flush();

            //接收服務器的回應
            InputStream inputStream = socket.getInputStream();
            ObjectInputStream ois = new ObjectInputStream(inputStream);
            Object get = ois.readObject();
            System.out.println("接收的信息為:" + get);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new Client(7000);
    }
}

/<code> 

針對於BIO,為什麼是阻塞IO,是因為BIO是基於Socket完成數據的讀寫操作,Server調用accept()方法持續監聽socket連接,所以就阻塞在accept()這裡(後面的操作如果沒有socket連接則無法執行),那這樣就代表服務器只能同時處理一個socket的操作。
所以後序我們通過為socket連接創建一個線程執行,這樣就可以增大服務器處理連接的數量了。
但是新的問題也就出來了,如果客戶端的連接很多,那麼就會導致服務器創建很多的線程
對socket進行處理,如果服務器端不斷開連接的話,那麼對應的線程也不會被銷燬,這樣大數量的線程的維護十分消耗資源。針對於這種情況設計出了Java的NIO。

NIO

首先我們需要介紹一下NIO。如果說BIO是面向socket進行讀寫操作的話,那麼NIO則是面向channel進行讀寫操作(或者說面向buffer)。
這裡我們在講解NIO之前,需要先講解一下這個buffer。眾所周知這就是一個緩衝,並且我們知道socket具有輸入流inputStream和輸出流outputStream(讀寫分開的),但是我們的channel是同時具有read和write兩個方法,而且兩個方法都是基於buffer進行操作(這裡就可以說明channel僅能比普通輸入輸出流好,相當於channel是一條雙向,輸入輸出流是兩條單向),所以我們可以知道buffer的重要性。

Buffer

諸如ByteBuffer,IntBuffer等都是Buffer的派生抽象類,需要調用抽象類的靜態方法allocate(X capacity)方法進行一個初始化操作,該方法就是初始化buffer的大小,或者使用wrap(X x)方法,該方法相當於直接將信息存入緩衝中。至於存入buffer的put()方法和取出緩存的get()方法在下面代碼中我就詳細介紹(有底層知識,具有源碼閱讀能力的可以根據我的註釋進行閱讀),最關鍵的還有flip()方法,它是作為一個讀寫切換的作用,他使的緩存可又讀又寫,又使得讀寫相互隔離(需要注意的是使用buffer儘量是依次寫完然後再一次讀完,最後在調用clear()方法進行復位,不然會導致buffer容量越來越小,具體解釋在下面代碼)。

<code>package net.nio.buffer;

import java.nio.IntBuffer;

public class TestBuffer {
    public static void main(String[] args) {

        /*
         *IntBuffer有四個重要參數
         * 1.mark 標記
         * 2.position 相當於當前下標、索引
         * 3.limit 代表緩衝的終點,讀取不能超過該下標,當然也不能超過最大容量。(在調用flip時候會將當前下標position值賦值給limit,然後position置0)
         * 4.Capacity 最大容量,在初始化IntBuffer對象時候就定義好了,不能改變(IntBuffer.allocate(int capacity) )
         *
         * ctrl+h 可以查看該類的子類
         */

        //intBuffer初始化
        IntBuffer intBuffer = IntBuffer.allocate(5);

        //放數據到緩衝區中
        intBuffer.put(10);
        intBuffer.put(11);
        intBuffer.put(12);
//        intBuffer.put(13);
//        intBuffer.put(14);

        /*
         *這裡的讀寫反轉的實現機制是:
         * 例如我們緩衝區容量為5,調用方法put()將數據寫入緩衝區中,假如我們寫入三個此時position為3,此時limit = capacity
         * 如果我們調用flip方法使得limit = 3 ,position = 0 ,mark我們現在先不管(下圖源碼已說明)
         *  public Buffer flip() {
         *      limit = position;
         *      position = 0;
         *      mark = -1;
         *      return this;
         * }
         *
         * 此時我們調用get()方法時候,取得下標是position的值,即從0下標讀取。直到讀取到position = limit = 3時候停止(不包括3)
         * 1.如果我們這個時候不調用flip()方法直接再次put()往緩衝區寫入數據(即沒從讀狀態切換到寫狀態),那麼就會報錯超過下標overflow
         * 2.如果我們調用一次flip()(即進入寫狀態)寫入一個數據後,那麼此時position = 0,limit = 3,此時我們最多存放3個數據(即下標0,1,2)
         * 如果我們不再次調用flip()切換狀態那麼就會導致,讀取到錯誤數據,(即只存入了一個數據,但是卻取出來了3個數據)
         *
         * 上述說明了一個問題,如果我們存取的數據越來越小,那麼這個緩衝區逐漸縮小,導致並不能存取他的最大容量,可能會浪費內存,
         *(因為position是不能超過limit的,然而調用flip()方法後會使的limit = position(賦值操作),那麼如果數據越來越少,
         * 就會導致緩衝區能使用的部分越來越小)
         *
         * 總結:緩衝區的大小設置應該根據實際使用進行設置(並且要及時調用clear() ),否則可能會導致緩衝區的內存浪費。
         */
        intBuffer.flip();
        //切換讀寫狀態
        //判斷緩存區是否還有剩餘
        while (intBuffer.hasRemaining()) {
            System.out.println(intBuffer.get());
        }
    }
}

/<code> 

Channel

Channel是NIO實現的基礎,對於NIO,Channel的地位相當於BIO的socket。
Channel具有非常多方法,其中使用最多的就是兩個方法write(ByteBuffer buf)和read(ByteBuffer buf)方法。
(這裡需要注意的是這個read和write是buffer作為主體的,即read()方法是channel往buffer裡寫數據,而write()方法是指buffer向channel寫數據)

<code>package net.nio.channel;

        import java.io.FileOutputStream;
        import java.nio.ByteBuffer;
        import java.nio.channels.FileChannel;

public class TestChannel {
    public static void main(String[] args) throws Exception{
        String abc = "我寫入文件了";

        //寫入的文件地址與文件名
        FileOutputStream fileOutputStream = new FileOutputStream("C:\\xxx\\xxx\\xxx\\test.txt");

        //從輸出流中獲取channel實例
        FileChannel channel = fileOutputStream.getChannel();

        //創建字節緩衝區
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        //將字符串轉化成字節數組並放入緩衝區中,然後緩衝區轉換讀寫狀態,由寫入狀態變為讀取狀態
        byteBuffer.put(abc.getBytes());
        byteBuffer.flip();

        //將緩衝區數據寫入到channel中(這裡write代表從緩衝區寫入,read代表從channel讀取到緩衝區)
        channel.write(byteBuffer);
        //關閉通道和輸出流
        channel.close();
        fileOutputStream.close();
    }
}

/<code>

簡單NIO實現

上面在介紹NIO時候講過,NIO是需要一個Selector線程去監聽那些客戶端有實現發生,從而在進行處理,而不是BIO的一個線程維護一個socket。

下面針對於NIO我們先不引入Selector,就用BIO的方式實現一個客戶端和服務器端。(相當於作為一個練手)

Server

<code>package net.nio.socket;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Arrays;

public class Server {
    public static void main(String[] args) throws Exception {
        //開啟nio的服務器端,並且綁定8000端口進行監聽
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress(8000);
        serverSocketChannel.bind(inetSocketAddress);

        //創建緩衝區數組
        ByteBuffer byteBuffer = ByteBuffer.allocate(40);

        //服務器端接收來自客戶端的請求,創建客戶端的socket的實例
        SocketChannel socketChannel = serverSocketChannel.accept();
        //將客戶端發送的數據讀取到buffer數組中
        socketChannel.read(byteBuffer);
        byte[] array = byteBuffer.array();
        String msg = new String(array);
        System.out.println("服務器收到信息 : " + msg);

        //對buffer數組進行讀寫反轉,由讀狀態到寫狀態
        byteBuffer.flip();

        //將數據回顯到客戶端去
        byteBuffer.put("ok".getBytes());

        //做完一套讀寫操作後,需要進行clear
        byteBuffer.clear();
        }
}

/<code> 

Client

<code>package net.nio.serverclient;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class NIOClient {
    public static void main(String[] args) throws Exception {
        SocketChannel socketChannel = SocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost",8000);
        socketChannel.configureBlocking(false);

        //如果客戶端未連接上服務器
        if (!socketChannel.connect(inetSocketAddress)) {
            System.out.println("客戶端連接不上服務器。。。。");

            //如果客戶端沒有完成連接
            while (!socketChannel.finishConnect()) {
                System.out.println("連接中。。。。");
            }
        }

        //進入到這裡說明連接成功
        String message = "hello , Server!";
        ByteBuffer byteBuffer = ByteBuffer.wrap(message.getBytes());

        //將buffer中的數據寫入socketChannel
        socketChannel.write(byteBuffer);
        System.out.println("發送完畢");
    }
}

/<code>

NIO實現(基於Selector)

首先我們需要知道Selector是什麼?
Selector是一個選擇器,既然是一個選擇器,那麼肯定是先有選項再有選擇,理解這個後就知道channel肯定有的就是rigister()方法(因為需要將自己註冊到Selector中)。

既然選項有了,那麼如何選擇呢?
Selector是針對已註冊的channel中對有事件(例如:服務器:接受,讀寫,客戶端:讀寫,服務器是在服務器開始就將自己註冊,客戶端是連接成功後由服務器將其註冊)發生的channel進行處理。

Selector註冊的不是簡單的channel,而是將channel和其監聽事件封裝成一個SelectionKey保存在Selector底層的Set集合中。

Selector的keys()和selectedKeys()兩個方法需要注意:
keys()方法是返回已註冊的所有selectionKey。
selectedKeys()方法是返回有事件發生的selectionKey。

上面就是Selector的簡單工作流程,下面我將附上代碼,因為有較詳細的註釋,所以除了重要知識點我不再多介紹。


Server

<code>package net.nio.serverclient;

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;
import java.util.Set;

public class NIOServer {
    public static void main(String[] args) throws Exception {
        //開啟ServerSocketChannel的監聽
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //綁定端口8000
        serverSocketChannel.bind(new InetSocketAddress(8000));

        //創建Selector對象
        Selector selector = Selector.open();

        //設置監聽為非阻塞
        serverSocketChannel.configureBlocking(false);

        //將ServerSocketChannel註冊到Selector中(註冊事件為ACCEPT)
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        //Selector監聽ACCEPT時間
        while (true) {
            //未有事件發生(下面是等待),返回值為int,代表事件發生個數
            if (selector.select(1000) == 0) {
                System.out.println("服務器等待了1S,無事件發生。。。。");
                continue;
            }

            //有客戶端請求過來,就獲取到相關的selectionKeys集合
            Set selectionKeys = selector.selectedKeys();
            Iterator iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                //獲取到事件
                SelectionKey selectionKey = iterator.next();
                //移出讀取過的事件
                iterator.remove();

                //根據對應事件對應處理
                if (selectionKey.isAcceptable()) {
                    //有新的客戶端連接服務器
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    //給客戶端設置非阻塞
                    socketChannel.configureBlocking(false);
                    //設置該SocketChannel為讀事件,併為它綁定一個Buffer
                    socketChannel.register(selector,SelectionKey.OP_READ,ByteBuffer.allocate(1024));
                }
                if (selectionKey.isReadable()) {
                    //通過Key反向獲取到事件的channel
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    //獲取到事件綁定的buffer
                    ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();
                    socketChannel.read(byteBuffer);
                    //重置緩衝
                    byteBuffer.clear();
                    String message = new String(byteBuffer.array());
                    System.out.println("接收到客戶端信息為: "+ message);
                }
            }
        }
    }
}

/<code>

Client

<code>package net.nio.serverclient;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;

public class NIOClient {
    public static void main(String[] args) throws Exception {
        SocketChannel socketChannel = SocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost",8000);
        socketChannel.configureBlocking(false);

        //如果客戶端未連接上服務器
        if (!socketChannel.connect(inetSocketAddress)) {
            System.out.println("客戶端連接不上服務器。。。。");

            //如果客戶端沒有完成連接
            while (!socketChannel.finishConnect()) {
                System.out.println("連接中。。。。");
            }
        }

        //進入到這裡說明連接成功
        while(true) {
            Scanner scanner = new Scanner(System.in);
            String message = scanner.nextLine();
            ByteBuffer byteBuffer = ByteBuffer.wrap(message.getBytes());

            //將buffer中的數據寫入socketChannel
            socketChannel.write(byteBuffer);
            System.out.println("發送完畢");
        }

    }
}

/<code>

我們這裡可以發現,只需要主函數中進行一個死循環,死循環中對selector註冊的channel進行監聽(select()方法),有事件發生則根據channel註冊的監聽事件對應進行處理。

這裡需要注意的是需要將ServerSocketChannel和SocketChannel編程非阻塞(調用configureBlocking(false)),不然是無法註冊到Selector中。

還有一件事需要注意:我們每次是通過iterator(迭代器)遍歷發生時間的Set ,為了避免重複處理時間,我們在獲取發生時間的selctionKey以後,就將其remove()。

最後

感謝你看到這裡,看完有什麼的不懂的可以在評論區問我,覺得文章對你有幫助的話記得給我點個贊,每天都會分享java相關技術文章或行業資訊,歡迎大家關注和轉發文章!


不會吧!做了這麼久開發還有不會NIO的,看阿里大佬怎麼用的吧


分享到:


相關文章: