掘金23 萬億數(shù)字經(jī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的選擇器允許一個單獨的線程來監(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 網(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 事件發(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)容。