1. 程式人生 > 實用技巧 >不會吧!做了這麼久開發還有不會NIO的,看看BAT大佬是怎麼用的吧

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

前言

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

BIO

簡單實現伺服器和客戶端

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);
    }
}

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);
    }
}

針對於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容量越來越小,具體解釋在下面程式碼)。

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());
        }
    }
}

Channel

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

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();
    }
}

簡單NIO實現

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

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

Server

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();
        }
}

Client

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("傳送完畢");
    }
}

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

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<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> 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);
                }
            }
        }
    }
}

Client

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("傳送完畢");
        }

    }
}

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

這裡需要注意的是需要將ServerSocketChannel和SocketChannel程式設計非阻塞(呼叫configureBlocking(false)),不然是無法註冊到Selector中。

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

最後

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