NIO是啥?
NIO是Java從JDK1.4開始引入的一系列改進版輸入輸出處理手段,也就是New IO,簡稱NIO,也有說法叫NonBlocking IO,是同步非阻塞式的IO模型,準確地說它支持阻塞非阻塞兩種模式。
筆者在NIO、BIO、AIO、同步異步、阻塞非阻塞傻傻分不清楚?一文中詳細總結了同步、非阻塞等相關概念,分析了NIO與傳統BIO的主要區別。
本篇主要介紹NIO提供的三大組件的概念及使用:Buffer,Channel,Selector。
Buffer
Buffer可以理解爲是一個容器,用於存儲數據,本質是個數組,存儲的元素類型是基本類型。
無論是發送還是讀取Channel中的數據,都必須先置入Buffer。
java.nio.Buffer
是一個抽象類,子類包括有除boolean外其他所有基本類型的XxBuffer,最常用的是ByteBuffer。
Buffer中的重要概念
capacity:緩衝區的容量,表示該Buffer的最大數據容量,即最多可以存儲多少數據。
limit:限制位,標記position能夠到達的最大位置,默認爲緩衝區最後一位。
position:操作位,指向即將操作位置,默認指向0。
mark:可選標記位。默認不啓用,Buffer允許直接將position定位到mark處。
他們滿足的關係:mark <= position <= limit <= capacity
以ByteBuffer.allocate(capacity)
爲例,說明幾個重要的過程:
- 初始化創建HeapByteBuffer,mark = -1,position = 0,limit = cap。
- 通過put方法向Buffer中加入數據,position++。
- 裝入數據結束後,調用flip方法,將limit設置爲position所在位置,position置爲0,表示[position,limit]這段需要開始進行輸出了【可以使用get方法讀取數據】。
- 輸出結束後,調用clear方法,將position置爲0,limit置爲cap,爲下一次讀取數據做好準備。
Buffer的所有子類都提供了put和get方法,對應向Buffer存入數據和從Buffe中取出數據,方式分爲以下兩種:
- 相對:從Buffer的當前pos處開始讀取或寫入數據,然後將pos的值按處理元素的個數增加。
- 絕對:直接根據索引向Buffer中讀取或寫入數據,不會影響pos位置本身的值。
Buffer使用Demo
public static void main(String[] args) {
// 創建 bytebuffer allocate指定底層數組長度
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
// 添加數據
byteBuffer.put("summerday".getBytes());
// 獲取操作位的位置pos = 9
System.out.println(byteBuffer.position());
// 如果我想遍歷summerday呢?哪裏結束? limit = 10
System.out.println(byteBuffer.limit());
//反轉緩衝區 可以利用flip()代替下面兩步操作
//將limit移到position的位置
byteBuffer.limit(byteBuffer.position());
//將pos移到0
byteBuffer.position(0);
// position < limit
// 可以利用 hasRemaining代替判斷pos和limit之間是否還有可處理元素。
while(byteBuffer.position() < byteBuffer.limit()){
// 獲取數據
byte b = byteBuffer.get();
System.out.println(b);
}
}
常用方法介紹
其實Buffer操作的邏輯比較簡單,每個方法操作的字段也不外乎上面介紹的幾個,下面是一些常用的方法:
設置方法
- Buffer position(newPosition): 將pos設置爲newPosition。
- Buffer limit(newLimit):將limit設置爲newLimit。
數據操作
- Buffer reset:將pos置爲mark的位置。
- Buffer rewind:將pos置爲0,取消設置的mark。
- Buffer flip: 將limit置pos位置,pos置0。
- Buffer clear:將position置爲0,limit置爲cap。
其他操作
public static void main(String[] args) {
// 如果數據已知,可以使用wrap方法創建ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.wrap("summerday".getBytes());
// 獲取底層字節數組
byte[] array = byteBuffer.array();
System.out.println(new String(array));
}
Channel
Channel概述
Channel 類似於傳統的流對象,但有些不同:
- Channel 直接將指定文件的部分或全部直接映射 Buffer。
- 程序不能直接訪 Channel 中的數據,包括讀取、寫入都不行,Channel只能與 Buffer 進行交互。意思是,程序要讀數據時需要先通過Buffer從Channel中獲取數據,然後從Buffer中讀取數據。
- Channel通常可以異步讀寫,但默認是阻塞的,需要手動設置爲非阻塞。
Channel不應該通過構造器來直接創建,而是通過傳統的節點InputStream、OutputStream的getChannel()方法來返回對應的Channel,或者通過RandomAccessFile對象的getChannel方法。
Channel中最常用的三種方法:
- map():將Channel對應的部分或全部數據映射成ByteBuffer。
- read():從Buffer中讀取數據。
- write():向Buffer中寫入數據。
RandomAccessFile#getChannel
下面是個簡單的示例,通過RandomAccessFile的getChannel方法:
public static void main(String[] args) throws FileNotFoundException {
RandomAccessFile file = new RandomAccessFile("D://b.txt", "rw");
// 獲取RandomAccessFile對應的channel
try (FileChannel fileChannel = file.getChannel()) {
ByteBuffer buf = ByteBuffer.allocate(48);
int byteRead = fileChannel.read(buf);
while(byteRead != -1){
System.out.println("read " + byteRead);
buf.flip();
while (buf.hasRemaining()){
System.out.println((char) buf.get());
}
buf.clear();
byteRead = fileChannel.read(buf);
}
} catch (IOException e) {
e.printStackTrace();
}
}
SocketChannel與ServerSocketChannel
Java爲Channel接口根據不同功能,提供了不同的實現類,比如我們下面的示例:支持TCP網絡通信的SocketChannel和ServerSocketChannel。
public class Client {
public static void main(String[] args) throws IOException {
// 開啓客戶端的channel
SocketChannel sc = SocketChannel.open();
// 手動設置爲非阻塞模式
sc.configureBlocking(false);
// 發起連接
sc.connect(new InetSocketAddress(8081));
// 手動判斷保證連接的建立
while(!sc.isConnected()){
// 如果多次連接都沒有臉上,會認爲此次連接無法建立
sc.finishConnect();
}
// 發送數據
sc.write(ByteBuffer.wrap("hello , i am client".getBytes()));
// 關閉通道
sc.close();
}
}
public class Server {
public static void main(String[] args) throws IOException {
// 開啓服務器端的通道
ServerSocketChannel ssc = ServerSocketChannel.open();
// 監聽端口號
ssc.bind(new InetSocketAddress(8081));
// 手動設置爲非阻塞模式
ssc.configureBlocking(false);
// 接收連接
SocketChannel sc = ssc.accept();
// 保證連接
while (sc == null) {
sc = ssc.accept();
}
// 讀取數據
ByteBuffer buffer = ByteBuffer.allocate(1024);
sc.read(buffer);
byte[] bs = buffer.array();
System.out.println(new String(bs, 0, buffer.position()));
// 關流
ssc.close();
}
}
Selector
NIO實現非阻塞IO的其中關鍵組件之一就是Selector多路複用選擇器,可以註冊多個Channel到一個Selector中。Selector可以不斷執行select操作,判斷這些註冊的Channel是否有已就緒的IO事件,如可讀,可寫,網絡連接已完成等。
一個線程通過使用一個Selector管理多個Channel。
public class Server {
public static void main(String[] args) throws IOException {
// 開啓服務端的通道
ServerSocketChannel ssc = ServerSocketChannel.open();
// 設置非阻塞
ssc.configureBlocking(false);
// 綁定端口
ssc.bind(new InetSocketAddress(8081));
// 開啓選擇器
Selector selector = Selector.open();
// 將通道註冊到選擇器上
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 選擇已註冊的通道
selector.select();
// 獲取選擇通道的事件
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 接收
if (key.isAcceptable()) {
// 從事件中獲取通道
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
// 接收連接
SocketChannel sc = channel.accept();
// 設置非阻塞
sc.configureBlocking(false);
// 註冊讀 + 寫事件
sc.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
}
// 讀
if (key.isReadable()) {
// 獲取通道
SocketChannel sc = (SocketChannel) key.channel();
// 讀取數據到buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
sc.read(buffer);
// 反轉緩衝區
buffer.flip();
System.out.println(new String(buffer.array(), 0, buffer.limit()));
// 在同一通道上註冊,將會將之前註冊的事件給註冊
// 註銷read事件
sc.register(selector, key.interestOps() ^ SelectionKey.OP_READ);
}
// 寫
if (key.isWritable()) {
// 獲取通道
SocketChannel sc = (SocketChannel) key.channel();
sc.write(ByteBuffer.wrap("hello client, i am server!".getBytes()));
// 註銷write事件
sc.register(selector, key.interestOps() ^ SelectionKey.OP_WRITE);
}
}
iterator.remove();
}
}
}
public class Client {
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress(8081));
sc.write(ByteBuffer.wrap("hello ! i am client !".getBytes()));
ByteBuffer buffer = ByteBuffer.allocate(1024);
sc.read(buffer);
System.out.println(new String(buffer.array(), 0, buffer.position()));
}
}