Netty-BIO、NIO、AIO、零拷貝-2

Java BIO 編程

一、I/O 模型

1、I/O 模型簡單的理解:就是用什麼樣的通道進行數據的發送和接收,很大程度上決定了程序通信的性能

2、Java 共支持 3 種網絡編程模型/IO 模式:BIO、NIO、AIO

3、Java BIO : 同步並阻塞(傳統阻塞型),服務器實現模式爲一個連接一個線程,即客戶端有連接請求時服務器端就需要啓動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷

4、Java NIO :同步非阻塞,服務器實現模式爲一個線程處理多個請求(連接),即客戶端發送的連接請求都會註冊到多路複用器上,多路複用器輪詢到連接有 I/O 請求就進行處理

5、Java AIO(NIO.2) : 異步非阻塞,AIO 引入異步通道的概念,採用了 Proactor 模式,簡化了程序編寫,有效的請求才啓動線程,它的特點是先由操作系統完成後才通知服務端程序啓動線程去處理,一般適用於連接數較多且連接時間較長的應用

二、BIO、NIO、AIO 適用場景

BIO :特點:同步並阻塞;使用場景:一個連接對應一個線程 2.線程開銷大 連接數目比較小且固定的架構,服務器資源要求比較高,程序簡單易理解
NIO :特點:同步非阻塞;使用場景:一個線程處理多個請求(連接) 2.多路複用器輪詢到連接有 I/O 請求 連接數目多且連接比較短(輕操作),比如聊天服務器,彈幕系統,服務器間通訊等。編程比較複雜
AIO :特點:異步非阻塞;使用場景:採用了 Proactor 模式 連接數較多且連接時間較長,比如相冊服務器,充分調用 OS 參與併發操作,編程比較複雜

三、Java BIO 基本介紹

  1. Java BIO 就是傳統的 java io 編程,其相關的類和接口在 java.io
  2. BIO(blocking I/O): 同步阻塞,服務器實現模式爲一個連接一個線程,即客戶端有連接請求時服務器端就需要啓動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷,可以通過線程池機制改善(實現多個客戶連接服務器)。
  3. BIO 方式適用於連接數目比較小且固定的架構,這種方式對服務器資源要求比較高,併發侷限於應用中,JDK1.4以前的唯一選擇,程序簡單易理解

四、Java BIO 工作機制

BIO 編程流程的梳理:

  1. 服務器端啓動一個 ServerSocket

  2. 客戶端啓動 Socket 對服務器進行通信,默認情況下服務器端需要對每個客戶建立一個線程與之通訊

  3. 客戶端發出請求後, 先諮詢服務器是否有線程響應,如果沒有則會等待,或者被拒絕

  4. 如果有響應,客戶端線程會等待請求結束後,在繼續執行

五、Java BIO 應用實例

實例說明:

  1. 使用 BIO 模型編寫一個服務器端,監聽 6666 端口,當有客戶端連接時,就啓動一個線程與之通訊。
  2. 要求使用線程池機制改善,可以連接多個客戶端.
  3. 服務器端可以接收客戶端發送的數據(telnet 方式即可)。
package com.sun.bio;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @Author: sunguoqiang
 * @Description: TODO
 * @DateTime: 2022/12/20 10:49
 **/
public class BioServer {

    public static void main(String[] args) throws Exception {
        // 1、創建線程池,爲多個客戶端提供線程處理請求
        ExecutorService executorService = Executors.newCachedThreadPool();
        // 2、創建socket服務器,綁定端口9999
        ServerSocket serverSocket = new ServerSocket(9999);
        // 3、循環接收客戶端請求
        while (true) {
            // 4、接收客戶端請求,獲取請求的客戶端
            Socket acceptSocket = serverSocket.accept();
            // 5、線程池處理請求
            executorService.submit(() -> {
                handle(acceptSocket);
            });
        }
    }

    // 6、處理請求的方法
    private static void handle(Socket socket) {
        System.out.println("處理當前請求的線程ID:" + Thread.currentThread().getId());
        InputStream inputStream = null;
        byte[] bytes = new byte[1024];
        try {
            inputStream = socket.getInputStream();
            while (true) {
                int read = inputStream.read(bytes);
                if (read != -1) {
                    System.out.println("線程ID[" + Thread.currentThread().getId() + "]接收到的數據:" + new String(bytes, 0, read));
                } else {
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                inputStream.close();
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}
View Code

六、Java BIO 問題分析

  1. 每個請求都需要創建獨立的線程,與對應的客戶端進行數據 Read,業務處理,數據 Write 。
  2. 當併發數較大時,需要創建大量線程來處理連接,系統資源佔用較大。
  3. 連接建立後,如果當前線程暫時沒有數據可讀,則線程就阻塞在 Read 操作上,造成線程資源浪費

Java NIO 編程

一、Java NIO 基本介紹

Java NIO 全稱 java non-blocking IO,是指 JDK 提供的新 API。從 JDK1.4 開始,Java 提供了一系列改進的輸入/輸出的新特性,被統稱爲 NIO(即 New IO),是同步非阻塞的

NIO 相關類都被放在 java.nio 包及子包下,並且對原 java.io 包中的很多類進行改寫。

NIO 有三大核心部分:Channel( 通道),Buffer( 緩衝區), Selector( 選擇器)

NIO 是區面向緩衝區,向或者面向塊編程的。數據讀取到一個它稍後處理的緩衝區,需要時可在緩衝區中前後移動,這就增加了處理過程中的靈活性,使用它可以提供非阻塞式的高伸縮性網絡

Java NIO 的非阻塞模式,使一個線程從某通道發送請求或者讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什麼都不會獲取,而不是保持線程阻塞,所以直至數據變的可以讀取之前,該線程可以繼續做其他的事情。非阻塞寫也是如此,一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。

通俗理解:NIO 是可以做到用一個線程來處理多個操作的。 假設有 10000 個請求過來,根據實際情況,可以分配50 或者 100 個線程來處理。不像之前的阻塞 IO 那樣,非得分配 10000 個。

HTTP2.0 使用了多路複用的技術,做到同一個連接併發處理多個請求,而且併發請求的數量比 HTTP1.1 大了好幾個數量級

案例說明 NIO 的 Buffer

package com.sun.netty.Buffer;

import java.nio.IntBuffer;

/**
 * @Author: sunguoqiang
 * @Description: TODO
 * @DateTime: 2022/12/20 11:29
 **/
public class BasicBuffer {
    public static void main(String[] args) {
        // 1、創建一個存儲int類型數據的Buffer,存儲容量爲5
        IntBuffer intBuffer = IntBuffer.allocate(5);
        // 2、向Buffer中添加數據
        for (int i = 0; i < intBuffer.capacity(); i++) {
            intBuffer.put(i*2);
        }
        // 3、如何從Buffer中讀取數據?
        // 3.1、讀寫轉換操作,必須做
        intBuffer.flip();
        // 4、正式讀取
        while(intBuffer.hasRemaining()){
            System.out.println(intBuffer.get());
        }
    }
}
View Code

二、NIO 和 BIO 的比較

1、BIO 以流的方式處理數據,而 NIO 以塊的方式處理數據,塊 I/O 的效率比流 I/O 高很多
2、BIO 是阻塞的,NIO 則是非阻塞的
3、BIO 基於字節流和字符流進行操作,而 NIO 基於 Channel(通道)和 Buffer(緩衝區)進行操作,數據總是從通道讀取到緩衝區中,或者從緩衝區寫入到通道中。Selector(選擇器)用於監聽多個通道的事件(比如:連接請求,數據到達等),因此使用單個線程就可以        監聽多個客戶端通道

三、NIO 三大核心原理示意圖

關係圖的說明:

  1. 每個 channel 都會對應一個 Buffer
  2. Selector 對應一個線程, 一個線程對應多個 channel(連接)
  3. 該圖反應了有三個 channel 註冊到 該 selector //程序
  4. 程序切換到哪個 channel 是有事件決定的, Event 就是一個重要的概念
  5. Selector 會根據不同的事件,在各個通道上切換
  6. Buffer 就是一個內存塊 , 底層是有一個數組
  7. 數據的讀取寫入是通過 Buffer, 這個和 BIO , BIO 中要麼是輸入流,或者是輸出流, 不能雙向,但是 NIO 的 Buffer 是可以讀也可以寫, 需要 flip 方法切換channel 是雙向的, 可以返回底層操作系統的情況, 比如 Linux , 底層的操作系統通道就是雙向的.

四、緩衝區(Buffer)

1、基本介紹

緩衝區(Buffer):

  緩衝區本質上是一個可以讀寫數據的內存塊,可以理解成是一個容器對象( 含數組),該對象提供了一組方法,可以更輕鬆地使用內存塊,,緩衝區對象內置了一些機制,能夠跟蹤和記錄緩衝區的狀態變化情況。Channel 提供從文件、網絡讀取數據的渠道,但是讀取或寫入的數據都必須經由 Buffer.

2、Buffer 類及其子類

1、在 NIO 中,Buffer 是一個頂層父類,它是一個抽象類, 類的層級關係圖:

2、Buffer 類定義了所有的緩衝區都具有的四個屬性來提供關於其所包含的數據元素的信息:

3、Buffer 類相關方法一覽

3、ByteBuffer

 從前面可以看出對於 Java 中的基本數據類型(boolean 除外),都有一個 Buffer 類型與之相對應,最常用的自然是 ByteBuffer 類(二進制數據),該類的主要方法如下:

五、通道(Channel)

1、基本介紹

1、NIO 的通道類似於流,但有些區別如下:

  • 通道可以同時進行讀寫,而流只能讀或者只能寫
  • 通道可以實現異步讀寫數據
  • 通道可以從緩衝讀數據,也可以寫數據到緩衝:

2、BIO 中的 stream 是單向的,例如 FileInputStream 對象只能進行讀取數據的操作,而 NIO 中的通道(Channel)是雙向的,可以讀操作,也可以寫操作。

3、Channel 在 NIO 中是一個接口 public interface Channel extends Closeable{}

4、常用的Channel 類有 :

  1. FileChannel
  2. DatagramChannel
  3. ServerSocketChannel
  4. SocketChannel

【ServerSocketChanne 類似 ServerSocket , SocketChannel 類似 Socket】

5、FileChannel 用於文件的數據讀寫,DatagramChannel 用於 UDP 的數據讀寫,ServerSocketChannel 和SocketChannel 用於 TCP 的數據讀寫。

6、圖示

2、FileChannel 類

FileChannel 主要用來對本地文件進行 IO 操作,常見的方法有

public int read(ByteBuffer dst) ,從通道讀取數據並放到緩衝區中
public int write(ByteBuffer src) ,把緩衝區的數據寫到通道中
public long transferFrom(ReadableByteChannel src, long position, long count),從目標通道中複製數據到當前通道
public long transferTo(long position, long count, WritableByteChannel target),把數據從當前通道複製給目標通道

3、應用實例 1-本地文件寫數據

package com.sun.netty.Buffer;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;

/**
 * @Author: sunguoqiang
 * @Description: TODO
 * @DateTime: 2022/12/21 9:30
 **/
public class FileChannel01 {

    public static void main(String[] args) throws Exception {
        String str="hello,你好!";
        // 1、創建文件輸出流
        FileOutputStream fileOutputStream = new FileOutputStream("1.txt");
        // 2、根據文件輸出流獲取channel
        FileChannel channel = fileOutputStream.getChannel();
        // 3、定義Buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        // 4、將數據放入緩衝區Buffer
        byteBuffer.put(str.getBytes(StandardCharsets.UTF_8));
        // 5、切記!!!Buffer讀寫轉換
        byteBuffer.flip();
        // 5、將緩衝區數據寫入管道channel
        channel.write(byteBuffer);
        // 6、關閉資源
        fileOutputStream.close();
    }
}
View Code

4、應用實例 2-本地文件讀數據

package com.sun.netty.Buffer;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * @Author: sunguoqiang
 * @Description: TODO
 * @DateTime: 2022/12/21 9:44
 **/
public class FileChannel02 {

    public static void main(String[] args) throws Exception {
        // 1、獲取文件輸入流
        FileInputStream fileInputStream = new FileInputStream("1.txt");
        // 2、通過文件輸入流獲取channel
        FileChannel channel = fileInputStream.getChannel();
        // 3、創建Buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        // 4、將文件從管道讀取到緩存中
        int read = channel.read(byteBuffer);
        // 5、輸出讀取文本
        System.out.println(new String(byteBuffer.array(),0,read));
        // 6、關閉資源
        fileInputStream.close();
    }
}
View Code

5、應用實例 3-使用一個 Buffer 完成文件讀取、寫入

package com.sun.netty.Buffer;

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

/**
 * @Author: sunguoqiang
 * @Description: TODO
 * @DateTime: 2022/12/21 9:53
 **/
public class FileChannel03 {

    public static void main(String[] args) throws Exception {
        // 1、獲取文件輸入流及channel
        FileInputStream fileInputStream = new FileInputStream("th.jpg");
        FileChannel inputStreamChannel = fileInputStream.getChannel();

        // 2、獲取文件輸出流及channel
        FileOutputStream fileOutputStream = new FileOutputStream("th_copy.jpg");
        FileChannel outputStreamChannel = fileOutputStream.getChannel();

        // 3、獲取緩衝區
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        // 4、循環讀取文件並保存流內容到目標文件
        while (true) {
            // 5、切記此步驟,漏寫則會執行不成功
            byteBuffer.clear();
            int read = inputStreamChannel.read(byteBuffer);
            if (read == -1) {
                break;
            }
            // 6、切記!!!讀寫轉換
            byteBuffer.flip();
            outputStreamChannel.write(byteBuffer);
        }
        outputStreamChannel.close();
        inputStreamChannel.close();
    }
}
View Code

6、應用實例 4-拷貝文件 transferFrom 、transferTo方法

package com.sun.netty.Buffer;

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

/**
 * @Author: sunguoqiang
 * @Description: TODO
 * @DateTime: 2022/12/21 9:53
 **/
public class FileChannel04 {

    public static void main(String[] args) throws Exception {
        // 1、獲取文件輸入流及channel
        FileInputStream fileInputStream = new FileInputStream("th.jpg");
        FileChannel inputStreamChannel = fileInputStream.getChannel();

        // 2、獲取文件輸出流及channel
        FileOutputStream fileOutputStream = new FileOutputStream("th_copy2.jpg");
        FileChannel outputStreamChannel = fileOutputStream.getChannel();

        // 3、獲取緩衝區
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        // 4、此處有兩種方式

        // 4.1 transferFrom()
        // outputStreamChannel.transferFrom(inputStreamChannel,0, inputStreamChannel.size());
        // 4.2 transferTo()
        inputStreamChannel.transferTo(0, inputStreamChannel.size(), outputStreamChannel);

        // 5、關閉資源
        outputStreamChannel.close();
        inputStreamChannel.close();
    }
}
View Code

7、關於 Buffer 和 Channel 的注意事項和細節

1、存入、讀取類型

ByteBuffer 支持類型化的 put 和 get, put 放入的是什麼數據類型,get 就應該使用相應的數據類型來取出,否則可能有 BufferUnderflowException 異常

package com.sun.netty.Buffer;

import java.nio.ByteBuffer;

/**
 * @Author: sunguoqiang
 * @Description: TODO
 * @DateTime: 2022/12/21 11:04
 **/
public class NIOBufferPutGet {

    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(64
        );
        //類型化方式放入數據
        buffer.putInt(100);
        buffer.putLong(9);
        buffer.putChar('強');
        buffer.putShort((short) 4);

        //取出,順序與放入的順序一致,求類型一致
        buffer.flip();
        System.out.println(buffer.getInt());
        System.out.println(buffer.getLong());
        System.out.println(buffer.getChar());
        System.out.println(buffer.getShort());
    }

}
View Code

2、可以將一個普通 Buffer 轉成只讀 Buffer

package com.sun.netty.Buffer;

import java.nio.ByteBuffer;

/**
 * @Author: sunguoqiang
 * @Description: TODO
 * @DateTime: 2022/12/21 11:07
 **/
public class ReadOnlyBuffer {

    public static void main(String[] args) {
        // 1、創建一個 buffer
        ByteBuffer buffer = ByteBuffer.allocate(64);
        for (int i = 0; i < 64; i++) { //給其放入0-63個數字
            buffer.put((byte) i);
        }
        // 2、讀寫轉換
        buffer.flip();

        // 3、得到一個只讀的 Buffer
        ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
        System.out.println(readOnlyBuffer.getClass()); // class java.nio.HeapByteBufferR

        // 4、讀取
        while (readOnlyBuffer.hasRemaining()) { // 判斷是否還有數據
            System.out.println(readOnlyBuffer.get()); // 取出,並給position+1
        }

        // 5、測試只能讀取,不能在put寫入
        readOnlyBuffer.put((byte) 100); // ReadOnlyBufferException
    }
}
View Code

3、NIO 還提供了 MappedByteBuffer, 可以讓文件直接在內存(堆外的內存)中進行修改, 而如何同步到文件由 NIO 來完成

   /*
    說明
    1. MappedByteBuffer 可讓文件直接在內存(堆外內存)修改, 操作系統不需要拷貝一次
    */
public class MappedByteBufferTest {
    public static void main(String[] args) throws Exception {
        RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");
        
        //獲取對應的通道
        FileChannel channel = randomAccessFile.getChannel();
        /**
        * 參數1: FileChannel.MapMode.READ_WRITE 使用的讀寫模式
        * 參數2: 0 : 可以直接修改的起始位置,字節位置
        * 參數3: 5: 是映射到內存的大小(不是索引位置) ,即將 1.txt 的多少個字節映射到內存
        * 可以直接修改的範圍就是 0-5
        * 實際類型 DirectByteBuffer
        */
        MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
        mappedByteBuffer.put(0, (byte) 'H');
        mappedByteBuffer.put(3, (byte) '9');
        mappedByteBuffer.put(5, (byte) 'Y');//IndexOutOfBoundsException
        
        
        //關閉資源
        randomAccessFile.close();
        System.out.println("修改成功~~");
    }
}
View Code

4、NIO 還支持 通過多個 Buffer (即 Buffer 數組) 完成讀寫操作,即 Scattering 和Gathering

/**
* Scattering:將數據寫入到 buffer 時,可以採用 buffer 數組,依次寫入 [分散]
* Gathering: 從 buffer 讀取數據時,可以採用 buffer 數組,依次讀
*/
public class ScatteringAndGatheringTest {
    public static void main(String[] args) throws Exception {
        //使用 ServerSocketChannel 和 SocketChannel 網絡
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
        //綁定端口到 socket ,並啓動
        serverSocketChannel.socket().bind(inetSocketAddress);
        
        //創建 buffer 數組
        ByteBuffer[] byteBuffers = new ByteBuffer[2];
        byteBuffers[0] = ByteBuffer.allocate(5);
        byteBuffers[1] = ByteBuffer.allocate(3);
        
        //等客戶端連接(telnet)
        SocketChannel socketChannel = serverSocketChannel.accept();
        int messageLength = 8; //假定從客戶端接收 8 個字節
        //循環的讀取
        while (true) {
            
            int byteRead = 0;
            while (byteRead < messageLength ) {
                long l = socketChannel.read(byteBuffers);
                byteRead += l; //累計讀取的字節數
                System.out.println("byteRead=" + byteRead);
                //使用流打印, 看看當前的這個 buffer 的 position 和 limit
                Arrays.asList(byteBuffers).stream().map(buffer -> "postion=" + buffer.position() + ", limit=" + buffer.limit()).forEach(System.out::println);
            }
            //將所有的 buffer 進行 flip
            Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip());
            
            
           
            //將數據讀出顯示到客戶端
            long byteWirte = 0;
            while (byteWirte < messageLength) {
                long l = socketChannel.write(byteBuffers); //
                byteWirte += l;
            }
            //將所有的 buffer 進行 clear
            Arrays.asList(byteBuffers).forEach(buffer-> {
                buffer.clear();
            });
            
            
            System.out.println("byteRead:=" + byteRead + " byteWrite=" + byteWirte + ", messagelength" + messageLength);
        }
    }
}
View Code

六、Selector(選擇器)

1、基本介紹

1、Java 的 NIO,用非阻塞的 IO 方式。可以用一個線程,處理多個的客戶端連接,就會使用到 Selector(選擇器)
2、Selector 能夠檢測多個註冊的通道上是否有事件發生(注意:多個 Channel 以事件的方式可以註冊到同一個Selector),如果有事件發生,便獲取事件然後針對每個事件進行相應的處理。這樣就可以只用一個單線程去管理多個通道,也就是管理多個連接和請求。
3、只有在 連接/通道 真正有讀寫事件發生時,纔會進行讀寫,就大大地減少了系統開銷,並且不必爲每個連接都創建一個線程,不用去維護多個線程
4、避免了多線程之間的上下文切換導致的開銷

  1. Netty 的 IO 線程 NioEventLoop 聚合了 Selector(選擇器,也叫多路複用器),可以同時併發處理成百上千個客戶端連接。
  2. 當線程從某客戶端 Socket 通道進行讀寫數據時,若沒有數據可用時,該線程可以進行其他任務。
  3. 線程通常將非阻塞 IO 的空閒時間用於在其他通道上執行 IO 操作,所以單獨的線程可以管理多個輸入和輸出通道。
  4. 由於讀寫操作都是非阻塞的,這就可以充分提升 IO 線程的運行效率,避免由於頻繁 I/O 阻塞導致的線程掛起。
  5. 一個 I/O 線程可以併發處理 N 個客戶端連接和讀寫操作,這從根本上解決了傳統同步阻塞 I/O 一連接一線程模型,架構的性能、彈性伸縮能力和可靠性都得到了極大的提升。

2、 Selector 類相關方法

Selector 類是一個抽象類, 常用方法和說明如下:

3、注意事項

1、NIO 中的 ServerSocketChannel 功能類似 ServerSocket,SocketChannel 功能類似 Socket
2、Selector 相關方法說明

  1. selector.select()//阻塞
  2. selector.select(1000);//阻塞 1000 毫秒,在 1000 毫秒後返回
  3. selector.wakeup();//喚醒 selector
  4. selector.selectNow();//不阻塞,立馬返還

4、NIO 非阻塞 網絡編程原理分析圖

對上圖的說明:

  1. 當客戶端連接時,會通過 ServerSocketChannel 得到 SocketChannel
  2. Selector 進行監聽 select 方法, 返回有事件發生的通道的個數.
  3. 將 socketChannel 註冊到 Selector 上, register(Selector sel, int ops), 一個 selector 上可以註冊多個 SocketChannel
  4. 註冊後返回一個 SelectionKey, 會和該 Selector 關聯(集合)
  5. 進一步得到各個 SelectionKey (有事件發生)
  6. 在通過 SelectionKey 反向獲取 SocketChannel , 方法 channel()
  7. 可以通過 得到的 channel , 完成業務處理

代碼:

NIOServer:服務器

package com.sun.netty.Selector;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

/**
 * @Author: sunguoqiang
 * @Description: TODO
 * @DateTime: 2022/12/21 17:06
 **/
public class NIOServer {

    public static void main(String[] args) throws Exception {
        // 1、創建ServerSocketChannel對象
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 2、創建Selector對象
        Selector selector = Selector.open();
        // 3、綁定端口
        serverSocketChannel.bind(new InetSocketAddress(8989));
        // 4、配置ServerSocketChannel爲非阻塞
        serverSocketChannel.configureBlocking(false);
        // 5、綁定ServerSocketChannel到Selector
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        // 6、循環接收客戶端消息
        while (true) {
            if (selector.select(60000) == 0) {
                System.out.println("等待60秒鐘無連接...");
                continue;
            }
            // 如果返回的>0, 就獲取到相關的 selectionKey 集合
            // 1.如果返回的>0, 表示已經獲取到關注的事件
            // 2. selector.selectedKeys() 返回關注事件的集合
            // 通過 selectionKeys 反向獲取通道
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            // 遍歷 Set<SelectionKey>, 使用迭代器遍歷
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                // 獲取到 SelectionKey
                SelectionKey selectionKey = iterator.next();
                // 根據 key 對應的通道發生的事件做相應處理
                if (selectionKey.isAcceptable()) { // 如果是 OP_ACCEPT, 有新的客戶端連接
                    // 該客戶端生成一個 SocketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    System.out.println("服務器端連接上一個請求:" + socketChannel.hashCode());
                    // 將 SocketChannel 設置爲非阻塞
                    socketChannel.configureBlocking(false);
                    // 將 socketChannel 註冊到 selector, 關注事件爲 OP_READ, 同時給 socketChannel
                    // 關聯一個 Buffer
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
                if (selectionKey.isReadable()) { // 發生 OP_READ
                    // 通過 key 反向獲取到對應 channel
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    // 獲取到該 channel 關聯的 buffer
                    ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
                    buffer.clear();
                    socketChannel.read(buffer);
                    if (buffer.capacity() - buffer.remaining() == 0) {
                        System.out.println("客戶端:" + socketChannel.hashCode() + "斷開連接...");
                    } else {
                        System.out.println("來自客戶端" + socketChannel.hashCode() + "的消息:" + new String(buffer.array(), 0, buffer.capacity() - buffer.remaining()));
                    }
                }
                // 手動從集合中移動當前的 selectionKey, 防止重複操作
                iterator.remove();
            }
        }
    }
}

NIOClient:客戶端

package com.sun.netty.Selector;

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

/**
 * @Author: sunguoqiang
 * @Description: TODO
 * @DateTime: 2022/12/21 17:57
 **/
public class NIOClient {

    public static void main(String[] args) throws Exception {
        //得到一個網絡通道
        SocketChannel socketChannel = SocketChannel.open();
        //設置非阻塞
        socketChannel.configureBlocking(false);
        //提供服務器端的 ip 和 端口
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 8989);
        //連接服務器
        if (!socketChannel.connect(inetSocketAddress)) {
            while (!socketChannel.finishConnect()) {
                System.out.println("因爲連接需要時間,客戶端不會阻塞,可以做其它工作..");
            }
        }
        //...如果連接成功,就發送數據
        String str = "hello, 阿昌~";
        //Wraps a byte array into a buffer
        ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
        //發送數據,將 buffer 數據寫入 channel
        socketChannel.write(buffer);
        System.in.read();
    }

}

5、 SelectionKey

1、SelectionKey,表示 Selector 和網絡通道的註冊關係

int OP_ACCEPT:有新的網絡連接可以 accept,值爲 16

int OP_CONNECT:代表連接已經建立,值爲 8

int OP_READ:代表讀操作,值爲 1

int OP_WRITE:代表寫操作,值爲 4

源碼中:

public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;

2、SelectionKey 相關方法

6、 ServerSocketChannel

ServerSocketChannel 在服務器端監聽新的客戶端 Socket 連接

專門負責監聽新的客戶端,獲取對應的SocketChannel

7、SocketChannel

SocketChannel,網絡 IO 通道,具體負責進行讀寫操作。

NIO 把緩衝區的數據寫入通道,或者把通道里的數據讀到緩衝區。

8、 NIO 網絡編程應用實例-羣聊系統

實例要求:

1、編寫一個 NIO 羣聊系統,實現服務器端和客戶端之間的數據簡單通訊(非阻塞)
2、實現多人羣聊
3、服務器端:可以監測用戶上線,離線,並實現消息轉發功能
4、客戶端:通過 channel 可以無阻塞發送消息給其它所有用戶,同時可以接受其它用戶發送的消息(有服務器轉發得到)
5、目的:進一步理解 NIO 非阻塞網絡編程機制

 

代碼:

服務器端

package com.sun.netty.GroupChat;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;

/**
 * 羣聊服務器端
 * 1、消息接收、轉發
 * 2、上線、離線提醒
 */
public class GroupChatServer {

    // 定義全局變量
    private ServerSocketChannel serverSocketChannel;
    private Selector selector;
    private static final int PORT = 9999;

    // 構造方法初始化
    public GroupChatServer() {
        try {
            serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.bind(new InetSocketAddress(PORT));
            selector = Selector.open();
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 監聽方法
    public void listen() {
        try {
            while (true) {
                int selectCount = selector.select();
                if (selectCount > 0) {
                    Iterator<SelectionKey> selectionKeyIterator = selector.selectedKeys().iterator();
                    while (selectionKeyIterator.hasNext()) {
                        SelectionKey selectionKey = selectionKeyIterator.next();
                        if (selectionKey.isAcceptable()) {
                            SocketChannel socketChannel = serverSocketChannel.accept();
                            socketChannel.configureBlocking(false);
                            socketChannel.register(selector, SelectionKey.OP_READ);
                            System.out.println(socketChannel.getRemoteAddress() + ":已上線...");
                        }
                        if (selectionKey.isReadable()) {
                            readData(selectionKey);
                        }
                    }
                    // 注意點!!!
                    selectionKeyIterator.remove();
                } else {
                    System.out.println("服務端等待連接...");
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 省略
        }

    }

    private void readData(SelectionKey selectionKey) {
        SocketChannel socketChannel = null;
        try {
            socketChannel = (SocketChannel) selectionKey.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int read = socketChannel.read(buffer);
            if (read > 0) {
                String msg = new String(buffer.array());
                System.out.println("來自客戶端[" + socketChannel.getRemoteAddress() + "]:" + msg);
                transferToOtherClient(selectionKey, msg);
            }
        } catch (IOException e) {
            try {
                System.out.println("客戶端[" + socketChannel.getRemoteAddress() + "]:離線了...");
                selectionKey.cancel();
                socketChannel.close();
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
        }
    }

    private void transferToOtherClient(SelectionKey selectionKey, String msg) throws IOException {
        System.out.println("服務器正在轉發消息...");
        Iterator<SelectionKey> iterator = selector.keys().iterator();
        SocketChannel senderChannel = (SocketChannel) selectionKey.channel();
        while (iterator.hasNext()) {
            SelectionKey key = iterator.next();
            Channel channel = key.channel();
            if (channel instanceof SocketChannel && key != selectionKey) {
                SocketChannel receiveSocketChannel = (SocketChannel) key.channel();
                ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
                receiveSocketChannel.write(byteBuffer);
            }
        }
    }


    public static void main(String[] args) {
        GroupChatServer groupChatServer = new GroupChatServer();
        groupChatServer.listen();
    }

}

客戶端

package com.sun.netty.GroupChat;

import java.io.IOException;
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;
import java.util.Scanner;

public class GroupChatClient {
    private static final String ip = "127.0.0.1";
    private static final int PORT = 9999;
    private SocketChannel channel;
    private Selector selector;
    private String username;

    public GroupChatClient() {
        try {
            channel = SocketChannel.open(new InetSocketAddress(ip, PORT));
            channel.configureBlocking(false);
            selector = Selector.open();
            channel.register(selector, SelectionKey.OP_READ);
            username = channel.getLocalAddress().toString();
            System.out.println(username + ":上線成功...");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void sendMsg(String msg) {
        // 注意點!!!
        msg = username + ":" + msg;
        try {
            channel.write(ByteBuffer.wrap(msg.getBytes()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void getMsg() {
        try {
            int selectCount = selector.select();
            if (selectCount > 0) {
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    if (key.isReadable()) {
                        SocketChannel channel = (SocketChannel) key.channel();
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        channel.read(byteBuffer);
                        System.out.println(new String(byteBuffer.array()).trim());
                    }
                }
                iterator.remove();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    public static void main(String[] args) {
        GroupChatClient client = new GroupChatClient();

        new Thread(() -> {
            while (true) {
                client.getMsg();
                try {
                    Thread.currentThread().sleep(3000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();

        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()) {
            String next = scanner.nextLine();
            client.sendMsg(next);
        }
    }


}

NIO 與零拷貝

涉及計算機系統底層,如果想要詳細瞭解請點擊此處查看相關pdf。零拷貝原理

JavaAIO 基本介紹

1、JDK 7 引入了 Asynchronous I/O,即 AIO。在進行 I/O 編程中,常用到兩種模式:Reactor 和 Proactor。
2、Java 的NIO 就是 Reactor,當有事件觸發時,服務器端得到通知,進行相應的處理
3、AIO 即 NIO2.0,叫做異步不阻塞的 IO。
4、AIO 引入異步通道的概念,採用了 Proactor 模式,簡化了程序編寫,有效的請求才啓動線程,它的特點是先由操作系統完成後才通知服務端程序啓動線程去處理,一般適用於連接數較多且連接時間較長的應用 目前 AIO 還沒有廣泛應用,Netty 也是基於 NIO,

     而不是 AIO, 因此我們就不詳解 AIO 了
5、有興趣可參考 <<Java 新 一 代 網 絡 編 程 模 型 AIO 原 理 及 Linux 系 統 AIO 介 紹 >> http://www.52im.net/thread-306-1-1.html

BIO、NIO、AIO 對比表

 

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