Java NIO由淺入深(作者原創(chuàng))

      網(wǎng)友投稿 830 2022-05-29

      個人簡介

      NIO三大組件

      Java NIO的核心:通道(Channel)和緩沖區(qū)(Buffer),通道是用來傳輸數(shù)據(jù)的,緩沖區(qū)是存儲數(shù)據(jù)的。

      常見的Channel有以下四種,其中FileChannel主要用于文件傳輸,其余三種用于網(wǎng)絡(luò)通信。

      FileChannel

      SocketChannel

      DatagramChannel

      ServerSocketChannel

      Buffer有幾種,使用最多的是ByteBuffer

      ByteBuffer

      MappedByteBuffer

      DirectByteBuffer

      HeapByteBuffer

      ShortBuffer

      IntBuffer

      LongBuffer

      FloatBuffer

      DoubleBuffer

      CharBuffer

      8大基本數(shù)據(jù)類型除了boolean沒有Buffer,其余的7種基本類型都有

      未使用Selector之前,有如下幾種方案

      1.多線程技術(shù)

      實現(xiàn)邏輯 :每一個連接進來都開一個線程去處理Socket。

      缺點:

      如果同時有100000個(大量)連接進來,系統(tǒng)大概率是擋不住的,而且線程會占用內(nèi)存,會導(dǎo)致內(nèi)存不足。

      線程需要進行上下文切換,成本高

      2.采用線程池技術(shù)

      實現(xiàn)邏輯 :創(chuàng)建一個固定大小(系統(tǒng)能夠承載的線程數(shù))的線程池對象,去處理連接的請求,假如線程池大小為

      100個線程數(shù),這時候同時并發(fā)連接1000個Socket,此時只有100個Socket會得到處理,其余的會阻塞。這樣很好的防止了系統(tǒng)線程數(shù)

      過多導(dǎo)致線程占用內(nèi)存大,不容易導(dǎo)致系統(tǒng)由于內(nèi)存占用的問題而崩潰。

      相對于第一種多線程技術(shù)處理客戶端Socket,第二種方案使用線程池去處理連接會更好,但是還是不夠好

      缺點:

      阻塞模式下,線程僅能處理一個連接,若socket連接一直未斷開,則該線程無法處理其他socket。

      3.使用Selector選擇器

      selector的作用就是配合一個線程來管理多個channel,獲取這些 channel 上發(fā)生的事件,這些 channel 工作在非阻塞模式下,當一個channel中沒有執(zhí)行任務(wù)時,可以去執(zhí)行其他channel中的任務(wù)

      注意:fileChannel因為是阻塞式的,所以無法使用selector

      使用場景:適合連接數(shù)多,但流量較少的場景

      流程: 假如當前Selector綁定的Channels沒有任何一個Channel觸發(fā)了感興趣的事件,

      則selector的select()方法會阻塞線程,直到channel觸發(fā)了事件。這些事件發(fā)生后,select方法就會返回這些事件交給thread來處理。

      區(qū)別:

      IO是面向流的,NIO是面向緩沖區(qū)(塊)的

      Java IO的各種流是阻塞的,而Java NIO是非阻塞的

      Java NIO由淺入深(作者原創(chuàng))

      Java NIO的選擇器允許一個單獨的線程來監(jiān)視多個輸入通道

      普通io讀取文件

      @Test public void test01(){ try { FileInputStream fileInputStream = new FileInputStream("data.txt"); long start = System.currentTimeMillis(); byte bytes[]=new byte[1024]; int n=-1; while ((n=fileInputStream.read(bytes,0,1024))!=-1){ String s = new String(bytes,0,n,"utf-8"); System.out.println(s); } long end = System.currentTimeMillis(); System.out.println("普通io共耗時:"+(end-start)+"ms"); } catch (Exception e) { e.printStackTrace(); } }

      緩沖流IO讀取文件

      @Test public void test02(){ try { BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("data.txt")); long start = System.currentTimeMillis(); byte bytes[]=new byte[1024]; int n=-1; while ((n=bufferedInputStream.read(bytes,0,1024))!=-1){ String s = new String(bytes,0,n,"utf-8"); System.out.println(s); } long end = System.currentTimeMillis(); System.out.println("緩沖流io共耗時:"+(end-start)+"ms"); } catch (Exception e) { e.printStackTrace(); } }

      Nio-FileChannel讀取文件

      //方式1 @Test public void test3(){ try { //獲取channel,FileInputStream生成的channel只有讀的權(quán)利 FileChannel channel = new FileInputStream("data.txt").getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //開辟一塊緩沖區(qū) long start = System.currentTimeMillis(); while (true){ //寫入操作 int read = channel.read(byteBuffer); //如果read=-1,說明緩存“塊”沒有數(shù)據(jù)了 if(read==-1){ break; }else { byteBuffer.flip();//讀寫切換,切換為讀的操作,實質(zhì)上就是把limit=position,position=0 String de = StandardCharsets.UTF_8.decode(byteBuffer).toString(); System.out.println(de); byteBuffer.clear(); //切換為寫 } } long end = System.currentTimeMillis(); System.out.println("heap nio共耗時:"+(end-start)+"ms"); } catch (Exception e) { e.printStackTrace(); } } //方式2 @Test public void test4(){ ByteBuffer byteBuffer = ByteBuffer.allocate(10); byteBuffer.put("helloWorld".getBytes()); debugAll(byteBuffer); byteBuffer.flip(); //讀模式 while (byteBuffer.hasRemaining()){ System.out.println((char)byteBuffer.get()); } byteBuffer.flip(); System.out.println(StandardCharsets.UTF_8.decode(byteBuffer).toString()); }

      創(chuàng)建ByteBuffer緩沖區(qū):

      ByteBuffer.allocate(int capacity)

      ByteBuffer.allocateDirect(int capacity)

      ByteBuffer.wrap(byte[] array,int offset, int length)

      ByteBuffer常用方法:

      get()

      get(int index)

      put(byte b)

      put(byte[] src)

      limit(int newLimit)

      mark()

      reset()

      clear()

      flip()

      compact()

      字符串轉(zhuǎn)換成ByteBuffer

      ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode("hello world\nabc\n\baaa");

      ByteBuffer轉(zhuǎn)換成String

      String str = StandardCharsets.UTF_8.decode(byteBuffer).toString();

      整個Demo

      @Test public void test5(){ //字符串轉(zhuǎn)換成ByteBuffer ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode("hello world\nabc\n\baaa"); //通過StandardCharsets的encode方法獲得ByteBuffer,此時獲得的ByteBuffer為讀模式,無需通過flip切換模式 // byteBuffer.flip(); //這句話不能加,encode轉(zhuǎn)換成ByteBuffer默認是讀模式 while (byteBuffer.hasRemaining()){ System.out.printf("%c",(char)byteBuffer.get()); } byteBuffer.flip(); //ByteBuffer轉(zhuǎn)換成String String str = StandardCharsets.UTF_8.decode(byteBuffer).toString(); System.out.println("\n--------------"); System.out.println(str); }

      @Test public void test6(){ String msg = "hello,world\nI'm abc\nHo"; ByteBuffer byteBuffer = ByteBuffer.allocate(32); byteBuffer.put(msg.getBytes()); byteBuffer=splitGetBuffer(byteBuffer); byteBuffer.put("w are you?\n".getBytes()); //多段發(fā)送數(shù)據(jù) byteBuffer=splitGetBuffer(byteBuffer); byteBuffer.put("aa bccdd?\n".getBytes()); //多段發(fā)送數(shù)據(jù) byteBuffer=splitGetBuffer(byteBuffer); } private ByteBuffer splitGetBuffer(ByteBuffer byteBuffer) { byteBuffer.flip(); StringBuilder stringBuilder = new StringBuilder(); int index=-1; for (int i = 0; i < byteBuffer.limit(); i++) { if(byteBuffer.get(i)!='\n'){ //get(i)不會讓position+1 stringBuilder.append((char) byteBuffer.get(i)); }else{ index=i; //記錄最后一個分隔符下標 String data = stringBuilder.toString(); ByteBuffer dataBuf = ByteBuffer.allocate(data.length()); dataBuf.put(data.getBytes()); dataBuf.flip(); debugAll(dataBuf); dataBuf.clear(); stringBuilder=new StringBuilder(); } } ++index; ByteBuffer temp = ByteBuffer.allocate(byteBuffer.capacity()); for (;index

      文件編程

      因為FileChannel只能工作在阻塞環(huán)境下,而Selector是非阻塞的,所以FileChannel無法注冊到Selector里面去。

      FileChannel不能直接打開,一定要用FileInputStream或者FileOutputStream或者RandomAccessFile來獲取FileChannel對象,

      使用getChannel方法即可。

      注意以下幾點:

      通過FileInputStream獲取的channel只能讀

      通過FileOutputStream獲取的channel只能寫

      通過 RandomAccessFile 是否能讀寫根據(jù)構(gòu)造 RandomAccessFile 時的讀寫模式?jīng)Q定

      通過 FileInputStream 獲取channel,通過read方法將數(shù)據(jù)寫入到ByteBuffer中,read方法的返回值表示讀到了多少字節(jié),若讀到了文件末尾則返回-1

      int read = channel.read(buffer);

      因為channel也是有大小的,所以 write方法并不能保證一次將 buffer中的內(nèi)容全部寫入channel。必須需要按照以下規(guī)則進行寫入

      // 通過hasRemaining()方法查看緩沖區(qū)中是否還有數(shù)據(jù)未寫入到通道中 while(buffer.hasRemaining()) { channel.write(buffer); }

      操作系統(tǒng)出于性能的考慮,會將數(shù)據(jù)緩存,不是立刻寫入磁盤,而是等到緩存滿了以后將所有數(shù)據(jù)一次性的寫入磁盤。可以調(diào)用force(true)方法將文件內(nèi)容和元數(shù)據(jù)(文件的權(quán)限等信息)立刻寫入磁盤

      //方法一: FileInputStream fileInputStream = new FileInputStream("data.txt"); //讀的通道 FileChannel from = fileInputStream.getChannel(); FileOutputStream fileInputStream1 = new FileOutputStream("to.txt"); //寫的通道 FileChannel to = fileInputStream1.getChannel(); long l = from.transferTo(0, from.size(), to); //方法二: RandomAccessFile r1 = new RandomAccessFile("data.txt", "rw"); //都開啟rw權(quán)限 FileChannel from1 = r1.getChannel(); RandomAccessFile r2 = new RandomAccessFile("to.txt", "rw"); FileChannel to2 = r2.getChannel(); from1.transferTo(0,r1.length(),to2);

      使用transferTo方法可以快速、高效地將一個channel中的數(shù)據(jù)傳輸?shù)搅硪粋€channel中,但一次只能傳輸2G的內(nèi)容,

      transferTo方法的底層使用了零拷貝技術(shù),

      Path用來表示文件路徑

      Paths是工具類,用來獲取Path實例

      Path path = Paths.get("data.txt"); Path path1 = Paths.get("D:\\java code\\netty-study\\data.txt");

      Path path = Paths.get("data.txt"); boolean exists = Files.exists(path);

      createDirectory(path)

      如果文件夾已存在,則會報錯。FileAlreadyExistsException,

      此方法只能創(chuàng)建一級目錄,如果用此方法創(chuàng)建多級目錄則會報錯NoSuchFileException。

      Path path = Paths.get("D:\\img"); Path directory = Files.createDirectory(path);

      createDirectories(path)

      Path path = Paths.get("D:\\img\\a\\b"); Path directories = Files.createDirectories(path);

      //這種方式如果目標文件‘to’存在則會報錯FileAlreadyExistsException Path from = Paths.get("data.txt"); Path to = Paths.get("D:\\img\\target.txt"); //文件名也要寫 Files.copy(from,to); //只需要加StandardCopyOption.REPLACE_EXISTING就不會報錯,因為它會直接替換掉目標文件 Path from = Paths.get("data.txt"); Path path = Paths.get("D:\\img\\target.txt"); //文件名也要寫 Files.copy(from,path, StandardCopyOption.REPLACE_EXISTING);

      Path source = Paths.get("data.txt"); Path target = Paths.get("D:\\img\\target.txt"); Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);

      StandardCopyOption.ATOMIC_MOVE保證文件移動的原子性

      Path target = Paths.get("D:\\img\\target.txt"); Files.delete(target); //刪除文件

      walkFileTree(Path, FileVisitor)方法

      Path:文件起始路徑

      FileVisitor:文件訪問器,使用訪問者模式,這個接口有如下方法

      preVisitDirectory:訪問目錄前的操作

      visitFile:訪問文件的操作

      visitFileFailed:訪問文件失敗時的操作

      postVisitDirectory:訪問目錄后的操作

      Path target = Paths.get("D:\\cTest"); Files.walkFileTree(target,new SimpleFileVisitor(){ @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { System.out.println("1:"+dir); return super.preVisitDirectory(dir, attrs); } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { System.out.println("2:"+file); return super.visitFile(file, attrs); } });

      網(wǎng)絡(luò)編程

      這里有一段簡易的通信代碼:

      服務(wù)器端:

      ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //打開serverSocketChannel serverSocketChannel.bind(new InetSocketAddress(8080)); while (true){ System.out.println("waiting....."); SocketChannel socketChannel = serverSocketChannel.accept(); //阻塞 System.out.println("connect success"); ByteBuffer byteBuffer = ByteBuffer.allocate(100); socketChannel.read(byteBuffer); //阻塞,等待消息發(fā)送過來即可封裝到緩存里去 byteBuffer.flip(); System.out.println(StandardCharsets.UTF_8.decode(byteBuffer).toString()); }

      客戶端:

      SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress( 8080)); ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode("this is nio"); socketChannel.write(byteBuffer);

      實際上,這個和以前的IO+Socket進行通信是一樣的,都是屬于阻塞狀態(tài)。

      configureBlocking(false)

      可以通過ServerSocketChannel的configureBlocking(false)方法將獲得連接設(shè)置為非阻塞的。此時若沒有連接,accept會返回null,

      可以通過SocketChannel的configureBlocking(false)方法將從通道中讀取數(shù)據(jù)設(shè)置為非阻塞的。若此時通道中沒有數(shù)據(jù)可讀,read會返回-1

      服務(wù)器端:

      ByteBuffer byteBuffer = ByteBuffer.allocate(100); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //打開通道 serverSocketChannel.bind(new InetSocketAddress(8082)); //由于accept方法是阻塞的,我們只需要一行代碼就能讓它變成非阻塞的 //開啟非阻塞的之后accept方法如果沒有連接到客戶端就會從阻塞變成返回'null' serverSocketChannel.configureBlocking(false);//開啟非阻塞 while (true){ // System.out.println("waiting..."); SocketChannel socketChannel = serverSocketChannel.accept(); //阻塞方法 // System.out.println(socketChannel); if(socketChannel!=null){ System.out.println("等待讀取"); socketChannel.configureBlocking(false); //設(shè)置SocketChannel為非阻塞 int read = socketChannel.read(byteBuffer);//阻塞方法 System.out.println("讀取到"+read+"字節(jié)"); if(read>0){ byteBuffer.flip(); System.out.println(StandardCharsets.UTF_8.decode(byteBuffer).toString()); } } }

      客戶端:

      SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(8082)); ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode("hello"); socketChannel.write(byteBuffer);

      Selector是基于事件驅(qū)動的

      單線程可以配合Selector完成對多個Channel讀寫事件的監(jiān)控,這稱之為多路復(fù)用。

      注意:

      多路復(fù)用只能用于網(wǎng)絡(luò)IO上,文件IO由于只能處于阻塞環(huán)境下才能進行,所以無法多路復(fù)用

      如果不用Selector的非阻塞模式,線程大部分時間都在做無用功,而Selector能夠保證以下幾點

      有可連接事件時才去連接

      有可讀事件才去讀取

      有可寫事件才去寫入

      進入SelectionKey這個類可以看到:

      public static final int OP_READ = 1 << 0; //read事件 public static final int OP_WRITE = 1 << 2; //write事件 public static final int OP_CONNECT = 1 << 3; //connect事件 public static final int OP_ACCEPT = 1 << 4; //accept事件

      select()

      select方法會一直阻塞直到綁定事件發(fā)生

      服務(wù)器端:

      Selector selector = Selector.open(); // 創(chuàng)建選擇器 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(8081)); serverSocketChannel.configureBlocking(false); // 通道必須是非阻塞的 serverSocketChannel.register( selector, SelectionKey.OP_ACCEPT); // 把channel注冊到selector,并選擇accept事件 for (; ; ) { selector.select(); // 選擇事件,此時會阻塞,當事件發(fā)生時會自動解除阻塞 System.out.println("begin"); // 遍歷事件發(fā)生的集合,獲取對應(yīng)事件 selector .selectedKeys() .forEach( selectionKey -> { if (selectionKey.isAcceptable()) { try { SocketChannel socketChannel = serverSocketChannel.accept(); System.out.println("已連接"); // 處理完之后記得在發(fā)生事件的集合中移除該事件 selector.selectedKeys().remove(selectionKey); } catch (IOException e) { e.printStackTrace(); } } }); }

      原生NIO是真tmd難用,惡心

      當accept事件處理之后立刻設(shè)置read事件,但不處理read事件,因為用戶可能只是連接,但是沒有寫數(shù)據(jù),所以要基于事件觸發(fā)

      別忘了accept事件處理之后要設(shè)置為非阻塞模式configureBlocking(false)

      Selector selector = Selector.open(); // 創(chuàng)建選擇器 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(8081)); serverSocketChannel.configureBlocking(false); // 通道必須是非阻塞的 serverSocketChannel.register( selector, SelectionKey.OP_ACCEPT); // 把channel注冊到selector,并選擇accept事件 try { while (true) { int count = selector.select(); // 選擇事件,此時會阻塞,當事件發(fā)生時會自動解除阻塞 Set selectionKeys = selector.selectedKeys(); Iterator iterator = selectionKeys.iterator(); while (iterator.hasNext()){ SelectionKey selectionKey = iterator.next(); if (selectionKey.isAcceptable()) { // 處理accept事件 try { ServerSocketChannel serverSocket = (ServerSocketChannel) selectionKey.channel(); System.out.println("已連接"); SocketChannel socketChannel = serverSocket.accept(); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); // 讀事件 iterator.remove(); } catch (IOException e) { } } else if (selectionKey.isReadable()) { // 處理read事件 // 獲取socketChannel,實際上這個channel就是上面注冊進selector的對象 SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(100); try{ int read = socketChannel.read(byteBuffer); System.out.println("read:"+read); }catch (Exception e){ // e.printStackTrace(); continue; //一定要這樣寫。。。。。。。防止多次read報錯 } byteBuffer.flip(); debugAll(byteBuffer); byteBuffer.clear(); iterator.remove(); } } } } catch (Exception e) { e.printStackTrace(); }

      事件發(fā)生后,要么處理,要么取消(cancel),不能什么都不做,否則下次該事件仍會觸發(fā)

      事件處理之后一定要把selector.selectedKeys這個集合中當前處理完成的事件remove掉

      零拷貝指的是數(shù)據(jù)無需拷貝到JVM內(nèi)存中,同時具有以下三個優(yōu)點:

      更少的用戶態(tài)與內(nèi)核態(tài)的切換

      不利用cpu計算,減少cpu緩存?zhèn)喂蚕?/p>

      零拷貝適合小文件傳輸

      使用DirectByteBuffer

      ByteBuffer.allocate(10)底層對應(yīng) HeapByteBuffer,使用的還是Java堆內(nèi)存

      ByteBuffer.allocateDirect(10)底層對應(yīng)DirectByteBuffer,使用的是操作系統(tǒng)內(nèi)存,不過需要手動釋放內(nèi)存

      優(yōu)點:

      減少了一次數(shù)據(jù)拷貝,用戶態(tài)與內(nèi)核態(tài)的切換次數(shù)沒有減少

      這塊內(nèi)存不受 JVM 垃圾回收的影響,因此內(nèi)存地址固定,有助于 IO 讀寫

      Java 調(diào)用 transferTo 方法后,要從 Java 程序的用戶態(tài)切換至內(nèi)核態(tài),使用 DMA將數(shù)據(jù)讀入內(nèi)核緩沖區(qū),不會使用 CPU

      只會將一些 offset 和 length 信息拷入 socket 緩沖區(qū),幾乎無消耗

      使用 DMA 將 內(nèi)核緩沖區(qū)的數(shù)據(jù)寫入網(wǎng)卡,不會使用 CPU

      整個過程僅只發(fā)生了1次用戶態(tài)與內(nèi)核態(tài)的切換,數(shù)據(jù)拷貝了 2 次

      Java 任務(wù)調(diào)度

      版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內(nèi)刪除侵權(quán)內(nèi)容。

      上一篇:一文教你快速了解并安裝IntelliJ IDEA及其目錄介紹
      下一篇:安裝Redis就那么幾步,很簡單!
      相關(guān)文章
      亚洲av无码日韩av无码网站冲| 亚洲成无码人在线观看| 亚洲精品第一综合99久久| 亚洲精品国产肉丝袜久久| 久久国产亚洲电影天堂| 亚洲国产成人片在线观看 | 亚洲国产精品无码AAA片| 国产偷国产偷亚洲清高动态图| 亚洲中文字幕视频国产| 久久亚洲高清综合| 国产亚洲精品成人AA片新蒲金 | 亚洲女人影院想要爱| 亚洲一区二区三区无码国产| 亚洲人成在线中文字幕| 久久久久精品国产亚洲AV无码| 亚洲天堂福利视频| 亚洲mv国产精品mv日本mv| 亚洲狠狠成人综合网| 精品国产日韩久久亚洲| 亚洲欧洲无码一区二区三区| 亚洲精品女同中文字幕| 国产精品亚洲精品日韩电影| 亚洲国产婷婷综合在线精品| 最新精品亚洲成a人在线观看| 国产亚洲AV无码AV男人的天堂| 久久亚洲国产精品| 亚洲精品免费在线视频| 亚洲乱码无限2021芒果| 一本天堂ⅴ无码亚洲道久久| 亚洲av无码兔费综合| 区三区激情福利综合中文字幕在线一区亚洲视频1 | 亚洲国产精品一区| 亚洲婷婷在线视频| 亚洲狠狠色丁香婷婷综合| www亚洲精品少妇裸乳一区二区| 亚洲一区视频在线播放| 久久亚洲精品成人777大小说| 亚洲美女免费视频| 亚洲中文字幕无码av| 日韩亚洲翔田千里在线| 国产亚洲精品线观看动态图|