網絡IO模型(BIO,NIO,AIO)

網絡IO模型

I/O 模型簡單的理解:就是用什麼樣的通道進行數據的發送和接收,很大程度上決定了程序通信的性能.Java共支持3種網絡編程模型/IO模式:BIO、NIO、AIO

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

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

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

BIO、NIO、AIO適用場景分析

BIO方式適用於連接數目比較小且固定的架構,這種方式對服務器資源要求比較高,併發侷限於應用中,JDK1.4以前的唯一選擇,但程序簡單易理解。

NIO方式適用於連接數目多且連接比較短(輕操作)的架構,比如聊天服務器,彈幕系統,服務器間通訊等。編程比較複雜,JDK1.4開始支持。

AIO方式使用於連接數目多且連接比較長(重操作)的架構,比如相冊服務器,充分調用OS參與併發操作,編程比較複雜,JDK7開始支持。 

1. Java的BIO

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

1.1 Java BIO 工作機制

 

 BIO完整流程的梳理

1. 服務器端啓動一個 ServerSocket
2. 客戶端啓動 Socket 對服務器進行通信,默認情況下服務器端需要對每個客戶 建立一個線程與之通訊
3. 客戶端發出請求後, 先諮詢服務器是否有線程響應,如果沒有則會等待,或者被拒絕
4. 如果有響應,客戶端線程會等待請求結束後,在繼續執行

 1.2 代碼演示

使用 BIO 模型編寫一個服務器端,監聽 9000 端口,當有客戶端連接時,就啓動一個線程與之通訊。

package com.brian.netty.io;

import lombok.extern.slf4j.Slf4j;

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;

@Slf4j
public class BlockingService {

    public static void main(String[] args) throws IOException {

        // create a cached thread pool
        ExecutorService executorService = Executors.newCachedThreadPool();
        // create a ServerSockets
        ServerSocket serverSocket = new ServerSocket(9000);
        log.info("===== ServerSocket Start =====");
        while (true) {
            log.info("===== waiting the client connect =====");
            //  listening and wait the client connect
            final Socket accept = serverSocket.accept();
            log.info("===== one client connected thread name is: {} =====", Thread.currentThread().getName());
            executorService.execute(() -> {
                try {
                    handle(accept);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        }
    }

    /**
     * communicate with client
     *
     * @param socket
     * @throws IOException
     */
    private static void handle(Socket socket) throws IOException {
        byte[] bytes = new byte[1024];
        //  get input stream by socket
        try (InputStream inputStream = socket.getInputStream()) {
            // loop read the data from client
            while (true) {
                int read = inputStream.read(bytes);
                if (-1 != read) {
                    String clientMessage = new String(bytes, 0, read);
                    log.info("===== thread <{}> get client message: {}", Thread.currentThread().getName(), clientMessage);
                } else {
                    break;
                }
            }
        }
    }
}

使用telnet,開啓兩個客戶端測試

serverSocket端日誌如下,可以看到接受到的每個客戶端消息分別對應一個線程

1.3 Java BIO 問題分析

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

2. Java的NIO

1. JavaNIO全稱java non-blocking IO,是指JDK提供的新API。從JDK1.4開始,Java提供了一系列改進的輸入/輸出的新特性,被統稱爲NIO(即New IO),是同步非阻塞的
2. NIO相關類都被放在java.nio 包及子包下,並且對原java.io包中的很多類進行改寫
3. NIO有三大核心部分:Channel(通道),Buffer(緩衝區),Selector(選擇器)
4. NIO是面向緩衝區 ,或者面向塊編程的。數據讀取到一個它稍後處理的緩衝區,需要時可在緩衝區中前後移動,這就增加了處理過程中的靈活性,使用它可以提供非阻塞式的高伸縮性網絡
5. JavaNIO的非阻塞模式,使一個線程從某通道發送請求或者讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什麼都不會獲取,而不是保持線程阻塞,
所以直至數據變的可以讀取之前,該線程可以繼續做其他的事情。 非阻塞寫也是如此,一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情
6. 通俗理解:NIO是可以做到用一個線程來處理多個操作的。假設有10000個請求過來,根據實際情況,可以分配50或者100個線程來處理。不像之前的阻塞IO那樣,非得分配100007. HTTP2.0使用了多路複用的技術,做到同一個連接併發處理多個請求,而且併發請求的數量比HTTP1.1大了好幾個數量級

2.1 NIO 和 BIO 的比較

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

 2.2 NIO的三個核心組件 Buffer,Channel, Selector

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

2.3 Buffer

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

 

2.3.1 Buffer 類及其子類

在 NIO 中,Buffer 是一個頂層父類,它是一個抽象類,底下有六個對應的子類IntBuffer, FloatBuffer, CharBuffer, DoubleBuffer, ShortBuffer, LongBuffer, ByteBuffer.如下圖所示,每個子類Buffer都是用數組在緩存數據。

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

 

 Buffer 類及其子類相關方法彙總

public abstract class Buffer {
    //JDK1.4時,引入的api
    public final int capacity()//返回此緩衝區的容量
    public final int position()//返回此緩衝區的位置
    public final Buffer position (int newPositio)//設置此緩衝區的位置
    public final int limit()//返回此緩衝區的限制
    public final Buffer limit (int newLimit)//設置此緩衝區的限制
    public final Buffer mark()//在此緩衝區的位置設置標記
    public final Buffer reset()//將此緩衝區的位置重置爲以前標記的位置
    public final Buffer clear()//清除此緩衝區, 即將各個標記恢復到初始狀態,但是數據並沒有真正擦除, 後面操作會覆蓋
    public final Buffer flip()//反轉此緩衝區
    public final Buffer rewind()//重繞此緩衝區
    public final int remaining()//返回當前位置與限制之間的元素數
    public final boolean hasRemaining()//告知在當前位置和限制之間是否有元素
    public abstract boolean isReadOnly();//告知此緩衝區是否爲只讀緩衝區
 
    //JDK1.6時引入的api
    public abstract boolean hasArray();//告知此緩衝區是否具有可訪問的底層實現數組
    public abstract Object array();//返回此緩衝區的底層實現數組
    public abstract int arrayOffset();//返回此緩衝區的底層實現數組中第一個緩衝區元素的偏移量
    public abstract boolean isDirect();//告知此緩衝區是否爲直接緩衝區
}

Buffer 子類中最常用的是ByteBuffer 類(二進制數據),該類的主要方法如下

public abstract class ByteBuffer {
    //緩衝區創建相關api
    public static ByteBuffer allocateDirect(int capacity)//創建直接緩衝區
    public static ByteBuffer allocate(int capacity)//設置緩衝區的初始容量
    public static ByteBuffer wrap(byte[] array)//把一個數組放到緩衝區中使用
    public static ByteBuffer wrap(byte[] array,int offset, int length)//構造初始化位置offset和上界length的緩衝區

     //緩存區存取相關API
    public abstract byte get( );//從當前位置position上get,get之後,position會自動+1
    public abstract byte get (int index);//從絕對位置get
    public abstract ByteBuffer put (byte b);//從當前位置上添加,put之後,position會自動+1
    public abstract ByteBuffer put (int index, byte b);//從絕對位置上put
 }

2.4 Channel

1. NIO的通道類似於流,但有些區別如下:
 - 通道可以同時進行讀寫,而流只能讀或者只能寫
 - 通道可以實現異步讀寫數據
 - 通道可以從緩衝讀數據,也可以寫數據到緩衝
2. BIO中的stream是單向的,例如FileInputStream對象只能進行讀取數據的操作,而NIO中的通道(Channel)是雙向的,可以讀操作也可以寫操作
3. Channel 在 NIO 中是一個接口public interface Channel extends Closeable{}
4. 常用的Channel類有:FileChannel、DatagramChannel,ServerSocketChannel和SocketChannel【ServerSocketChannel類似ServerSocket,SocketChannel類似Socket】
5. FileChannel用於文件的數據讀寫,DatagramChannel用於UDP的數據讀寫, ServerSocketChannel和SocketChannel用於TCP的數據讀寫

 2.4.1 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) //把數據從當前通道複製給目標通道

channel demo (read%write)代碼演示

https://github.com/showkawa/springBoot_2017/blob/master/spb-demo/spb-gateway/src/test/java/com/kawa/spbgateway/service/FileChannelTest.java

package com.kawa.spbgateway.service;

import lombok.extern.slf4j.Slf4j;
import org.junit.Test;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.UUID;


@Slf4j
public class FileChannelTest {
   

   // #1 read @Test
public void When_GetDataFromFileChannel_Except_Success() throws IOException { //create the file input stream var file = new File("/home/un/code/springBoot_2017/spb-demo/spb-gateway/src/main/resources/core.yml"); try (var fileInputStream = new FileInputStream(file)) { // get the FileChannel (FileChannelImpl) by FileInputStream var fileChannel = fileInputStream.getChannel(); // create the buffer var byteBuffer = ByteBuffer.allocate((int) file.length()); // read the channel data to buffer fileChannel.read(byteBuffer); log.info("=== core.yml ===: \r\n{}", new String(byteBuffer.array())); } }   
      
   // #2 read and write @Test
public void When_OutputData_Except_Success() throws IOException { //create the file input stream var file = new File("/home/un/code/springBoot_2017/spb-demo/spb-gateway/src/main/resources/core.yml"); ByteBuffer outputStr; try (var fileInputStream = new FileInputStream(file)) { // get the FileChannel (FileChannelImpl) by FileInputStream var fileChannel = fileInputStream.getChannel(); // create the buffer outputStr = ByteBuffer.allocate((int) file.length()); // read the channel data to buffer fileChannel.read(outputStr); } // create a file output stream var fileName = UUID.randomUUID().toString().replace("-", ""); try (var fileOutputStream = new FileOutputStream("/home/un/app/test/" + fileName)) { // get file channel by stream var channel = fileOutputStream.getChannel(); outputStr.flip(); channel.write(outputStr); } } }

其中 #1 read的示意圖如下

#2 read and write的示意圖如下

channel demo (transferFrom)代碼演示 

    @Test
    public void When_CopyFileByTransferFrom_Except_Success() throws IOException {
        //create FileInputStream and FileOutputStream
        try (var sourceStream = new FileInputStream("/home/un/code/springBoot_2017/spb-demo/spb-gateway/src/main/resources/core.yml");
             var targetStream = new FileOutputStream("/home/un/app/test/text.txt")) {
            // create the FileChannel
            var sourceCh = sourceStream.getChannel();
            var targetCh = targetStream.getChannel();
            // use transferFrom transfer data to target FileChannel
            targetCh.transferFrom(sourceCh,0 , sourceCh.size());
        }
    }

Buffer和Channel的使用注意事項

1.ByteBuffer 支持類型化的 put 和 get, put 放入的是什麼數據類型,get 就應該使用相應的數據類型來取出,否則可能有 BufferUnderflowException 異常。
2. 可以將一個普通 Buffer 轉成只讀 Buffer,如果向只讀Buffer寫數據會拋出異常ReadOnlyBufferException

ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();

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

    @Test
    public void When_ReadDataByMappedByteBuffer_Except_Success() throws IOException {
        File file = new File("/home/un/code/springBoot_2017/spb-demo/spb-gateway/src/main/resources/core.yml");
        long len = file.length();
        byte[] ds = new byte[(int) len];

        MappedByteBuffer mappedByteBuffer = new RandomAccessFile(file, "r")
                .getChannel()
                .map(FileChannel.MapMode.READ_ONLY, 0, len);
        for (int offset = 0; offset < len; offset++) {
            byte b = mappedByteBuffer.get();
            ds[offset] = b;
        }

        Scanner scan = new Scanner(new ByteArrayInputStream(ds)).useDelimiter("\n");
        while (scan.hasNext()) {
            log.info("=== MappedByteBuffer ===: {}", scan.next());
        }

        // try to put
        // java.nio.ReadOnlyBufferException
        mappedByteBuffer.flip();
        mappedByteBuffer.put("brian".getBytes());
    }

4. 前面我們講的讀寫操作,都是通過一個Buffer完成的,NIO 還支持通過多個Buffer (即Buffer數組) 完成讀寫操作

package com.kawa.spbgateway.service;

import lombok.extern.slf4j.Slf4j;

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

/**
 *  telnet 127.0.0.1 9988
 */
@Slf4j
public class ScatteringAndGatheringTest {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress(9988);
        serverSocketChannel.socket().bind(inetSocketAddress);

        ByteBuffer[] byteBuffers = new ByteBuffer[2];
        byteBuffers[0] = ByteBuffer.allocate(10);
        byteBuffers[1] = ByteBuffer.allocate(10);

        SocketChannel sc = serverSocketChannel.accept();

        int msgLength = 20;
        while (true) {
            int byteRead = 0;
            while (byteRead < msgLength) {
                long readL = sc.read(byteBuffers);
                byteRead += readL;
                log.info("=== byteRead ===:{}", byteRead);
                Arrays.stream(byteBuffers).forEach(bu -> {
                    log.info("=== byteRead ===:{ position:{},limit:{} }", bu.position(), bu.limit());
                });
            }

            Arrays.stream(byteBuffers).forEach(buffer -> buffer.flip());

            long byteWrite = 0;
            while (byteWrite < msgLength) {
                long writeL = sc.write(byteBuffers);
                byteWrite += writeL;
            }
            StringBuffer msg = new StringBuffer();
            Arrays.stream(byteBuffers).forEach(bu -> {
                msg.append(new String(bu.array()));
            });
            log.info("=== byteWrite current msg ===: {}", msg);
            Arrays.stream(byteBuffers).forEach(bu -> bu.clear());
            log.info(">>>>>>>>>> byteRead:{}, byteWrite:{},msgLength:{}", byteRead, byteWrite, msgLength);
        }
    }
}

2.5 Selector

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

 2.5.1 Selector示意圖

 

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

2.5.2 Selector類相關方法

public abstract class Selector implements Closeable {

   // 得到一個Selector對象 public static Selector open() throws IOException { return SelectorProvider.provider().openSelector(); }
// 是否是open狀態,如果調用了close()方法則會返回false
public abstract boolean isOpen();
// 獲取當前Selector的Provider public abstract SelectorProvider provider();
// 獲取當前channel註冊在Selector上所有的key
public abstract Set keys();
// 從內部集合得到所有的SelectionKey -> 當前channel就緒的事件列表 public abstract Set selectedKeys();   
// 獲取當前是否有事件就緒,該方法立即返回結果,不會阻塞;如果返回值>0,則代表存在一個或多個
public abstract int selectNow() throws IOException;
   // selectNow的阻塞超時方法,超時時間內,有事件就緒時纔會返回;否則超過時間也會返回 public abstract int select(long timeout) throws IOException;   
// selectNow的阻塞方法,直到有事件就緒時纔會返回
public abstract int select() throws IOException; // 喚醒Selector -> 調用該方法會時,阻塞在select()處的線程會立馬返回;即使當前不存在線程阻塞在select()處,
     那麼下一個執行select()方法的線程也會立即返回結果,相當於執行了一次selectNow()方法 public abstract Selector wakeup(); // 用完Selector後調用其close()方法會關閉該Selector,且使註冊到該Selector上的所有SelectionKey實例無效。channel本身並不會關閉 public abstract void close() throws IOException; }

2.5.3 Selector原理圖

NIO非阻塞網絡編程相關的(Selector、SelectionKey、ServerScoketChannel 和 SocketChannel) 關係梳理圖

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

2.5.3 Selector Demo1 

服務器端接受多個客戶端的消息並且打印

Server Code

package com.kawa.spbgateway.service;

import lombok.extern.slf4j.Slf4j;

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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;


@Slf4j
public class NioServerTest {

    public static void main(String[] args) throws IOException {
        // 1. create ServerSocketChannel
        ServerSocketChannel ssc = ServerSocketChannel.open();
        // 2. create Selector
        Selector selector = Selector.open();
        // 3. ServerSocketChannel  bind and listen port 9998
        ssc.socket().bind(new InetSocketAddress(9998));
        // 4. set the mode non-blocking
        ssc.configureBlocking(false);
        // 5. register the ServerSocketChannel to Selector with event "OP_ACCEPT"
        ssc.register(selector, SelectionKey.OP_ACCEPT);

        // 6. waiting the client SocketChannel connect
        while (true) {
            // get the SelectionKey and the related event
            selector.select();
            Iterator sks = selector.selectedKeys().iterator();
            while (sks.hasNext()) {
                // get the SelectionKey
                SelectionKey sk = sks.next();
                // remove the SelectionKey avoid repeat handle the key
                sks.remove();
                // handle OP_ACCEPT event
                if (sk.isAcceptable()) {
                    try {
                        SocketChannel socketChannel = ssc.accept();
                        log.info(">>>>>>>>>> connected client:{}", socketChannel.getRemoteAddress());
                        socketChannel.configureBlocking(false);
                        // register a OP_READ event to current channel
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                // handle OP_READ event
                if (sk.isReadable()) {
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    SocketChannel channel = (SocketChannel) sk.channel();
                    try {
                        channel.read(buffer);
                        String msg = new String(buffer.array()).trim();
                        log.info("===== get msg from {} >> {}", channel.getRemoteAddress(), msg);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }

                }
            }
        }
    }
}

Client Code

package com.kawa.spbgateway.service;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.UUID;

@Slf4j
public class NioClientTest {
    public static void main(String[] args) throws IOException, InterruptedException {
        //1. get a SocketChannel
        SocketChannel sc = SocketChannel.open();
        //2. set non-blocking mode
        sc.configureBlocking(false);
        //3. set connect server address and port
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 9998);
        //4. connect server
        if (!sc.connect(inetSocketAddress)) {
            while (!sc.finishConnect()) {
                log.info(">>>>>>>> connecting to server");
            }
        }
        //5. send msg
        while (true) {
            log.info(">>>>>>>> send mag to server");
            String msg = UUID.randomUUID().toString();
            ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
            sc.write(buffer);
            Thread.sleep(5000);
        }


    }
}

Test result

2.5.4 SelectionKey

SelectionKey,表示 Selector 和網絡通道的註冊關係, 共四種:

int OP_READ: 代表讀操作,值爲 1
int OP_WRITE: 代表寫操作,值爲 4
int OP_CONNECT:代表連接已經建立,值爲 8
int OP_ACCEPT: 有新的網絡連接可以 accept,值爲 16

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;

相關方法

Object  attach(Object ob)      將給定的對象附加到此鍵
Object  attachment()           獲取當前的附加對象
abstract  void cancel()        請求取消此鍵的通道到其選擇器的註冊
abstract  SelectableChannel  channel()     返回爲之創建此鍵的通道
abstract  int interestOps()     獲取此鍵的interest集合
abstract  SelectionKey interestOps(int ops)     將此鍵的 interest 集合設置爲給定值
boolean  isAcceptable()        測試此鍵的通道是否已準備好接受新的套接字連接
boolean  isConnectable()       測試此鍵的通道是否已完成其套接字連接操作
boolean  isReadable()          測試此鍵的通道是否已準備好進行讀取
abstract  boolean  isValid()   告知此鍵是否有效
boolean  isWritable()          測試此鍵的通道是否已準備好進行寫入
abstract  int  readyOps()      獲取此鍵的ready操作集合
abstract  Selector  selector() 返回爲此選擇器創建的鍵

 關於方法的詳細說明可參考:https://www.apiref.com/java11-zh/java.base/java/nio/channels/SelectionKey.html

2.5.5 ServerSocketChannel

ServerSocketChannel 在服務器端監聽新的客戶端 Socket 連接,相關方法如下:

abstract SocketChannel  accept()    接受與此通道套接字的連接
ServerSocketChannel  bind(SocketAddress local)    將通道的套接字綁定到本地地址並配置套接字以偵聽連接
abstract ServerSocketChannel  bind(SocketAddress local, int backlog)    將通道的套接字綁定到本地地址並配置套接字以偵聽連接
abstract SocketAddress  getLocalAddress()    返回此通道的套接字綁定的套接字地址
static ServerSocketChannel  open()    打開服務器套接字通道
abstract  ServerSocketChannel  setOption(SocketOption name, T value)    設置套接字選項的值
abstract ServerSocket  socket()    檢索與此通道關聯的服務器套接字
int  validOps()    返回標識此通道支持的操作的操作集

關於方法的詳細說明可參考:https://www.apiref.com/java11-zh/java.base/java/nio/channels/ServerSocketChannel.html

2.5.6 SocketChannel

SocketChannel,網絡 IO 通道,具體負責進行讀寫操作。NIO 把緩衝區的數據寫入通道,或者把通道里的數據讀到緩衝區,相關方法如下:

abstract SocketChannel  bind(SocketAddress local)    將通道的套接字綁定到本地地址
abstract boolean  connect(SocketAddress remote)    連接此通道的插座
abstract boolean  finishConnect()     完成連接套接字通道的過程
abstract SocketAddress  getLocalAddress()    返回此通道的套接字綁定的套接字地址
abstract SocketAddress  getRemoteAddress()    返回此通道的套接字連接的遠程地址
abstract boolean  isConnected()    判斷此通道的網絡插座是否已連接
abstract boolean  isConnectionPending()     判斷此通道上的連接操作是否正在進行中
static SocketChannel  open()    打開套接字通道
static SocketChannel  open(SocketAddress remote)    打開套接字通道並將其連接到遠程地址
abstract int  read(ByteBuffer dst)    從該通道讀取一個字節序列到給定的緩衝區
long  read(ByteBuffer[] dsts)    從該通道讀取一系列字節到給定的緩衝區
abstract long  read(ByteBuffer[] dsts, int offset, int length)    從該通道讀取一系列字節到給定緩衝區的子序列
abstract  SocketChannel  setOption(SocketOption name, T value)    設置套接字選項的值
abstract SocketChannel  shutdownInput()    在不關閉通道的情況下關閉連接以進行讀取
abstract SocketChannel  shutdownOutput()    在不關閉通道的情況下關閉連接以進行寫入
abstract Socket  socket()    檢索與此通道關聯的套接字
int  validOps()    返回標識此通道支持的操作的操作集
abstract int  write(ByteBuffer src)    從給定緩衝區向該通道寫入一個字節序列
long  write(ByteBuffer[] srcs)    從給定的緩衝區向該通道寫入一個字節序列
abstract long  write(ByteBuffer[] srcs, int offset, int length)    從給定緩衝區的子序列向該通道寫入一個字節序列

關於方法的詳細說明可參考:https://www.apiref.com/java11-zh/java.base/java/nio/channels/SocketChannel.html

2.5.7 Selector Demo2

 服務器端和客戶端之間的數據簡單聊天通訊

Server Code

package com.kawa.spbgateway.service;

import lombok.extern.slf4j.Slf4j;

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

@Slf4j
public class ChatServerTest {
    private Selector selector;
    private ServerSocketChannel listenChannel;

    public ChatServerTest() {
        try {
            selector = Selector.open();
            listenChannel = ServerSocketChannel.open();
            listenChannel.socket().bind(new InetSocketAddress(9999));
            listenChannel.configureBlocking(false);
            listenChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void listen() {
        while (true) {
            try {
                int event = selector.select();
                if (event > 0) {
                    Iterator<SelectionKey> sks = selector.selectedKeys().iterator();
                    while (sks.hasNext()) {
                        SelectionKey sk = sks.next();
                        if (sk.isAcceptable()) {
                            SocketChannel sc = listenChannel.accept();
                            sc.configureBlocking(false);
                            // when acceptable, register OP_READ event
                            sc.register(selector, SelectionKey.OP_READ);
                            log.info("online -> {}", sc.getRemoteAddress());
                        }
                        if (sk.isReadable()) {
                            reaData(sk);
                        }
                        // avoid repeat handle
                        sks.remove();
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }


    }

    private void reaData(SelectionKey sk) {
        SocketChannel channel = (SocketChannel) sk.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        try {
            int readLength = channel.read(buffer);
            if (readLength > 0) {
                String msg = channel.getRemoteAddress() +" -> " +new String(buffer.array()).trim();


                log.info("get msg from {}", msg);
                // send msg to all client
                sendToAllClient(channel, msg);
            }
        } catch (IOException e) {
            try {
                log.info("offline -> {}", channel.getRemoteAddress());
                sk.channel();
                channel.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }

    private void sendToAllClient(SocketChannel channel, String msg) throws IOException {
        for (SelectionKey key : selector.keys()) {
            Channel targetChannel = key.channel();

            if (targetChannel instanceof SocketChannel && targetChannel != channel) {
                SocketChannel destination = (SocketChannel) targetChannel;
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                destination.write(buffer);
            }
        }
    }

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


}

Client Code

package com.kawa.spbgateway.service;

import lombok.extern.slf4j.Slf4j;

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;

@Slf4j
public class ChatClientTest {
    private Selector selector;
    private SocketChannel socketChannel;
    private String clientName;

    public ChatClientTest() {
        try {
            selector = Selector.open();
            socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
            socketChannel.configureBlocking(false);
            socketChannel.register(selector, SelectionKey.OP_READ);
            clientName = socketChannel.getLocalAddress().toString().substring(1);
            log.info("{} is ready", clientName);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void sendMsg(String msg) {
        try {
            socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void getMsg() {
        try {
            int select = selector.select();
            if (select > 0) {
                Iterator<SelectionKey> sks = selector.selectedKeys().iterator();
                while (sks.hasNext()) {
                    SelectionKey key = sks.next();
                    if (key.isReadable()) {
                        SocketChannel channel = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        channel.read(buffer);
                        String msg = new String(buffer.array()).trim();
                        log.info("get mag from {}", msg);
                    }
                    sks.remove();
                }

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

    public static void main(String[] args) {
        ChatClientTest chatClientTest = new ChatClientTest();
        new Thread(() -> {
            while (true) {
                chatClientTest.getMsg();
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        // Send msg
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()) {
            String msg = scanner.nextLine().trim();
            chatClientTest.sendMsg(msg);

        }

    }
}

測試result

 

4. Java的AIO

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

簡單的demo

package com.kawa.spbgateway.service;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;

@Slf4j
public class AioServerTest {

    public static void main(String[] args) throws IOException, InterruptedException {
        // init the server socket channel and listen port 10000
        AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(10000));

        serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
            @SneakyThrows
            @Override
            public void completed(AsynchronousSocketChannel client, Void attachment) {
                serverSocketChannel.accept(null, this);
                log.info(">>>>> connect from :{}", client.getRemoteAddress().toString());
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                client.read(buffer, buffer, new CompletionHandler<>() {
                    @SneakyThrows
                    @Override
                    public void completed(Integer index, ByteBuffer buffer) {
                        buffer.flip();
                        log.info(">>>>> get message: {}", new String(buffer.array()).trim());
                        client.close();
                    }

                    @Override
                    public void failed(Throwable exc, ByteBuffer attachment) {
                        log.info(">>>>> get message exception: {}", exc.getMessage());
                    }
                });
            }

            @Override
            public void failed(Throwable exc, Void attachment) {
                log.info(">>>>> accept channel exception: {}", exc.getMessage());
            }
        });


        // to keep the process not stop
        Thread.currentThread().join();
    }
}

telnet連接測試

 

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