一、NIO是什么?
NIO可以說是比BIO更強大的IO,可以設置非阻塞模式(通過事件的方式監聽數據的到來)
-
BIO是基于socket通信,一個線程對應一個socket連接,讀取數據要一直等待
-
NIO是基于channel通信,一個線程管理一個selector,一個selector管理多個channel,這樣一個線程就能管理多個客戶端連接
二、NIO核心組件
?① Selector:選擇器,負責管理多個通道channel,通過監聽通道綁定的事件,及時處理通道數據
?② Channel:通道,一般一端確定,另一端操作緩沖區Buffer。可以是文件通道,網絡通道等
?③ Buffer:緩沖區,包括直接內存緩沖和JVM內存緩沖。直接內存緩沖是在操作系統的非應用程序內存申請的空間,不占用JVM內存;JVM內存緩沖就是在堆空間申請的緩沖區。
? ?由于IO無法直接操作應用內存,因此需要將應用內存數據拷貝到直接內存,如果使用直接內存,就可以少一次拷貝操作
組件模型圖:
三、常用API
(1)緩沖區Buffer
① 介紹
? position:下一次讀取或寫入的位置
? limit:數據末尾后一個位置
? capacitry:緩沖區容量
② 相關API
以ByteBuffer為例
// 申請操作系統的內存緩沖區
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
// 獲取position、limit、capacity
int position = buffer.position();
int limit = buffer.limit();
int capacity = buffer.capacity();// 向緩沖區中寫入數據
String data = "hello, my name is jack!";
buffer.put(data.getBytes());// 切換模式,標定界限(limit=position,position=0)
buffer.flip();// 讀取緩沖區的數據
int dataLength = buffer.remaining(); // 獲取剩余數據量
String content = new String(buffer.array(), 0, dataLength);// 清空緩沖區,實際上是將各個下標設置為初始值,如:position=0,limit=capacity
buffer.clear();
(2)通道Channel
拷貝文件的案例
public void test02() throws Exception {// 利用通道進行文件讀寫String fileName1 = "E:\\code\\java\\io_learn\\io_02_nio\\src\\main\\resources\\1.jpg";String fileName2 = "E:\\code\\java\\io_learn\\io_02_nio\\src\\main\\resources\\2.jpg";// 打開文件輸入流FileInputStream fis = new FileInputStream(fileName1);// 通過輸入流獲取通道FileChannel fisChannel = fis.getChannel();// 打開文件輸出流FileOutputStream fos = new FileOutputStream(fileName2);// 通過輸出流獲取輸出通道FileChannel fosChannel = fos.getChannel();// 創建緩沖區ByteBuffer buffer = ByteBuffer.allocate(1024);// 將輸入通道的數據讀取到buffer,然后從buffer讀取數據到輸出通道while ((fisChannel.read(buffer)) > 0 ) {// limit=position,position=0buffer.flip();// 將數據從buffer寫入到文件fosChannel.write(buffer);// 將各個下標指針歸還到最初位置buffer.clear();}// 釋放通道資源fisChannel.close();fosChannel.close();}
?利用transferFrom實現管道數據轉移
public void transferFromTest() throws Exception {/*利用transferFrom或transferTo實現文件的copy*/FileInputStream fis = new FileInputStream("E:\\code\\java\\io_learn\\io_02_nio\\src\\main\\resources\\1.jpg");FileChannel fisChannel = fis.getChannel();FileOutputStream fos = new FileOutputStream("E:\\code\\java\\io_learn\\io_02_nio\\src\\main\\resources\\11.jpg");FileChannel fosChannel = fos.getChannel();// 將數據轉移到目的通道fosChannel.transferFrom(fisChannel , 0, fisChannel.size());fisChannel.close();fosChannel.close();}
利用transferTo實現管道數據轉移
public void transferToTest() throws Exception {/*利用transferFrom或transferTo實現文件的copy*/FileInputStream fis = new FileInputStream("E:\\code\\java\\io_learn\\io_02_nio\\src\\main\\resources\\1.jpg");FileChannel fisChannel = fis.getChannel();FileOutputStream fos = new FileOutputStream("E:\\code\\java\\io_learn\\io_02_nio\\src\\main\\resources\\10.jpg");FileChannel fosChannel = fos.getChannel();// ?將數據轉移到目的通道fisChannel.transferTo(0, fisChannel.size(), fosChannel);fisChannel.close();fosChannel.close();}
注意:transferFrom和transferTo方法一次只能傳輸2G的數據(受操作系統限制)
(3)選擇器Selector
一般是通過服務通道監聽端口,等待客戶端的連接通道,然后將連接通道注冊到Selector,等待事件觸發通道行為
public void test() throws Exception {// 1.獲取通道ServerSocketChannel serverSocketChannel =ServerSocketChannel.open();// 2。設置為非阻塞模式serverSocketChannel.configureBlocking(false);// 3.綁定端口serverSocketChannel.bind(new InetSocketAddress(9898));// 4.獲取選擇器Selector selector = Selector.open();// 5.注冊通道到選擇器,通過客戶端建立通道的事件觸發serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);while (selector.select() > 0) {// 獲取到對應事件,進行處理...}}
?四、NIO實現聊天室案例
服務端Server
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;public class Server {public static void main(String[] args) throws Exception {// 1.創建服務端通道ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();// 2.設置為非阻塞模式serverSocketChannel.configureBlocking(false);// 3.綁定監聽端口serverSocketChannel.bind(new InetSocketAddress(9898));// 4.打開SelectorSelector selector =Selector.open();// 5.注冊監聽客戶端連接的通道serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);// 6.循環監聽連接等事件while (selector.select() > 0) {// 獲取事件迭代器Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();while (keyIterator.hasNext()) {// 獲取到具體事件SelectionKey key = keyIterator.next();// 判斷事件類型// 客戶端建立連接的事件if (key.isAcceptable()) {// 獲取到客戶端的連接通道SocketChannel socketChannel = serverSocketChannel.accept();// 設置通道為非阻塞模式socketChannel.configureBlocking(false);// 將連接通道注冊到選擇器socketChannel.register(selector, SelectionKey.OP_READ);}// 如果是讀取事件,證明客戶端發來了消息,進行數據讀取else if (key.isReadable()) {try {// 創建緩沖區ByteBuffer byteBuffer = ByteBuffer.allocate(1024);// 消息內容,正常聊天,消息一般很短,不會超過1kbString content = "";// 獲取到客戶端通道SocketChannel channel = (SocketChannel)key.channel();// 通過通道將數據讀取到bufferif (channel.read(byteBuffer) > 0) {// 切換為可讀模式byteBuffer.flip();// 將byte[]轉為stringcontent = new String(byteBuffer.array(), 0, byteBuffer.remaining());// 清空緩沖區byteBuffer.clear();}// 打印消息內容System.out.println(content);// 將數據寫入緩沖區byteBuffer.put(content.getBytes());// 將數據發送給其他所有非自己的客戶端Set<SelectionKey> keys = selector.keys();for (SelectionKey selectionKey : keys) {// 通過事件獲取到客戶端連接通道SelectableChannel selectableChannel = selectionKey.channel();// 如果通道是服務端監聽連接的通道ServerSocketChannel或者是自己,則不能轉發if (selectableChannel instanceof ServerSocketChannel || selectableChannel == channel) {continue;}// 將消息轉發給其他客戶端// 轉為客戶端channel類型SocketChannel otherClientChannel = (SocketChannel) selectableChannel;// 重新設置為讀模式byteBuffer.flip();// 發送消息otherClientChannel.write(byteBuffer);}} catch (Exception e) {// 出現異常,說明客戶端下線SocketChannel channel = (SocketChannel)key.channel();System.out.println("客戶端" + channel.getLocalAddress() + "離線");channel.close();// 釋放通道,接觸通道綁定的事件key.cancel();}}// 將使用完的事件進行移除keyIterator.remove();}}// 釋放資源serverSocketChannel.close();selector.close();}}
客戶端Client
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;public class Client {public static void main(String[] args) throws IOException {// 1.創建socket channelSocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));socketChannel.configureBlocking(false); // 設置為非阻塞模式// 2.開啟SelectorSelector selector = Selector.open();// 3.注冊通道,監聽通道的讀事件socketChannel.register(selector, SelectionKey.OP_READ);// 開啟一個接收服務端數據的線程new Thread(() -> {try {// 有事件,則執行處理,沒有事件則進行阻塞while(selector.select() > 0) {Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();// 遍歷事件,找到可讀事件,讀取服務端發來的數據while (keyIterator.hasNext()) {SelectionKey selectionKey = keyIterator.next();// 可讀事件,讀取服務端發來的數據if (selectionKey.isReadable()) {// 創建緩沖區ByteBuffer buffer = ByteBuffer.allocate(1024);// 讀取數據到緩沖區socketChannel.read(buffer);// 切換為讀模式buffer.flip();System.out.println("\n" + new String(buffer.array(), 0, buffer.remaining()));System.out.print("我的發言: ");}// 使用完畢的事件要進行移除keyIterator.remove();}}} catch (IOException e) {e.printStackTrace();}}).start();// 等待客戶的輸入,聊天控制臺ByteBuffer buffer = ByteBuffer.allocate(1024);Scanner sc = new Scanner(System.in);while (true) {System.out.print("我的發言: ");// 讀取用戶輸入String msg = socketChannel.getLocalAddress() + ": " + sc.nextLine();// 將消息放入緩沖區buffer.put(msg.getBytes());// 切換為寫模式buffer.flip();// 發送緩沖區的數據給服務噸socketChannel.write(buffer);// 清除本次數據buffer.clear();}}
}