NIO入門,看這篇文章就夠了

思維導圖

 

NIO入門,看這篇文章就夠了

 

 

學如逆水行舟,不進則退

1 NIO概述

1.1 定義

java.nio全稱java non-blocking IO,是指JDK1.4 及以上版本里提供的新api(New IO) ,爲所有的原始類型(boolean類型除外)提供緩存支持的數據容器,使用它可以提供非阻塞式的高伸縮性網絡(來源於百度百科)。

1.2 爲什麼使用NIO

在上面的描述中提到,是在JDK1.4以上的版本才提供NIO,那在之前使用的是什麼呢?答案很簡單,就是BIO(阻塞式IO),也就是我們常用的IO流。

BIO的問題其實不用多說了,因爲在使用BIO時,主線程會進入阻塞狀態,這就非常影響程序的性能,不能充分利用機器資源。但是這樣就會有人提出疑問了,那我使用多線程不就可以了嗎?

但是在高併發的情況下,會創建很多線程,線程會佔用內存,線程之間的切換也會浪費資源開銷。

而NIO只有在連接/通道真正有讀寫事件發生時(事件驅動),纔會進行讀寫,就大大地減少了系統的開銷。不必爲每一個連接都創建一個線程,也不必去維護多個線程。

避免了多個線程之間的上下文切換,導致資源的浪費。

2 NIO的三大核心

NIO的核心 對應的類或接口 應用 作用 緩衝區 java.nio.Buffer 文件IO/網絡IO 存儲數據 通道 java.nio.channels.Channel 文件IO/網絡IO 運輸 選擇器
java.nio.channels.Selector 網絡IO 控制器

2.1緩衝區(Buffer)

2.1.1 什麼是緩衝區

我們先看以下這張類圖,可以看到Buffer有七種類型。

NIO入門,看這篇文章就夠了

 

Buffer是一個內存塊。在NIO中,所有的數據都是用Buffer處理,有讀寫兩種模式。所以NIO和傳統的IO的區別就體現在這裏。傳統IO是面向Stream流,NIO而是面向緩衝區(Buffer)。

2.1.2 常用的類型ByteBuffer

一般我們常用的類型是ByteBuffer,把數據轉成字節進行處理。實質上是一個byte[]數組。

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>{
    //存儲數據的數組
    final byte[] hb;
    //構造器方法
    ByteBuffer(int mark, int pos, int lim, int cap, byte[] hb, int offset) {
        super(mark, pos, lim, cap);
        //初始化數組
        this.hb = hb;
        this.offset = offset;
    }
}
複製代碼

2.1.3 創建Buffer的方式

主要分成兩種:JVM堆內內存塊Buffer、堆外內存塊Buffer。

創建堆內內存塊(非直接緩衝區)的方法是:

//創建堆內內存塊HeapByteBuffer
ByteBuffer byteBuffer1 = ByteBuffer.allocate(1024);

String msg = "java技術愛好者";
//包裝一個byte[]數組獲得一個Buffer,實際類型是HeapByteBuffer
ByteBuffer byteBuffer2 = ByteBuffer.wrap(msg.getBytes());
複製代碼

創建堆外內存塊(直接緩衝區)的方法:

//創建堆外內存塊DirectByteBuffer
ByteBuffer byteBuffer3 = ByteBuffer.allocateDirect(1024);
複製代碼

2.1.3.1 HeapByteBuffer與DirectByteBuffer的區別

其實根據類名就可以看出,HeapByteBuffer所創建的字節緩衝區就是在JVM堆中的,即JVM內部所維護的字節數組。而DirectByteBuffer是直接操作操作系統本地代碼創建的內存緩衝數組

DirectByteBuffer的使用場景:

  1. java程序與本地磁盤、socket傳輸數據
  2. 大文件對象,可以使用。不會受到堆內存大小的限制。
  3. 不需要頻繁創建,生命週期較長的情況,能重複使用的情況。

HeapByteBuffer的使用場景:

除了以上的場景外,其他情況還是建議使用HeapByteBuffer,沒有達到一定的量級,實際上使用DirectByteBuffer是體現不出優勢的。

2.1.3.2 Buffer的初體驗

接下來,使用ByteBuffer做一個小例子,熟悉一下:

	public static void main(String[] args) throws Exception {
        String msg = "java技術愛好者,起飛!";
        //創建一個固定大小的buffer(返回的是HeapByteBuffer)
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        byte[] bytes = msg.getBytes();
        //寫入數據到Buffer中
        byteBuffer.put(bytes);
        //切換成讀模式,關鍵一步
        byteBuffer.flip();
        //創建一個臨時數組,用於存儲獲取到的數據
        byte[] tempByte = new byte[bytes.length];
        int i = 0;
        //如果還有數據,就循環。循環判斷條件
        while (byteBuffer.hasRemaining()) {
            //獲取byteBuffer中的數據
            byte b = byteBuffer.get();
            //放到臨時數組中
            tempByte[i] = b;
            i++;
        }
        //打印結果
        System.out.println(new String(tempByte));//java技術愛好者,起飛!
    }
複製代碼

這上面有一個flip()方法是很重要的。意思是切換到讀模式。上面已經提到緩存區是雙向的既可以往緩衝區寫入數據,也可以從緩衝區讀取數據。但是不能同時進行,需要切換。那麼這個切換模式的本質是什麼呢?

2.1.4 三個重要參數

//位置,默認是從第一個開始
private int position = 0;
//限制,不能讀取或者寫入的位置索引
private int limit;
//容量,緩衝區所包含的元素的數量
private int capacity;
複製代碼

那麼我們以上面的例子,一句一句代碼進行分析:

String msg = "java技術愛好者,起飛!";
//創建一個固定大小的buffer(返回的是HeapByteBuffer)
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
複製代碼

當創建一個緩衝區時,參數的值是這樣的:

 

NIO入門,看這篇文章就夠了

 

 

NIO入門,看這篇文章就夠了

 

 

當執行到byteBuffer.put(bytes),當put()進入多少數據,position就會增加多少,參數就會發生變化:

 

NIO入門,看這篇文章就夠了

 

 

NIO入門,看這篇文章就夠了

 

 

接下來關鍵一步byteBuffer.flip(),會發生如下變化:

 

NIO入門,看這篇文章就夠了

 

 

NIO入門,看這篇文章就夠了

 

 

flip()方法的源碼如下:

	public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }
複製代碼

爲什麼要這樣賦值呢?因爲下面有一句循環條件判斷:

byteBuffer.hasRemaining();
public final boolean hasRemaining() {
    //判斷position的索引是否小於limit。
    //所以可以看出limit的作用就是記錄寫入數據的位置,那麼當讀取數據時,就知道讀到哪個位置
	return position < limit;
}
複製代碼

接下來就是在while循環中get()讀取數據,讀取完之後。

NIO入門,看這篇文章就夠了

 

 

NIO入門,看這篇文章就夠了

 

 

最後當position等於limit時,循環判斷條件不成立,就跳出循環,讀取完畢。

所以可以看出實質上capacity容量大小是不變的,實際上是通過控制position和limit的值來控制讀寫的數據。

2.2 管道(Channel)

首先我們看一下Channel有哪些子類:

 

NIO入門,看這篇文章就夠了

 

 

常用的Channel有這四種:

FileChannel,讀寫文件中的數據。 SocketChannel,通過TCP讀寫網絡中的數據。 ServerSockectChannel,監聽新進來的TCP連接,像Web服務器那樣。對每一個新進來的連接都會創建一個SocketChannel。 DatagramChannel,通過UDP讀寫網絡中的數據。

Channel本身並不存儲數據,只是負責數據的運輸。必須要和Buffer一起使用。

2.2.1 獲取通道的方式

2.2.1.1 FileChannel

FileChannel的獲取方式,下面舉個文件複製拷貝的例子進行說明:

 

NIO入門,看這篇文章就夠了

 

 

首先準備一個"1.txt"放在項目的根目錄下,然後編寫一個main方法:

	public static void main(String[] args) throws Exception {
        //獲取文件輸入流
        File file = new File("1.txt");
        FileInputStream inputStream = new FileInputStream(file);
        //從文件輸入流獲取通道
        FileChannel inputStreamChannel = inputStream.getChannel();
        //獲取文件輸出流
        FileOutputStream outputStream = new FileOutputStream(new File("2.txt"));
        //從文件輸出流獲取通道
        FileChannel outputStreamChannel = outputStream.getChannel();
        //創建一個byteBuffer,小文件所以就直接一次讀取,不分多次循環了
        ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length());
        //把輸入流通道的數據讀取到緩衝區
        inputStreamChannel.read(byteBuffer);
        //切換成讀模式
        byteBuffer.flip();
        //把數據從緩衝區寫入到輸出流通道
        outputStreamChannel.write(byteBuffer);
        //關閉通道
        outputStream.close();
        inputStream.close();
        outputStreamChannel.close();
        inputStreamChannel.close();
    }
複製代碼

執行後,我們就獲得一個"2.txt"。執行成功。

 

NIO入門,看這篇文章就夠了

 

 

以上的例子,可以用一張示意圖表示,是這樣的:

 

NIO入門,看這篇文章就夠了

 

 

2.2.1.2 SocketChannel

接下來我們學習獲取SocketChannel的方式。

還是一樣,我們通過一個例子來快速上手:

	public static void main(String[] args) throws Exception {
        //獲取ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 6666);
        //綁定地址,端口號
        serverSocketChannel.bind(address);
        //創建一個緩衝區
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        while (true) {
            //獲取SocketChannel
            SocketChannel socketChannel = serverSocketChannel.accept();
            while (socketChannel.read(byteBuffer) != -1){
                //打印結果
                System.out.println(new String(byteBuffer.array()));
                //清空緩衝區
                byteBuffer.clear();
            }
        }
    }
複製代碼

然後運行main()方法,我們可以通過telnet命令進行連接測試:

 

NIO入門,看這篇文章就夠了

 

 

通過上面的例子可以知道,通過ServerSocketChannel.open()方法可以獲取服務器的通道,然後綁定一個地址端口號,接着accept()方法可獲得一個SocketChannel通道,也就是客戶端的連接通道。

最後配合使用Buffer進行讀寫即可。

這就是一個簡單的例子,實際上上面的例子是阻塞式的。要做到非阻塞還需要使用選擇器Selector。

2.3 選擇器(Selector)

Selector翻譯成選擇器,有些人也會翻譯成多路複用器,實際上指的是同一樣東西。

只有網絡IO纔會使用選擇器,文件IO是不需要使用的。

選擇器可以說是NIO的核心組件,它可以監聽通道的狀態,來實現異步非阻塞的IO。換句話說,也就是事件驅動。以此實現單線程管理多個Channel的目的。

 

NIO入門,看這篇文章就夠了

 

 

2.3.1 核心API

API方法名 作用 Selector.open() 打開一個選擇器。 select() 選擇一組鍵,其相應的通道已爲 I/O 操作準備就緒。 selectedKeys() 返回此選擇器的已選擇鍵集。

以上的API會在後面的例子用到,先有個印象。

3 NIO快速入門

3.1 文件IO

3.1.1 通道間的數據傳輸

這裏主要介紹兩個通道與通道之間數據傳輸的方式:

transferTo():把源通道的數據傳輸到目的通道中。

	public static void main(String[] args) throws Exception {
        //獲取文件輸入流
        File file = new File("1.txt");
        FileInputStream inputStream = new FileInputStream(file);
        //從文件輸入流獲取通道
        FileChannel inputStreamChannel = inputStream.getChannel();
        //獲取文件輸出流
        FileOutputStream outputStream = new FileOutputStream(new File("2.txt"));
        //從文件輸出流獲取通道
        FileChannel outputStreamChannel = outputStream.getChannel();
        //創建一個byteBuffer,小文件所以就直接一次讀取,不分多次循環了
        ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
        //把輸入流通道的數據讀取到輸出流的通道
        inputStreamChannel.transferTo(0, byteBuffer.limit(), outputStreamChannel);
        //關閉通道
        outputStream.close();
        inputStream.close();
        outputStreamChannel.close();
        inputStreamChannel.close();
    }	
複製代碼

transferFrom():把來自源通道的數據傳輸到目的通道。

	public static void main(String[] args) throws Exception {
        //獲取文件輸入流
        File file = new File("1.txt");
        FileInputStream inputStream = new FileInputStream(file);
        //從文件輸入流獲取通道
        FileChannel inputStreamChannel = inputStream.getChannel();
        //獲取文件輸出流
        FileOutputStream outputStream = new FileOutputStream(new File("2.txt"));
        //從文件輸出流獲取通道
        FileChannel outputStreamChannel = outputStream.getChannel();
        //創建一個byteBuffer,小文件所以就直接一次讀取,不分多次循環了
        ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
        //把輸入流通道的數據讀取到輸出流的通道
        outputStreamChannel.transferFrom(inputStreamChannel,0,byteBuffer.limit());
        //關閉通道
        outputStream.close();
        inputStream.close();
        outputStreamChannel.close();
        inputStreamChannel.close();
    }
複製代碼

3.1.2 分散讀取和聚合寫入

我們先看一下FileChannel的源碼:

public abstract class FileChannel extends AbstractInterruptibleChannel
    implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel {   
}
複製代碼

從源碼中可以看出實現了GatheringByteChannel, ScatteringByteChannel接口。也就是支持分散讀取和聚合寫入的操作。怎麼使用呢,請看以下例子:

我們寫一個main方法來實現複製1.txt文件,文件內容是:

abcdefghijklmnopqrstuvwxyz//26個字母
複製代碼

代碼如下:

	public static void main(String[] args) throws Exception {
        //獲取文件輸入流
        File file = new File("1.txt");
        FileInputStream inputStream = new FileInputStream(file);
        //從文件輸入流獲取通道
        FileChannel inputStreamChannel = inputStream.getChannel();
        //獲取文件輸出流
        FileOutputStream outputStream = new FileOutputStream(new File("2.txt"));
        //從文件輸出流獲取通道
        FileChannel outputStreamChannel = outputStream.getChannel();
        //創建三個緩衝區,分別都是5
        ByteBuffer byteBuffer1 = ByteBuffer.allocate(5);
        ByteBuffer byteBuffer2 = ByteBuffer.allocate(5);
        ByteBuffer byteBuffer3 = ByteBuffer.allocate(5);
        //創建一個緩衝區數組
        ByteBuffer[] buffers = new ByteBuffer[]{byteBuffer1, byteBuffer2, byteBuffer3};
        //循環寫入到buffers緩衝區數組中,分散讀取
        long read;
        long sumLength = 0;
        while ((read = inputStreamChannel.read(buffers)) != -1) {
            sumLength += read;
            Arrays.stream(buffers)
                    .map(buffer -> "posstion=" + buffer.position() + ",limit=" + buffer.limit())
                    .forEach(System.out::println);
            //切換模式
            Arrays.stream(buffers).forEach(Buffer::flip);
            //聚合寫入到文件輸出通道
            outputStreamChannel.write(buffers);
            //清空緩衝區
            Arrays.stream(buffers).forEach(Buffer::clear);
        }
        System.out.println("總長度:" + sumLength);
        //關閉通道
        outputStream.close();
        inputStream.close();
        outputStreamChannel.close();
        inputStreamChannel.close();
    }
複製代碼

打印結果:

posstion=5,limit=5
posstion=5,limit=5
posstion=5,limit=5

posstion=5,limit=5
posstion=5,limit=5
posstion=1,limit=5

總長度:26
複製代碼

可以看到循環了兩次。第一次循環時,三個緩衝區都讀取了5個字節,總共讀取了15,也就是讀滿了。還剩下11個字節,於是第二次循環時,前兩個緩衝區分配了5個字節,最後一個緩衝區給他分配了1個字節,剛好讀完。總共就是26個字節。

這就是分散讀取,聚合寫入的過程。

使用場景就是可以使用一個緩衝區數組,自動地根據需要去分配緩衝區的大小。可以減少內存消耗。網絡IO也可以使用,這裏就不寫例子演示了。

3.1.3 非直接/直接緩衝區

非直接緩衝區的創建方式:

static ByteBuffer allocate(int capacity)
複製代碼

直接緩衝區的創建方式:

static ByteBuffer allocateDirect(int capacity)
複製代碼

非直接/直接緩衝區的區別示意圖:

 

NIO入門,看這篇文章就夠了

 

 

NIO入門,看這篇文章就夠了

 

 

從示意圖中我們可以發現,最大的不同在於直接緩衝區不需要再把文件內容copy到物理內存中。這就大大地提高了性能。其實在介紹Buffer時,我們就有接觸到這個概念。直接緩衝區是堆外內存,在本地文件IO效率會更高一點。

接下來我們來對比一下效率,以一個136 MB的視頻文件爲例:

public static void main(String[] args) throws Exception {
    long starTime = System.currentTimeMillis();
    //獲取文件輸入流
    File file = new File("D:\\小電影.mp4");//文件大小136 MB
    FileInputStream inputStream = new FileInputStream(file);
    //從文件輸入流獲取通道
    FileChannel inputStreamChannel = inputStream.getChannel();
    //獲取文件輸出流
    FileOutputStream outputStream = new FileOutputStream(new File("D:\\test.mp4"));
    //從文件輸出流獲取通道
    FileChannel outputStreamChannel = outputStream.getChannel();
    //創建一個直接緩衝區
    ByteBuffer byteBuffer = ByteBuffer.allocateDirect(5 * 1024 * 1024);
    //創建一個非直接緩衝區
	//ByteBuffer byteBuffer = ByteBuffer.allocate(5 * 1024 * 1024);
    //寫入到緩衝區
    while (inputStreamChannel.read(byteBuffer) != -1) {
        //切換讀模式
        byteBuffer.flip();
        outputStreamChannel.write(byteBuffer);
        byteBuffer.clear();
    }
    //關閉通道
    outputStream.close();
    inputStream.close();
    outputStreamChannel.close();
    inputStreamChannel.close();
    long endTime = System.currentTimeMillis();
    System.out.println("消耗時間:" + (endTime - starTime) + "毫秒");
}
複製代碼

結果:

直接緩衝區的消耗時間:283毫秒

非直接緩衝區的消耗時間:487毫秒

3.2 網絡IO

其實NIO的主要用途是網絡IO,在NIO之前java要使用網絡編程就只有用Socket。而Socket是阻塞的,顯然對於高併發的場景是不適用的。所以NIO的出現就是解決了這個痛點。

主要思想是把Channel通道註冊到Selector中,通過Selector去監聽Channel中的事件狀態,這樣就不需要阻塞等待客戶端的連接,從主動等待客戶端的連接,變成了通過事件驅動。沒有監聽的事件,服務器可以做自己的事情。

3.2.1 使用Selector的小例子

接下來趁熱打鐵,我們來做一個服務器接受客戶端消息的例子:

首先服務端代碼:

public class NIOServer {
    public static void main(String[] args) throws Exception {
        //打開一個ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 6666);
        //綁定地址
        serverSocketChannel.bind(address);
        //設置爲非阻塞
        serverSocketChannel.configureBlocking(false);
        //打開一個選擇器
        Selector selector = Selector.open();
        //serverSocketChannel註冊到選擇器中,監聽連接事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //循環等待客戶端的連接
        while (true) {
            //等待3秒,(返回0相當於沒有事件)如果沒有事件,則跳過
            if (selector.select(3000) == 0) {
                System.out.println("服務器等待3秒,沒有連接");
                continue;
            }
            //如果有事件selector.select(3000)>0的情況,獲取事件
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            //獲取迭代器遍歷
            Iterator<SelectionKey> it = selectionKeys.iterator();
            while (it.hasNext()) {
                //獲取到事件
                SelectionKey selectionKey = it.next();
                //判斷如果是連接事件
                if (selectionKey.isAcceptable()) {
                    //服務器與客戶端建立連接,獲取socketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    //設置成非阻塞
                    socketChannel.configureBlocking(false);
                    //把socketChannel註冊到selector中,監聽讀事件,並綁定一個緩衝區
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
                //如果是讀事件
                if (selectionKey.isReadable()) {
                    //獲取通道
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    //獲取關聯的ByteBuffer
                    ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
                    //打印從客戶端獲取到的數據
                    socketChannel.read(buffer);
                    System.out.println("from 客戶端:" + new String(buffer.array()));
                }
                //從事件集合中刪除已處理的事件,防止重複處理
                it.remove();
            }
        }
    }
}
複製代碼

客戶端代碼:

public class NIOClient {
    public static void main(String[] args) throws Exception {
        SocketChannel socketChannel = SocketChannel.open();
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 6666);
        socketChannel.configureBlocking(false);
        //連接服務器
        boolean connect = socketChannel.connect(address);
        //判斷是否連接成功
        if(!connect){
            //等待連接的過程中
            while (!socketChannel.finishConnect()){
                System.out.println("連接服務器需要時間,期間可以做其他事情...");
            }
        }
        String msg = "hello java技術愛好者!";
        ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
        //把byteBuffer數據寫入到通道中
        socketChannel.write(byteBuffer);
        //讓程序卡在這個位置,不關閉連接
        System.in.read();
    }
}
複製代碼

接下來啓動服務端,然後再啓動客戶端,我們可以看到控制檯打印以下信息:

服務器等待3秒,沒有連接
服務器等待3秒,沒有連接
from 客戶端:hello java技術愛好者!                       
服務器等待3秒,沒有連接
服務器等待3秒,沒有連接
複製代碼

通過這個例子我們引出以下知識點。

3.2.2 SelectionKey

在SelectionKey類中有四個常量表示四種事件,來看源碼:

public abstract class SelectionKey {
    //讀事件
    public static final int OP_READ = 1 << 0; //2^0=1
    //寫事件
    public static final int OP_WRITE = 1 << 2; // 2^2=4
    //連接操作,Client端支持的一種操作
    public static final int OP_CONNECT = 1 << 3; // 2^3=8
    //連接可接受操作,僅ServerSocketChannel支持
    public static final int OP_ACCEPT = 1 << 4; // 2^4=16
}
複製代碼

附加的對象(可選),把通道註冊到選擇器中時可以附加一個對象。

public final SelectionKey register(Selector sel, int ops, Object att)
複製代碼

從selectionKey中獲取附件對象可以使用attachment()方法

public final Object attachment() {
    return attachment;
}
複製代碼

4 使用NIO實現多人聊天室

接下來進行一個實戰例子,用NIO實現一個多人運動版本的聊天室。

服務端代碼:

public class GroupChatServer {

    private Selector selector;

    private ServerSocketChannel serverSocketChannel;

    public static final int PORT = 6667;

    //構造器初始化成員變量
    public GroupChatServer() {
        try {
            //打開一個選擇器
            this.selector = Selector.open();
            //打開serverSocketChannel
            this.serverSocketChannel = ServerSocketChannel.open();
            //綁定地址,端口號
            this.serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", PORT));
            //設置爲非阻塞
            serverSocketChannel.configureBlocking(false);
            //把通道註冊到選擇器中
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 監聽,並且接受客戶端消息,轉發到其他客戶端
     */
    public void listen() {
        try {
            while (true) {
                //獲取監聽的事件總數
                int count = selector.select(2000);
                if (count > 0) {
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    //獲取SelectionKey集合
                    Iterator<SelectionKey> it = selectionKeys.iterator();
                    while (it.hasNext()) {
                        SelectionKey key = it.next();
                        //如果是獲取連接事件
                        if (key.isAcceptable()) {
                            SocketChannel socketChannel = serverSocketChannel.accept();
                            //設置爲非阻塞
                            socketChannel.configureBlocking(false);
                            //註冊到選擇器中
                            socketChannel.register(selector, SelectionKey.OP_READ);
                            System.out.println(socketChannel.getRemoteAddress() + "上線了~");
                        }
                        //如果是讀就緒事件
                        if (key.isReadable()) {
                            //讀取消息,並且轉發到其他客戶端
                            readData(key);
                        }
                        it.remove();
                    }
                } else {
                    System.out.println("等待...");
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
	
    //獲取客戶端發送過來的消息
    private void readData(SelectionKey selectionKey) {
        SocketChannel socketChannel = null;
        try {
            //從selectionKey中獲取channel
            socketChannel = (SocketChannel) selectionKey.channel();
            //創建一個緩衝區
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            //把通道的數據寫入到緩衝區
            int count = socketChannel.read(byteBuffer);
            //判斷返回的count是否大於0,大於0表示讀取到了數據
            if (count > 0) {
                //把緩衝區的byte[]轉成字符串
                String msg = new String(byteBuffer.array());
                //輸出該消息到控制檯
                System.out.println("from 客戶端:" + msg);
                //轉發到其他客戶端
                notifyAllClient(msg, socketChannel);
            }
        } catch (Exception e) {
            try {
                //打印離線的通知
                System.out.println(socketChannel.getRemoteAddress() + "離線了...");
                //取消註冊
                selectionKey.cancel();
                //關閉流
                socketChannel.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }

    /**
     * 轉發消息到其他客戶端
     * msg 消息
     * noNotifyChannel 不需要通知的Channel
     */
    private void notifyAllClient(String msg, SocketChannel noNotifyChannel) throws Exception {
        System.out.println("服務器轉發消息~");
        for (SelectionKey selectionKey : selector.keys()) {
            Channel channel = selectionKey.channel();
            //channel的類型實際類型是SocketChannel,並且排除不需要通知的通道
            if (channel instanceof SocketChannel && channel != noNotifyChannel) {
                //強轉成SocketChannel類型
                SocketChannel socketChannel = (SocketChannel) channel;
                //通過消息,包裹獲取一個緩衝區
                ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
                socketChannel.write(byteBuffer);
            }
        }
    }

    public static void main(String[] args) throws Exception {
        GroupChatServer chatServer = new GroupChatServer();
        //啓動服務器,監聽
        chatServer.listen();
    }
}
複製代碼

客戶端代碼:

public class GroupChatClinet {

    private Selector selector;

    private SocketChannel socketChannel;

    private String userName;

    public GroupChatClinet() {
        try {
            //打開選擇器
            this.selector = Selector.open();
            //連接服務器
            socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", GroupChatServer.PORT));
            //設置爲非阻塞
            socketChannel.configureBlocking(false);
            //註冊到選擇器中
            socketChannel.register(selector, SelectionKey.OP_READ);
            //獲取用戶名
            userName = socketChannel.getLocalAddress().toString().substring(1);
            System.out.println(userName + " is ok~");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
	
    //發送消息到服務端
    private void sendMsg(String msg) {
        msg = userName + "說:" + msg;
        try {
            socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
	
    //讀取服務端發送過來的消息
    private void readMsg() {
        try {
            int count = selector.select();
            if (count > 0) {
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    //判斷是讀就緒事件
                    if (selectionKey.isReadable()) {
                        SocketChannel channel = (SocketChannel) selectionKey.channel();
                        //創建一個緩衝區
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        //從服務器的通道中讀取數據到緩衝區
                        channel.read(byteBuffer);
                        //緩衝區的數據,轉成字符串,並打印
                        System.out.println(new String(byteBuffer.array()));
                    }
                    iterator.remove();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        GroupChatClinet chatClinet = new GroupChatClinet();
        //啓動線程,讀取服務器轉發過來的消息
        new Thread(() -> {
            while (true) {
                chatClinet.readMsg();
                try {
                    Thread.sleep(3000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
        //主線程發送消息到服務器
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()) {
            String msg = scanner.nextLine();
            chatClinet.sendMsg(msg);
        }
    }
}
複製代碼

先啓動服務端的main方法,再啓動兩個客戶端的main方法:

NIO入門,看這篇文章就夠了

 

然後使用兩個客戶端開始聊天了~

 

NIO入門,看這篇文章就夠了

 

 

NIO入門,看這篇文章就夠了

 

 

以上就是使用NIO實現多人聊天室的例子,同學們可以看着我這個例子自己完成一下。要多寫代碼纔好理解這些概念。

 

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