NIO
(1)基本介紹
1)JAVA NIO全程 java non-blocking IO,是指JDK提供的新API。從JDK1.4開始,Java提供了一系列改進的輸入/輸出的新特性,被統稱為NIO,是同步非阻塞的
2)NIO相關類都被放在java.nio包及子包下,并且對原java.io包中的很多類進行改寫
3)NIO有三大核心部分:Channel(通道),Buffer(緩沖區),Selector(選擇器)
4)NIO是面向緩沖區,或者面向塊編程的。數據讀取到一個它稍后處理的緩沖區,需要時可在緩沖區中前后移動,這就增加了處理過程中的靈活性,使用它可以提供非阻塞式高伸縮性網絡
5)Java NIO的非阻塞模式,使一個線程從某通道發送請求或者讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什么都不會獲取,而不是保持線程阻塞,所以直至數據變的可以讀取之前,該線程可以繼續做其它的事情。非阻塞也是如此,一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情
6)HTTP2.0使用了多路復用的技術,做到同一個連接并發處理多個請求,并且并發請求的數量比HTTP1.1大了好幾個數量級
(2)NIO和BIO的比較
1)BIO以流的方式處理數據,而NIO以塊的方式處理數據,塊I/O的效率比流I/O高很多
2)BIO是阻塞的,NIO是非阻塞的
3)BIO基于字節流和字符流進行操作,而NIO基于Channel(通道)和Buffer(緩沖區)進行操作,數據總是從通道讀取到緩沖區中,或者從緩沖區寫入到通道中。Selector(選擇器)用于監聽多個通道的時間(比如:連接請求,數據到達等),因此使用單個線程就可以監聽多個客戶端通道
(3)NIO三大核心原理

每個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類定義了所有的緩沖區都具有的四個屬性來提供關于其所包含的數據元素的信息:


常用方法

通道(Channel)
(1)基本介紹
NIO的通道類似于流,但有些區別如下:
通道可以同時進行讀寫,而流只能讀或者寫
通道可以實現異步讀寫數據
通道可以從緩沖讀數據,也可以寫數據到緩沖
BIO中的stream是單向的,例如FileInputStream對象只能進行讀取數據的操作,而NIO中的通道(Channel)是雙向的,可以讀操作,也可以寫操作
常見的Channel類有:FileChannel、DatagramChannel、ServerSocketChannel和SocketChannel。
FileChannel用于文件的數據讀寫,DatagramChannel用于UDP的數據讀寫,ServerSocketChannel和SocketChannel用于TCP的數據讀寫
(2)FileChannel

(3)案例1-本地文件寫數據
public class NIOFileChannel01 { public static void main(String[] args) throws Exception{ String str = "hello,NIO"; //創建一個輸出流->channel FileOutputStream fileOutputStream = new FileOutputStream("/Users/Apple/學習/study/test01.txt"); //通過 fileOutputStream 獲取 對應的 FileChannel //這個 fileChannel 真實 類型是 FileChannelImpl FileChannel fileChannel = fileOutputStream.getChannel(); //創建一個緩沖區 ByteBuffer ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //將 str 放入 byteBuffer byteBuffer.put(str.getBytes()); //對byteBuffer 進行flip byteBuffer.flip(); //將byteBuffer 數據寫入到 fileChannel fileChannel.write(byteBuffer); fileOutputStream.close(); } }
(4)案例2-本地文件讀數據
public class NIOFileChannel02 {
public static void main(String[] args) throws Exception {
//創建文件的輸入流
File file = new File("/Users/apple/學習/study/test01.txt");
FileInputStream fileInputStream = new FileInputStream(file);
//通過fileInputStream 獲取對應的FileChannel -> 實際類型 FileChannelImpl
FileChannel fileChannel = fileInputStream.getChannel();
//創建緩沖區
ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
//將通道的數據讀入到Buffer
fileChannel.read(byteBuffer);
?
//將byteBuffer 的 字節數據 轉成String
System.out.println(new String(byteBuffer.array()));
fileInputStream.close();
?
}
}
(5)案例3-使用Buffer完成文件的讀取、寫入
public class NIOFileChannel03 {
public static void main(String[] args) throws Exception {
?
FileInputStream fileInputStream = new FileInputStream("1.txt");
FileChannel fileChannel01 = fileInputStream.getChannel();
?
FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
FileChannel fileChannel02 = fileOutputStream.getChannel();
?
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
?
//循環讀取
while (true) {
?
//這里有一個重要的操作,一定不要忘了
/*
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
*/
byteBuffer.clear(); //清空buffer
int read = fileChannel01.read(byteBuffer);
System.out.println("read =" + read);
//表示讀完
if (read == -1) {
break;
}
//將buffer 中的數據寫入到 fileChannel02 -- 2.txt
byteBuffer.flip();
fileChannel02.write(byteBuffer);
}
?
//關閉相關的流
fileInputStream.close();
fileOutputStream.close();
}
}
(6)案例4-拷貝文件 transferFrom 方法
public class NIOFileChannel04 {
public static void main(String[] args) throws Exception {
?
//創建相關流
FileInputStream fileInputStream = new FileInputStream("a.jpg");
FileOutputStream fileOutputStream = new FileOutputStream("a1.jpg");
?
//獲取各個流對應的fileChannel
FileChannel sourceCh = fileInputStream.getChannel();
FileChannel destCh = fileOutputStream.getChannel();
?
//使用transferForm完成拷貝
destCh.transferFrom(sourceCh,0,sourceCh.size());
//關閉相關通道和流
sourceCh.close();
destCh.close();
fileInputStream.close();
fileOutputStream.close();
}
}
(7)案例5-零拷貝文件-transferTo文件
零拷貝參考資料:
https://www.cnblogs.com/yibutian/p/9482640.html
http://www.360doc.com/content/19/0528/13/99071_838741319.shtml
public class NewIOClient {
public static void main(String[] args) throws Exception {
String filename = "/Users/apple/password.txt";
?
//得到一個文件channel
FileChannel fileChannel = new FileInputStream(filename).getChannel();
FileChannel fileChannel1 = new FileOutputStream("/Users/apple/password1.txt").getChannel();
?
//準備發送
long startTime = System.currentTimeMillis();
?
//在linux下一個transferTo 方法就可以完成傳輸
//在windows 下 一次調用 transferTo 只能發送8m,就需要分段傳輸文件
//transferTo 底層使用到零拷貝
long transferCount = fileChannel.transferTo(0, fileChannel.size(), fileChannel1);
?
System.out.println("發送的總的字節數 =" + transferCount + " 耗時:" + (System.currentTimeMillis() - startTime));
?
//關閉
fileChannel.close();
?
}
}
?
(8)注意事項和細節
1)ByteBuffer 支持類型化的 put 和 get, put 放入的是什么數據類型,get 就應該使用相應的數據類型來取出,否則可能有 BufferUnderflowException 異常
public class NIOByteBufferPutGet {
public static void main(String[] args) {
? //創建一個Buffer
ByteBuffer buffer = ByteBuffer.allocate(64);
?
//類型化方式放入數據
buffer.putInt(100);
buffer.putLong(9);
buffer.putChar('a');
buffer.putShort((short) 4);
?
//取出
buffer.flip();
?
System.out.println();
?
System.out.println(buffer.getInt());
System.out.println(buffer.getLong());
System.out.println(buffer.getChar());
System.out.println(buffer.getLong());
}
}
2)可以將一個普通Buffer轉成只讀Buffer
public class ReadOnlyBuffer {
public static void main(String[] args) {
?
//創建一個buffer
ByteBuffer buffer = ByteBuffer.allocate(64);
?
for(int i = 0; i < 64; i++) {
buffer.put((byte)i);
}
?
//讀取
buffer.flip();
?
//得到一個只讀的Buffer
ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
System.out.println(readOnlyBuffer.getClass());
?
//讀取
while (readOnlyBuffer.hasRemaining()) {
System.out.println(readOnlyBuffer.get());
}
?
readOnlyBuffer.put((byte)100); //ReadOnlyBufferException
}
}
3)NIO 還提供了 MappedByteBuffer, 可以讓文件直接在內存(堆外的內存)中進行修改
public class MappedByteBufferTest {
public static void main(String[] args) throws Exception {
?
RandomaccessFile randomAccessFile = new RandomAccessFile("/Users/apple/學習/study/test01.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("修改成功~~");
}
}
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();
//假定從客戶端接收8個字節
int messageLength = 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.stream(byteBuffers).map(buffer -> "position=" + buffer.position() + ", limit=" + buffer.limit()).forEach(System.out::println);
}
?
//將所有的buffer進行flip
Arrays.asList(byteBuffers).forEach(Buffer::flip);
?
//將數據讀出顯示到客戶端
long byteWirte = 0;
while (byteWirte < messageLength) {
long l = socketChannel.write(byteBuffers);
byteWirte += l;
}
?
//將所有的buffer 進行clear
Arrays.asList(byteBuffers).forEach(Buffer::clear);
?
System.out.println("byteRead:=" + byteRead + " byteWrite=" + byteWirte + ", messageLength" + messageLength);
}
?
}
}
選擇器(Selector)
(1)基本介紹
1)Java的NIO,用非阻塞的IO方式。可以用一個線程,處理多個的客戶端連接,就會用到選擇器
2)Selector能夠檢測多個注冊的通道上是否有事件發生。如果有事件發生,便獲取事件然后針對每個事件進行相應的處理。這樣就可以只用一個單線程去管理多個通道,也就是管理多個連接和請求
3)只有在連接/通道真正有讀寫事件發生時,才會進行讀寫,就大大地減少了系統開銷,并且不必為每個連接都創建一個線程,不用去維護多個線程
4)避免了多線程之間的上下文切換導致的開銷
(2)常用方法

(3)代碼示例
NIOServer.java
public class NIOServer {
public static void main(String[] args) throws Exception {
?
//創建ServerSocketChannel -> ServerSocket
?
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
?
//得到一個Selector對象
Selector selector = Selector.open();
?
//綁定一個端口6666, 在服務器端監聽
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//設置為非阻塞
serverSocketChannel.configureBlocking(false);
?
//把 serverSocketChannel 注冊到 selector 關心 事件為 OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
?
System.out.println("注冊后的selectionkey 數量=" + selector.keys().size());
?
?
//循環等待客戶端連接
while (true) {
?
//這里我們等待1秒,如果沒有事件發生, 返回
if (selector.select(1000) == 0) {
System.out.println("服務器等待了1秒,無連接");
continue;
}
?
//如果返回的>0, 就獲取到相關的 selectionKey集合
//1.如果返回的>0, 表示已經獲取到關注的事件
//2. selector.selectedKeys() 返回關注事件的集合
// 通過 selectionKeys 反向獲取通道
Set<SelectionKey> selectionKeys = selector.selectedKeys();
System.out.println("selectionKeys 數量 = " + selectionKeys.size());
?
//遍歷 Set<SelectionKey>, 使用迭代器遍歷
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
?
while (keyIterator.hasNext()) {
//獲取到SelectionKey
SelectionKey key = keyIterator.next();
//根據key 對應的通道發生的事件做相應處理
//如果是 OP_ACCEPT, 有新的客戶端連接
if (key.isAcceptable()) {
//該該客戶端生成一個 SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("客戶端連接成功 生成了一個 socketChannel " + socketChannel.hashCode());
//將 SocketChannel 設置為非阻塞
socketChannel.configureBlocking(false);
//將socketChannel 注冊到selector, 關注事件為 OP_READ, 同時給socketChannel
//關聯一個Buffer
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
?
System.out.println("客戶端連接后 ,注冊的selectionkey 數量=" + selector.keys().size()); //2,3,4..
?
?
}
//發生 OP_READ
if (key.isReadable()) {
?
//通過key 反向獲取到對應channel
SocketChannel channel = (SocketChannel) key.channel();
?
//獲取到該channel關聯的buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
channel.read(buffer);
System.out.println("form 客戶端 " + new String(buffer.array()));
?
}
?
//手動從集合中移動當前的selectionKey, 防止重復操作
keyIterator.remove();
?
}
?
}
?
}
}
NIOClient.java
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", 6666);
//連接服務器
if (!socketChannel.connect(inetSocketAddress)) {
?
while (!socketChannel.finishConnect()) {
System.out.println("因為連接需要時間,客戶端不會阻塞,可以做其它工作..");
}
}
?
//...如果連接成功,就發送數據
String str = "hello, Selector~";
//Wraps a byte array into a buffer
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
//發送數據,將 buffer 數據寫入 channel
socketChannel.write(buffer);
System.in.read();
?
}
}
(4)SelectionKey
SelectionKey,表示 Selector 和網絡通道的注冊關系, 共四種:
int OP_ACCEPT:有新的網絡連接可以 accept,值為 16
int OP_CONNECT:代表連接已經建立,值為 8
int OP_READ:代表讀操作,值為 1
int OP_WRITE:代表寫操作,值為 4
相關方法:
