NIO三大核心
- 在NIO中有三個核心對象
- 緩衝區(Buffer)
- 選擇器(Selector)
- 通道(channel)
什麼是緩衝區Buffer?
- 緩衝區實際上是一個容器對象,本質上是一個數組
- 在NIO庫中,幾乎所有的數據都是使用緩衝區處理的
- 在進行讀寫操作的時候,首先會經過緩衝區
- 在面向流的IO系統中,所有的數據都是 讀寫 到Stream對象中
類繼承關係
- 在NIO庫中,頂層抽象類Buffer定義了緩衝區的規範。它提供了各個基本數據類型對應的buffer
工作原理
- 在緩衝區中有三個重要的屬性
- position capacity limit
postiton:指定下一個將要被寫入或者讀取的元素索引,調用put/get方法時更新,初始化爲0
limit :指定剩餘數據容量/剩餘可存儲數據空間
capacity:可以存儲在緩衝區的最大數據容量
- 一個簡單示例
public class BufferPlay {
public static void main(String[] args) throws Exception {
// 文件IO處理 使用文件輸入流讀取文件 把數據從磁盤讀取到內存—>內核緩衝區->進程緩衝區
FileInputStream fis = new FileInputStream("filePath");
// 創建文件的操作管道
FileChannel channel = fis.getChannel();
// 分配一個大小固定的緩衝區,即一個固定長度的byte數組
ByteBuffer buffer = ByteBuffer.allocate(10);
output("初始化", buffer);
// 進行一次讀操作
channel.read(buffer);
output("調用channel.read()", buffer);
// 鎖定操作範圍
buffer.flip();
output("調用buffer.flip()", buffer);
// 判斷有無可讀數據
while (buffer.remaining() > 0) {
byte b = buffer.get();
}
output("調用buffer.get()", buffer);
// 復位
buffer.clear();
output("調用buffer.clear()", buffer);
// 關閉管道
channel.close();
}
private static void output(String string, ByteBuffer buffer) {
System.out.println(string + " : ");
// 容量,數組大小
System.out.print("capacity: " + buffer.capacity() + ", ");
// 當前操作數據所在的位置,也可以叫做遊標
System.out.print("position: " + buffer.position() + ", ");
// 鎖定值,flip,數據操作範圍索引只能在position - limit 之間
System.out.println("limit: " + buffer.limit());
System.out.println();
}
}
// 運行結果
初始化 :
capacity: 10, position: 0, limit: 10
調用channel.read() :
capacity: 10, position: 8, limit: 10
調用buffer.flip() :
capacity: 10, position: 0, limit: 8
調用buffer.get() :
capacity: 10, position: 8, limit: 8
調用buffer.clear() :
capacity: 10, position: 0, limit: 10
- 圖解
緩衝區分配
public class BufferWrap {
public void myMethod() {
/* 調用 allocate(int i) 方法相當於創建了一個指定大小的數組,並封裝爲緩衝區對象
* 也可以使用 wrap(byte[] arr) 方法,把一個現有的數組封裝爲緩衝區對象
*
*/
// 分配指定大小的緩衝區
ByteBuffer allocate = ByteBuffer.allocate(10);
// 定義一個 Byte 數組 容量爲10
byte[] arr = new byte[10];
ByteBuffer result = ByteBuffer.wrap(arr);
}
}
緩衝區分片
public class BufferSlice {
/*
* 緩衝區分片:在現有緩衝區上切出一片來作爲一個新的緩衝區,即子緩衝區
* 現有緩衝區與字緩衝區在底層數組層面是數據共享的
* 子緩衝區相當於現有緩衝區的一個視圖窗口
*/
public static void main(String[] args) {
// 分配一個緩衝區大小爲10的數組
ByteBuffer buffer = ByteBuffer.allocate(10);
// 向緩衝區中寫入數據 0-9
for (int i = 0; i < buffer.capacity(); ++i) {
buffer.put((byte) i);
}
// 創建子緩衝區 設置字緩衝區的位置 爲 原緩衝區位置的 3-6 容量爲4
buffer.position(3);
buffer.limit(7);
ByteBuffer slice = buffer.slice();
// 改變子緩衝區的內容
for (int i=0; i<slice.capacity(); ++i) {
byte b = slice.get( i );
b *= 10;
slice.put(i, b);
}
// 重新設置 position 和 limit 的位置,方便讀取驗證數據
buffer.position(0);
buffer.limit(buffer.capacity());
while (buffer.remaining() > 0) {
System.out.print(buffer.get()+" ");
}
}
}
只讀緩衝區
- 只讀緩衝區不允許寫入數據
// 創建一個緩衝區
ByteBuffer buffer = ByteBuffer.allocate(10);
// 創建一個只讀緩衝區-從原有緩衝區基礎上覆制
ByteBuffer readOnly = buffer.asReadOnlyBuffer();
-
只讀緩衝區與原緩衝區共享數據,原緩衝區內容發生變化,只讀緩衝區內容隨之變化
-
只讀緩衝區不能轉換爲常規緩衝區,修改只讀緩衝區的內容會報異常
直接緩衝區
- 爲加快IO速度,使用一種特殊方式爲其分配內存的緩衝區
- 操作系統在進行IO操作之前,會嘗試跳過中間緩衝區,直接將內容分配給直接緩衝區
ByteBuffer buffer = ByteBuffer.allocateDirect();
內存映射
- 內存映射是一種讀寫文件數據的方法
- 速度優於常規基於流或者通道的IO
- 通過使文件中的數據出現在內存數組中來完成,一般只有文件中實際讀寫的內容部分纔會映射到內存中
public class BufferMapped {
public static void main(String[] args) throws IOException {
RandomAccessFile raf = new RandomAccessFile("filePath", "rw");
FileChannel channel = raf.getChannel();
// 把緩衝區和文件系統進行映射關聯
MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, 8);
// 修改數據——執行完畢在對應文件中查看
map.put(1, (byte) 100);
map.put(7, (byte) 101);
raf.close();
channel.close();
}
}
選擇器Selector
傳統的Server/Client
- 傳統的Server/Client交互基於TPR (Thread per Request)
- 服務器會爲每一個客戶端請求建立一個線程,由該線程負責處理一個用戶請求
- 請求數量很多的情況下,系統難以承受
NIO模型中採用的方式
- NIO中的非阻塞IO,採用了基於Reactor的工作模式,IO調用不會被阻塞
- 通過註冊特定的IO事件,如可讀數據到達、新的套接字連接等。發生特定事件時,系統返回通知
- 實現非阻塞IO的核心對象就是選擇器Selector
- Selector是註冊各種IO事件的地方,對不同事件發生做出相對響應
圖片引用自咕泡學院Tom老師的課堂筆記
-
當有事件發生時,可以從Selector中獲取相應的SelectionKey
-
從Selectionkey中可以找到事件發生的具體SelectableChannel
-
SelecttableChannel處理完畢,可對key重新進行註冊
-
代碼示例
public class NioServerSocket {
/**
* selector 選擇器
*/
private Selector selector;
/**
* 緩衝區
*/
private ByteBuffer buffer = ByteBuffer.allocate(1024);
private int port = 8080;
public NioServerSocket(int port) {
// 初始化 輪詢器
try {
this.port = port;
ServerSocketChannel server = ServerSocketChannel.open();
// socket 服務綁定到目標 IP:PORT 默認爲本地IP即 localhost
server.bind(new InetSocketAddress(this.port));
//NIO:BIO的升級版本 兼容BIO NIO模型默認採用阻塞式
// 設置false:非阻塞
server.configureBlocking(false);
// 可以接收請求了
this.selector = Selector.open();
// ON_ACCEPT :設置爲可接收請求狀態
server.register(this.selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public void listen() {
System.out.println("listen on "+ this.port + "");
//輪詢主線程
try {
while (true) {
// 接收所有的請求 select()方法會阻塞,直到拿到一個key
this.selector.select();
Set<SelectionKey> selectionKeys = this.selector.selectedKeys();
// 不斷地迭代
Iterator<SelectionKey> iterator = selectionKeys.iterator();
// 同步體現:每次只能拿到一個key,每次只能處理一種狀態
while (iterator.hasNext()) {
// 每一個key代表一種狀態
SelectionKey key = iterator.next();
iterator.remove();
process(key);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void process(SelectionKey key) throws IOException {
// 針對每一種 key 的狀態給一個反應
if(key.isAcceptable()) {
ServerSocketChannel severChannel = (ServerSocketChannel) key.channel();
SocketChannel channel = severChannel.accept();
channel.configureBlocking(false);
// 當數據準備就緒的時候,將狀態改爲可讀
key = channel.register(this.selector, SelectionKey.OP_READ);
} else if (key.isReadable()){
// key.channel 從多路複用器中拿到客戶端的引用
// 多路複用體現在,key.channel 每一次拿到的對象都是同一個引用
SocketChannel channel = (SocketChannel) key.channel();
int length = channel.read(this.buffer);
if (length > 0) {
this.buffer.flip();
String content = new String(this.buffer.array(), 0, length);
key = channel.register(this.selector, SelectionKey.OP_WRITE);
key.attach(content);
System.out.println("讀取內容:"+content);
}
} else if (key.isWritable()) {
SocketChannel channel = (SocketChannel) key.channel();
String content = (String) key.attachment();
channel.write(ByteBuffer.wrap(("輸出"+content).getBytes()));
channel.close();
}
}
public static void main(String[] args) {
new NioServerSocket(8080).listen();
}
}
通道Channel
- 數據通過Buffer對象來處理
- 數據 和 Buffer對象交互中,通過通道Channel來完成
NIO中Channel結構
- NIO中所有的通道對象都實現了Channel接口
Channel的作用
- 使用NIO讀寫數據
從 InputStream 中獲取 Channel
創建Buffer
將數據通過Channle 讀/寫 到Buffer