初識Java NIO

 

目錄

一.Linux IO模型

二. 瞭解Linux IO流程

三. 各IO模型執行過程

(一)阻塞式IO模型

(二)非阻塞式IO模型

(三)IO多路複用模型

(四)信號驅動式IO模型

(五)異步IO模型

(六)各IO模型對比

四. BIO下的Socket處理方式

1. Single Thread Socket Server

2. Multiple Thread Socket Server

3. Thread Pool Socket Server

五. Java NIO

(一)Java NIO Buffer

(二)Channel

(三)Selector

六. 代碼實例


一.Linux IO模型

在《Unix網絡編程》這本書中將IO模型劃分爲以下五種:

  1. 阻塞式IO模型Blocking IO
  2. 非阻塞式IO模型Non-Blocking IO/New IO
  3. IO複用
  4. 信號驅動式IO模型
  5. 異步IO模型Asynchronous IO

其中前四種都是同步模式。

 

二. 瞭解Linux IO流程

在Linux中運行的應用程序如果需要進行IO操作,需要涉及到兩個空間的概念,用戶空間和內核空間。

上圖表示的是一個數據讀取的過程:

  1. DMA先從將磁盤數據拷貝到內核空間,該過程也稱爲數據準備過程;
  2. 應用程序拷貝內核空間數據到用戶空間。
  3. 寫數據的過程與之相反。

從應用程序角度來看,分爲兩步:

  1. 等待內核將數據準備好(Waiting for the data to be ready)
  2. 從內核向進程複製數據(Copying the data from the kernel to the process)

 

三. 各IO模型執行過程

(一)阻塞式IO模型

應用程序發起一次系統調用,即IO請求,詢問內核數據是否準備好,發現沒有,則進程一直等待數據準備好爲止;在接收到系統調用後內核開始準備數據報,準備好後進行內核態到用戶態的數據拷貝,拷貝完成後,返回準備就緒信息給調用進程。

(二)非阻塞式IO模型

應用進程輪詢詢問內核數據是否準備好。當有數據報準備好時就進行數據報的拷貝操作,當沒有準備好時,內核直接返回未準備就緒的信號,不讓進程阻塞,並且開始準備數據,等待進程下一次詢問。

(三)IO多路複用模型

IO多路複用模型會通過一個select函數對多個文件描述符(集合)進行循環監聽,當某個文件描述符就緒時,就對這個文件描述符的數據進行處理。

(四)信號驅動式IO模型

應用程序通知內核,當數據準備就緒時給它發送一個SIGIO信號,應用程序會對這個信號進行捕捉,並且調用相應的信號處理函數執行數據拷貝的過程。

(五)異步IO模型

當應用程序調用aio_read時,內核一邊開始準備數據,另一邊將程序控制權返回給應用進程,讓應用進程處理其他事情;當內核中有數據報準備就緒時,由內核將數據報拷貝到用戶空間,這也是與其他四種模型的最大區別,拷貝完成後返回aio_read中定義好的函數處理程序。

(六)各IO模型對比

從下圖中可以看出阻塞程度是:阻塞式IO >非阻塞式IO > IO複用>信號驅動式IO >異步IO。

 

四. BIO下的Socket處理方式

下圖是Socket和ServerSocket通信模型:

1. Single Thread Socket Server

public void startServer() throws IOException {
    final ServerSocket serverSocket = new ServerSocket(8080);
    System.out.println("Listening for connection on port 8080...");
    while (!Thread.interrupted()) {
        final Socket socket = serverSocket.accept();
        // 1. Read request from the socket of client.
        // 2. Prepare a response.
        // 3. Send response to the client.
        // 4. Close the socket.
    }
}

2. Multiple Thread Socket Server

public void startServer() throws IOException {
    final ServerSocket serverSocket = new ServerSocket(8080);
    System.out.println("Listening for connection on port 8080...");
    while (!Thread.interrupted()) {
        final Socket socket = serverSocket.accept();
        new Thread(() -> {
            // 1. Read request from the socket of client.
            // 2. Prepare a response.
            // 3. Send response to the client.
            // 4. Close the socket.
        }).start();
    }
}

3. Thread Pool Socket Server

public void startServer() throws IOException {
    final ServerSocket serverSocket = new ServerSocket(8080);
    // 爲了代碼簡潔,這裏直接通過工具類創建一個線程池
    ExecutorService executor = Executors.newFixedThreadPool(10);
    System.out.println("Listening for connection on port 8080...");
    while (!Thread.interrupted()) {
        final Socket socket = serverSocket.accept();
        executor.execute(() -> {
            // 1. Read request from the socket of client.
            // 2. Prepare a response.
            // 3. Send response to the client.
            // 4. Close the socket.
        });
    }
}

 

五. Java NIO

Java IO和Java NIO的區別

Java NIO三個關鍵對象

  • Buffer
  • Channel
  • Selector

(一)Java NIO Buffer

1. 一個Buffer本質上是內存的一個內存塊,允許對這塊內存進行數據讀寫操作,在Java NIO中定義了以下幾種Buffer實現:

2. Java NIO的Buffer主要有三個核心屬性:

(1)Capacity:

緩衝區容量,一旦設定就不可更改,比如capacity爲1024的IntBuffer,代表其最大可以存放1024個int類型的數據。

(2)Position:

記錄下一個可操作(可讀/可寫)地址。

(3)Limit:

Java Buffer是通過一個指針來維護讀寫操作的,從寫操作模式切換到讀操作模式,position都會歸零,這些可以保證從頭開始讀寫,讀寫模式切換必須使用flip方法。

在初始化後默認是寫操作模式,此時limit代表的是最大能寫入數據,即limit = capacity;

最大能寫入的數據,初始狀態下limit = capacity。

寫操作結束後,flip切換到讀模式下,此時limit等於Buffer中實際寫入數據的大小,比如在寫模式下寫入了10個int類型的數據,那麼此時limit=10。

(4)Mark:

標記當前position位置,在執行reset方法後將pisition恢復到標記位置,mark的位置必須小於等於position,它們之間的關係:0<=mark<=position<=limit<=capacity

3. ByteBuffer實現

Java NIO中ByteBuffer有兩種具體實現Direct ByteBuffer和Heap ByteBuffer,其中HeapByteBuffer也被認爲是Non-Direct ByteBuffer。

 

DirectByteBuffer

HeapByteBuffer

創建開銷

存儲位置

Native heap

JVM heap

維護一個字節數組byte[]

數據拷貝

無需臨時緩衝區做拷貝

先拷貝到DirectByteBuffer類型的臨時緩衝區,並且這個緩衝區具有緩存功能。

GC影響

每次創建或者釋放時都調用一次System.gc()

4. Buffer操作方法

方法名稱

作用

allocate

創建一個Heap ByteBuffer類型的緩衝區

allocateDirect

創建一個Direct ByteBuffer類型的緩衝區

wrap

通過外部傳入一個數組創建Heap ByteBuffer類型的緩衝區

flip

切換讀寫操作模式

put

將單個字節寫入緩衝區position位置

get

從緩衝區中讀取單個字節

mark

標記當前position

reset

恢復position到mark標記的位置

rewind

將position設回0,所以你可以重讀Buffer中的所有數據。limit保持不變。

clear

清空緩存區,這裏的清空並不是清除緩衝區數據,而是position被重置爲0,limit被重置爲capacity。

compact

Compact與clear不同,compact是將所有未讀的數據拷貝到Buffer起始處。然後將position設到最後一個未讀數據下一位。limit屬性依然像clear()方法一樣設置成capacity。

remaining

計算並返回Buffer緩衝區中剩餘數據大小

(二)Channel

所有的NIO操作都是基於通道Channel的,通道是數據來源和數據寫入的目標,Java NIO中主要實現了以下幾類Channel:

  • FileChannel:文件通道,用於文件讀寫
  • SocketChannel:可以將它理解爲TCP協議方式的連接通道,也可以簡單理解爲TCP客戶端
  • ServerSocketChannel:TCP協議方式的服務端,用於監聽具體端口的數據接受請求
  • DatagramChannel:用於基於UDP協議連接的數據接收和發送

(三)Selector

Selector是Java NIO規範中十分重要的組件,它的作用是用來檢查一個或多個NIO Channel通道的狀態是否處於可讀或可寫,實現了單線程可以管理多個Channel的目標。

 

六. 代碼實例

package com.zjhuang.socket.server;

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

/**
 * 實現Java NIO處理socket請求
 *
 * @author 
 * @create 2018/11/20 19:21
 **/
public class NioSocketServer implements Runnable {

    /**
     * 選擇器
     */
    private Selector selector;
    /**
     * Server端Socket通道
     */
    private ServerSocketChannel channel;

    public NioSocketServer(int port) throws IOException {
        // 創建選擇器
        selector = Selector.open();
        // 打開一個server端socket通道
        channel = ServerSocketChannel.open();
        // 設置爲non-blocking模式
        channel.configureBlocking(false);
        // 通道綁定到指定端口
        channel.socket().bind(new InetSocketAddress(port), 1024);
        // 將通道註冊到selector上,並且對Accept事件感興趣
        channel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("正在監聽" + port + "端口請求...");
    }

    @Override
    public void run() {
        while (!Thread.interrupted()) {
            try {
                // 檢查就緒的通道
                if (selector.select(1000) == 0) {
                    continue;
                }
                // 返回就緒通道集合
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                // 遍歷集合
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    handle(key);
                    // 移除已處理通道
                    iterator.remove();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        if (null != selector) {
            try {
                // 關閉Selector,並使註冊到該Selector上的所有SelectionKey實例無效
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 處理通道的讀寫事件
     *
     * @param key SelectionKey對象
     * @throws IOException
     */
    private void handle(SelectionKey key) throws IOException {
        // 判斷新接入的通道是否有效
        if (key.isValid()) {
            if (key.isAcceptable()) {
                // 當通道就緒時註冊一個新的socketChannel,並對讀取事件感興趣
                ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                SocketChannel socketChannel = serverSocketChannel.accept();
                socketChannel.configureBlocking(false);
                socketChannel.register(selector, SelectionKey.OP_READ);
            }
            if (key.isReadable()) {
                SocketChannel socketChannel = (SocketChannel) key.channel();
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = socketChannel.read(readBuffer);
                if (readBytes == -1) {
                    socketChannel.close();
                    key.cancel();
                } else {
                    // 切換到讀模式
                    readBuffer.flip();
                    // 創建一個readBuffer鍾剩餘數據大小的字節數組
                    byte[] bytes = new byte[readBuffer.remaining()];
                    // 從readBuffer中讀取指定大小的數據到字節數組中
                    readBuffer.get(bytes);
                    // 打印接收到的數據
                    System.out.println("服務端接收到數據:" + new String(bytes, "UTF-8"));
                    // 回顯數據
                    socketChannel.write(ByteBuffer.wrap("success\r\n".getBytes()));
                }
            }
        }
    }

    public static void main(String[] args) throws Exception {
        new Thread(new NioSocketServer(8080)).start();
    }

}

 

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