java基礎之NIO介紹及使用
java.nio全稱java non-blocking IO,是指jdk1.4 及以上版本里提供的新api(New IO) ,為所有的原始類型(boolean類型除外)提供緩存支持的數據容器,使用它可以提供非阻塞式的高伸縮性網絡。
二、三大組件NIO三大組件:Channel、Buffer、Selector
1.Channel 和Buffer
Channel是一個對象,可以通過它讀取和寫入數據。拿 NIO 與原來的 I/O 做個比較,通道就像是流,而且他們面向緩沖區(Buffer)的。所有數據都通過Buffer對象來處理,永遠不會將字節直接寫入通道中,而是將數據寫入包含一個或多個字節的緩沖區。也不會直接從通道中讀取字節,而是將數據從通道讀入緩沖區,再從緩沖區獲取這個字節。
Channel是讀寫數據的雙向通道,可以從Channel將數據讀取Buffer,也可將Buffer的數據寫入Channel,而之前的Stream要么是輸入(InputStream)、要么是輸出(OutputStream),只在一個方向上流通。 而通道(Channel)可以用于讀、寫或者同時用于讀寫
常見的Channel
1.FileChannel
2.DatagramChannel
3.SocketChannel
4.ServerSocketChannel
Buffer緩沖區用來讀寫數據,常見的Buffer
1.ByteBuffer
2.ShortBuffer
3.IntBuffer
4.LongBuffer
5.FloatBuffer
6.DoubleBuffer
7.CharBuffer
2.Selector
在多線程模式下,阻塞IO時,一個線程只能處理一個請求,比如http請求,當請求響應式關閉連接,釋放線程資源。Selector選擇器的作用就是配合一個線程來管理多個Channel,獲取這些Channel上發生的事件,這些Channel工作在非阻塞模式下,不會讓線程一直在一個Channel上,適合連接數特別多,但流量低的場景。
調用Selector的select()方法會阻塞直到Channel發送了讀寫就緒事件,這些事件發生,select()方法就會
返回這些事件交給thread來處理。
三、ByteBuffer的使用屬性:
Buffer的讀寫操作就是通過改變position,mark,limit的值來實現。注意他們之間的關系可以很輕松的讀、寫、覆蓋。
position:對于寫入模式,表示當前可寫入數據的下標,對于讀取模式,表示接下來可以讀取的數據的下標。當前操作位置的下標。position()方法獲取。 mark:用來標志當前操作位置,調用mark()方法,使mark = position,可以在讀和寫切換過程中標記前一個操作下標位置。 limit:Buffer的限界值。對于寫模式,相當于可寫入的最大值,數組長度。對于讀模式,表示最多可以讀取的數據的位置下標,通過flip()方法進行讀寫切換,原理改變position,mark,limit的值。 capacity:數組容量大小方法:
Buffer的方法全是根據改變position的值進行操作的。
put():put方法寫入數據,可以單個byte字節,或者byte數組。或者其它類型,根據Buffer實例而定。 get():get方法讀取數據,可以傳入byte數組和不傳參讀取一個字節。 mark():標記當前下標position位置,mark = position 。讀寫操作切換或者特殊要求時,標記當前的下標位置。 reset():將position 值重置為mark()方法標記的值。 array():Buffer內數據的byte數組。沒有值的位用0補位。 flip():limit為position值,將position置為0,mark初始值,寫入操作切換為讀操作。 rewind():將position 和 mark設為初始值,調用這個可以一直讀取內容或者一直寫入覆蓋之前內容,從第一位開始讀/寫。 clear():將屬性值還原。之前array()數組的值還在。 hasRemaining():判斷是否到最后四、測試Demoprivate static void buffer1() { String data = 'abc'; //byte[] bytes = new byte[1024]; //創建一個字節緩沖區,1024byte ByteBuffer byteBuffer = ByteBuffer.allocate(15); System.out.println(byteBuffer.toString()); // 標記當前下標position位置,mark = position ,返回當前對象 System.out.println(byteBuffer.mark()); //對于寫入模式,表示當前可以寫入的數組大小,默認為數組的最大長度,對于讀取模式,表示當前最多可以讀取的數據的位置下標 System.out.println(byteBuffer.limit()); // 對于寫入模式,表示當前可寫入數據的下標,對于讀取模式,表示接下來可以讀取的數據的下標 System.out.println(byteBuffer.position()); //capacity 表示當前數組的容量大小 System.out.println(byteBuffer.capacity()); //將字節數據寫入 byteBuffer byteBuffer.put(data.getBytes()); //保存了當前寫入的數據 for(Byte b : byteBuffer.array()){System.out.print(b + ' '); } System.out.println(); System.out.println(new String(byteBuffer.array())); //讀寫模式切換 改變 limit,position ,mark的值 byteBuffer.flip(); //切換為寫模式,將第一個字節覆蓋 byteBuffer.put('n'.getBytes()); // abc 改為 nbc System.out.println(new String(byteBuffer.array())); //讓position為之前標記的值,調用mark()方法是將mark = position,這里將position = mark,mark為初始值拋出異常 byteBuffer.mark(); byteBuffer.reset(); //將position 和 mark設為初始值,調用這個可以一直讀取內容或者一直寫入覆蓋之前內容,從第一位開始讀/寫 byteBuffer.rewind(); for(Byte b : byteBuffer.array()){System.out.print(b + ' '); } System.out.println(); System.out.println(byteBuffer.toString()); //找到可寫入的開始位置,不覆蓋之前的數據 byteBuffer.compact(); System.out.println(byteBuffer.toString());}
寫入讀取完整操作
private static void buffer(){ //寫入的數據 String data = '1234'; //創建一個字節緩沖區,1024byte ByteBuffer byteBuffer = ByteBuffer.allocate(15); //寫入數據 byteBuffer.put(data.getBytes()); //打輸出 49 50 51 52 0 0 0 0 0 0 0 0 0 0 0 println(byteBuffer.array()); byteBuffer.put((byte) 5); //追加寫入 輸出: 49 50 51 52 5 0 0 0 0 0 0 0 0 0 0 println(byteBuffer.array()); //覆蓋寫入 byteBuffer.flip(); //將寫入下標的 position = 0 byteBuffer.put((byte) 1); byteBuffer.put((byte) 2); byteBuffer.put((byte) 3); byteBuffer.put((byte) 4); byteBuffer.put((byte) 5); // 打印輸出 : 1 2 3 4 5 0 0 0 0 0 0 0 0 0 0 println(byteBuffer.array()); //讀取數據操作 byteBuffer.flip();//從頭開始讀 while (byteBuffer.position() != byteBuffer.limit()){System.out.println(byteBuffer.get()); } //此時 position 和 數據數 limit相等 System.out.println(byteBuffer.toString()); //批量讀取 byteBuffer.flip(); //將 position 置位0,從頭開始操作 //創建一個byte數組,數組大小為可讀取的大小 byte[] bytes = new byte[byteBuffer.limit()]; byteBuffer.get(bytes); //輸出bytes 1 2 3 4 5 println(bytes);} private static void println(byte[] bytes){ for(Byte b : bytes){ System.out.print(b + ' '); } System.out.println();}
字符串跟ByteBuffer之間的轉換
public static void main(String[] args) { /*======================字符串轉buffer===========================*/ //1.字符串 轉為buytebuffer 需要轉為讀模式再進行讀取操作 ByteBuffer buffer = ByteBuffer.allocate(1024); buffer.put('nio'.getBytes()); //2.charset 自動轉為讀模式 ByteBuffer buffer1 = StandardCharsets.UTF_8.encode('nio'); //3, warp 自動轉為讀模式 ByteBuffer buffer2 = ByteBuffer.wrap('nio'.getBytes()); /*======================buffer轉字符串===========================*/ String str = StandardCharsets.UTF_8.decode(buffer1).toString(); System.out.println(str);}五、Channel的使用
文件編程FileChannel
FileChannel只能工作在阻塞模式下,不能配合在Selector使用,Channel是雙向通道,但是FileChannel根據獲取源頭判定可讀或可寫
FileInputStream獲取,只可讀 FileOutputStream獲取,只可寫 RandomAccessFile獲取,可讀、可寫(雙向通道)** * 文件流對象打開Channel,FileChannel是阻塞的 * @throws Exception */private static void channel() throws Exception{ FileInputStream in = new FileInputStream('C:UserszqqDesktop123.txt'); FileOutputStream out = new FileOutputStream('C:UserszqqDesktop321.txt'); //通過文件輸入流創建通道channel FileChannel channel = in.getChannel(); //獲取FileChannel FileChannel outChannel = out.getChannel(); //創建緩沖區byteBuffer ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //將管道channel中數據讀取緩存區byteBuffer中,channel只能與Buffer交互 while (channel.read(byteBuffer) != -1){//position設置為0,從頭開始讀byteBuffer.flip();outChannel.write(byteBuffer);//byteBuffer 屬性還原byteBuffer.clear(); } //關閉各種資源 channel.close(); out.flush(); out.close(); in.close(); out.close();}
/** * 靜態方法打開Channel * @throws Exception */public static void channel1() throws Exception{ // StandardOpenOption.READ :讀取一個已存在的文件,如果不存在或被占用拋出異常 // StandardOpenOption.WRITE :以追加到文件頭部的方式,寫入一個已存在的文件,如果不存在或被占用拋出異常 // StandardOpenOption.APPEND:以追加到文件尾部的方式,寫入一個已存在的文件,如果不存在或被占用拋出異常 // StandardOpenOption.CREATE:創建一個空文件,如果文件存在則不創建 // StandardOpenOption.CREATE_NEW:創建一個空文件,如果文件存在則報錯 // StandardOpenOption.DELETE_ON_CLOSE:管道關閉時刪除文件 //創建讀通道 FileChannel inChannel = FileChannel.open(Paths.get('C:UserszqqDesktop123.txt'), StandardOpenOption.READ); FileChannel outChannel = FileChannel.open(Paths.get('C:UserszqqDesktop321.txt'), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE); //內存映射 MappedByteBuffer inByteBuffer = inChannel.map(FileChannel.MapMode.READ_ONLY,0,inChannel.size()); MappedByteBuffer outByteBuffer = outChannel.map(FileChannel.MapMode.READ_WRITE,0,inChannel.size()); //直接對緩沖區數據讀寫 byte[] bytes = new byte[inByteBuffer.limit()]; inByteBuffer.get(bytes);//讀取inByteBuffer內的數據,讀到bytes數組中 outByteBuffer.put(bytes);//寫入到outByteBuffer inChannel.close(); outChannel.close();}
RandomAccessFile打開通道Channel
/** * 通過RandomAccessFile獲取雙向通道 * @throws Exception */private static void random() throws Exception{ try (RandomAccessFile randomAccessFile = new RandomAccessFile('D:workspaceDemotest.txt','rw')){//獲取ChannelFileChannel fileChannel = randomAccessFile.getChannel();//截取字節//fileChannel.truncate(10);//創建ByteBuffer,注意文件大小ByteBuffer byteBuffer = ByteBuffer.allocate(1024);fileChannel.read(byteBuffer);System.out.println(new String(byteBuffer.array(),'GBK'));byteBuffer.clear();String data = 'this is datar';byteBuffer.put(data.getBytes());byteBuffer.flip();//讀寫切換while (byteBuffer.hasRemaining()){ fileChannel.write(byteBuffer);}//將通道數據強制寫到磁盤fileChannel.force(true); }}
FileChannel數據傳輸transferTo
/** * 一個文件向另一個文件傳輸(copy) */private static void transferTo() { try ( FileChannel inChannel = new FileInputStream('C:Users44141Desktopdemoin.txt').getChannel(); FileChannel outChannel = new FileOutputStream('C:Users44141Desktopdemoout.txt').getChannel() ){//底層使用操作系統零拷貝進行優化,效率高。限制2ginChannel.transferTo(0,inChannel.size(),outChannel); }catch (Exception e){e.printStackTrace(); }}/** * 大于2g數據 */private static void transferToGt2g() { try ( FileChannel inChannel = new FileInputStream('C:Users44141Desktopdemoin.txt').getChannel(); FileChannel outChannel = new FileOutputStream('C:Users44141Desktopdemoout1.txt').getChannel() ){//記錄inChannel初始化大小long size = inChannel.size();//多次傳輸for(long rsize = size; rsize > 0;){ //transferTo返回位移的字節數 rsize -= inChannel.transferTo((size-rsize),rsize,outChannel);} }catch (Exception e){e.printStackTrace(); }}六、網絡編程
阻塞模式
阻塞模式服務端每個方法都會阻塞等待客戶端操作。比如accept()方法阻塞等待客戶端連接,read()方法阻塞等待客戶端傳送數據,并發訪問下沒法高效的進行工作。
1.服務端代碼
private static void server() throws Exception{ //1.創建服務器 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //2.綁定監聽端口 serverSocketChannel.bind(new InetSocketAddress(8080)); while (true){System.out.println('開始監聽連接=============' );//4.accept 監聽進來的連接 返回SocketChannel對象,accept默認阻塞SocketChannel socketChannel = serverSocketChannel.accept();System.out.println('有連接連入===============' );ByteBuffer byteBuffer = ByteBuffer.allocate(1024);// read()是阻塞方法,等客戶端發送數據才會執行socketChannel.read(byteBuffer);byteBuffer.flip();String str = StandardCharsets.UTF_8.decode(byteBuffer).toString();System.out.println('接收到數據=============:' + str); }}
2.客戶端代碼
private static void client() throws Exception { //1.創建客戶端 SocketChannel socketChannel = SocketChannel.open(); //連接服務端 socketChannel.connect(new InetSocketAddress('localhost',8080)); //2 秒后寫入數據 Thread.sleep(2 * 1000); socketChannel.write(StandardCharsets.UTF_8.encode('nio')); System.out.println();}
非阻塞模式
服務端設置成非阻塞模式。客戶端不用動。
private static void server() throws Exception{ ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //設置成非阻塞模式 serverSocketChannel.configureBlocking(false); serverSocketChannel.bind(new InetSocketAddress(8080)); while (true){SocketChannel socketChannel = serverSocketChannel.accept();//非阻塞模式下,SocketChannel會返回為nullif(socketChannel != null){ //非阻塞模式 socketChannel.configureBlocking(false); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); socketChannel.read(byteBuffer); byteBuffer.flip(); String str = StandardCharsets.UTF_8.decode(byteBuffer).toString(); System.out.println('接收到數據=============:' + str);} }}七、Selector
Selector選擇器的作用就是配合一個線程來管理多個Channel,被Selector管理的Channel必須是非阻塞模式下的,所以Selector沒法與FileChannel(FileChannel只有阻塞模式)一起使用。
創建Selector
創建Selector只需要調用一個靜態方法
//創建SelectorSelector selector = Selector.open();
Selector可以用來監聽Channel的事件,總共有四個事件:
accept:會在有連接請求時觸發,靜態常量 SelectionKey.OP_ACCEPT connect:客戶端建立連接后觸發,靜態常量 SelectionKey.OP_CONNECT read:可讀時觸發,靜態常量 SelectionKey.OP_READ write:可寫時觸發,靜態常量 SelectionKey.OP_WRITESelector與Channel綁定
//設置成非阻塞模式serverSocketChannel.configureBlocking(false);SelectionKey selectionKey = serverSocketChannel.register(selector,0,null);//綁定關注的事件selectionKey.interestOps(SelectionKey.OP_ACCEPT);八、網絡編程(多路復用)
1.客戶端代碼 SocketChannel
public static void main(String[] args) throws Exception { client();}private static void client() throws Exception { //1.創建客戶端 SocketChannel socketChannel = SocketChannel.open(); //連接服務端 socketChannel.connect(new InetSocketAddress('localhost',8080)); //2 秒后寫入數據 Thread.sleep(2 * 1000); socketChannel.write(StandardCharsets.UTF_8.encode('nio')); //3.讀取服務端返回數據 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); socketChannel.read(byteBuffer); byteBuffer.flip(); System.out.println('服務端返回數據=======:' + StandardCharsets.UTF_8.decode(byteBuffer).toString()); //斷開連接 socketChannel.close();}
2.服務端代碼
public static void main(String[] args) throws Exception{ server();}private static void server() throws Exception{ //創建Selector Selector selector = Selector.open(); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //設置成非阻塞模式 serverSocketChannel.configureBlocking(false); //將Channel注冊到selector上(綁定關系) //當事件發生后可以根據SelectionKey知道哪個事件和哪個Channel的事件 SelectionKey selectionKey = serverSocketChannel.register(selector,0,null); //selectionKey 關注ACCEPT事件(也可以在上面注冊時用第二個參數設置) selectionKey.interestOps(SelectionKey.OP_ACCEPT); serverSocketChannel.bind(new InetSocketAddress(8080)); while (true){System.out.println('阻塞====================');// select方法沒有事件發生時阻塞線程,有事件線程會恢復運行selector.select();System.out.println('開始處理事件=================');// 處理事件Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()){ SelectionKey sk = iterator.next(); //獲取到SelectionKey對象之后,將集合內的引用刪掉(Selecotr不會自動刪除) iterator.remove(); //取消事件,不操作(不處理或者不取消事件,selector.select()方法不會阻塞) //sk.cancel(); //區分不同的事件做不同的動作 if(sk.isAcceptable()){ //有連接請求事件//通過SelectionKey 獲取對應的channelServerSocketChannel channel = (ServerSocketChannel) sk.channel();SocketChannel socketChannel = channel.accept();socketChannel.configureBlocking(false);//讀取數據內容,bytebuffer大小注意消息邊界(一個字符串被分割讀取多次出來結果不準確)ByteBuffer byteBuffer = ByteBuffer.allocate(1024);//將socketChannel綁定SelectorSelectionKey socketKey = socketChannel.register(selector,0,byteBuffer);//關注可讀事件socketKey.interestOps(SelectionKey.OP_READ); }else if(sk.isReadable()){//可讀事件try { //取到Channel SocketChannel socketChannel = (SocketChannel) sk.channel(); //獲取到綁定的ByteBuffer ByteBuffer byteBuffer = (ByteBuffer) sk.attachment(); int read = socketChannel.read(byteBuffer); //如果是正常斷開 read = -1 if(read == -1){//取消事件sk.cancel();continue; } byteBuffer.flip(); String str = StandardCharsets.UTF_8.decode(byteBuffer).toString(); System.out.println('服務端讀取到數據===========:' + str); //寫數據回客戶端 ByteBuffer writeBuffer = StandardCharsets.UTF_8.encode('this is result'); socketChannel.write(writeBuffer); //如果數據一次沒寫完關注可寫事件進行再次寫入(大數據一次寫不完的情況) if(writeBuffer.hasRemaining()){//關注可寫事件,添加事件,用interestOps()方法獲取到之前的事件加上可寫事件(類似linux系統的賦權限 777)sk.interestOps(sk.interestOps() + SelectionKey.OP_WRITE);sk.attach(writeBuffer);//位運算符也可以//sk.interestOps(sk.interestOps() | SelectionKey.OP_WRITE); }}catch (IOException e){ e.printStackTrace(); //客戶端異常斷開連接 ,取消事件 sk.cancel();} }else if(sk.isWritable()){//取到ChannelSocketChannel socketChannel = (SocketChannel) sk.channel();//獲取到綁定的ByteBufferByteBuffer writeBuffer = (ByteBuffer) sk.attachment();socketChannel.write(writeBuffer);//如果全部寫完,取消可寫事件綁定,解除writeBuffer綁定if(!writeBuffer.hasRemaining()){ sk.attach(null); //取消可寫事件 sk.interestOps(sk.interestOps() - SelectionKey.OP_WRITE);} }} }}
到此這篇關于java基礎之NIO介紹及使用的文章就介紹到這了,更多相關Java NIO詳解內容請搜索好吧啦網以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持好吧啦網!
相關文章: