1. 程式人生 > >Java NIO:通道

Java NIO:通道

最近打算把Java網路程式設計相關的知識深入一下(IO、NIO、Socket程式設計、Netty) Java NIO主要需要理解緩衝區、通道、選擇器三個核心概念,作為對Java I/O的補充, 以提升大批量資料傳輸的效率。 學習NIO之前最好能有基礎的網路程式設計知識 [Java I/O流](https://www.cnblogs.com/pepper-0611/p/13735018.html) [Java 網路程式設計](https://www.cnblogs.com/pepper-0611/p/13767359.html) [Java NIO:緩衝區](https://www.cnblogs.com/pepper-0611/p/13767775.html) 通道(Channel)作為NIO的三大核心概念之一(緩衝區、通道、選擇器),用於在位元組緩衝區與位於通道另一側的實體(檔案或者套接字)之間有效的傳輸資料(**核心是傳輸資料**) NIO程式設計的一般模式是:把資料填充到傳送位元組緩衝區 --> 通過通道傳送到通道對端檔案或者套接字 ## 通道基礎 使用Channel的目的是進行資料傳輸,使用前需要開啟通道、使用後需要關閉通道 ### 開啟通道 我們知道I/O有兩大類:File IO和 Stream I/O,其對應到通道也就有檔案通道(FileChannel)和套接字通道(SocketChannel、ServerSocketChannel、DatagramChannel)兩種 對於套接字通道,使用靜態工廠方法開啟 ```java SocketChannel sc = SocketChannel.open(); ServerSocketChannel sc = ServerSocketChannel.open(); DatagramChannel sc = DatagramChannel.open(); ``` 對於檔案通道只能通過對一個RandomAccessFile、FileInputStream、FileOutputStream物件呼叫getChannel()方法獲取 ```java FileInputStream in = new FileInputStream("/tmp/a.txt"); FileChannel fc = in.getChannel(); ``` ### 使用通道進行資料傳輸 下段程式碼首先將要寫入的資料放到ByteBuffer中, 然後開啟檔案通道,把緩衝區中的資料放到檔案通道。 ```java //準備資料並放入位元組緩衝區 ByteBuffer bf = ByteBuffer.allocate(1024); bf.put("i am cool".getBytes()); bf.flip(); //開啟檔案通道 FileOutputStream out = new FileOutputStream("/tmp/a.txt"); FileChannel fc = out.getChannel(); //資料傳輸 fc.write(bf); //關閉通道 fc.close(); ``` ### 關閉通道 如同Socket、FileInputStream等物件使用完畢之後需要關閉一樣, 通道使用之後也需要關閉。一個開啟的通道代表與一個特定I/O服務的特定連線並封裝該連線的狀態,通道關閉時連線丟失,不再連線任何東西。 ### 阻塞 & 非阻塞模式 通道有阻塞和非阻塞兩種執行模式,非阻塞模式的通道永遠不會休眠,請求的操作要麼立即完成,要麼返回一個結果表明未進行任何操作(具體看Socket通道處的描述)。只有面向流的通道可使用非阻塞模式 ## 檔案通道 檔案通道用於對檔案進行訪問, 通過對一個RandomAccessFile、FileInputStream、FileOutputStream物件呼叫getChannel()方法獲取。呼叫getChannel方法返回一個連線到相同檔案的FileChannel物件,該FileChannel物件具有與file物件相同的訪問許可權。 ### 檔案訪問 使用檔案通道的目的還是對檔案進行讀寫操作,通道的讀寫api如下: ```java public abstract int read(ByteBuffer dst) throws IOException; public abstract int write(ByteBuffer src) throws IOException; ``` 下面是一段讀取檔案的Demo ```java //開啟檔案channel RandomAccessFile f = new RandomAccessFile("/tmp/a.txt", "r"); FileChannel fc = f.getChannel(); //從channel中讀取資料,直到檔案尾 ByteBuffer bb = ByteBuffer.allocate(1024); while (fc.read(bb) != -1) { ; } //翻轉(讀之前需要先進行翻轉) bb.flip(); StringBuilder builder = new StringBuilder(); //把每一個位元組轉為字元(ascii編碼) while (bb.hasRemaining()) { builder.append((char) bb.get()); } System.out.println(builder.toString()); ``` 上面這個demo有個問題:我們只能讀取位元組, 然後由應用程式去解碼,這個問題我們可以通過工具類Channels將通道包裝成Reader和Writer來解決;當然我們也可以直接使用Java I/O流模式的Reader和Writer操作字元 ### 檔案通道位置與檔案空洞 檔案通道位置(position)就是普通檔案的位置, position的值決定了檔案中哪個位置的資料接下來將被讀或者寫 讀取超出檔案尾部位置的資料會返回-1(檔案EOF) 往一個超出檔案尾部的位置寫入資料會造成檔案空洞:比如一個檔案現在有10個位元組, 但是此時往position=20 處寫入資料就會造成10~20之間的位置是沒有資料的,這就是檔案空洞 ### force操作 force操作強制通道將全部修改立即應用到磁碟檔案(防止系統宕機導致修改丟失) ```java public abstract void force(boolean metaData) throws IOException; ``` ## 記憶體檔案對映 FileChannel提供了一個map()方法,該方法可以在一個開啟的檔案和特殊型別的ByteBuffer(MappedByteBuffer)之間建立一個虛擬記憶體對映。 因為map方法返回的MappedByteBuffer物件是直接緩衝區,所以通過MappedByteBuffer來操作檔案非常高效(尤其是大量資料傳輸的情況) ### MappedByteBuffer的使用 通過MappedByteBuffer讀取檔案 ```java FileInputStream in = new FileInputStream("/tmp/a.txt"); FileChannel fc = in.getChannel(); MappedByteBuffer mbb = fc.map(MapMode.READ_ONLY, 0, fc.size()); StringBuilder builder = new StringBuilder(); while (mbb.hasRemaining()) { builder.append((char) mbb.get()); } System.out.println(builder.toString()); ``` ### MappedByteBuffer的三種模式 - READ_ONLY - READ_WRITE - PRIVATE 只讀和讀寫模式都好理解,PRIVATE模式下寫操作寫的是一個臨時緩衝區,不會真正去寫檔案。(**寫時拷貝思想**) ## Socket通道 Socket 通道可以執行在非阻塞模式且是可選擇的,這兩點使得對於網路程式設計我們不再需要為每個Socket連線建立一個執行緒,而是使用一個執行緒即可管理成百上千的Socket連線。 所有的Socket通道在例項化的時候都會建立一個物件的Socket物件, Socket通道並不負責協議相關的操作, 協議相關的操作都委派給對等socket物件(如SocketChannel物件委派給Socket物件) ### 非阻塞模式 相較於傳統Java Socket的阻塞模式,SocketChannel提供了非阻塞模式,以構建高效能的網路應用程式 非阻塞模式下,幾乎所有的操作都是立刻返回的。比如下面的SocketChannel執行在非阻塞模式下,connect操作會立即返回,如果success為true代表連線已經建立成功了, 如果success為false, 代表連線還在建立中(tcp連線需要一些時間)。 ```java //開啟Socket通道 SocketChannel ch = SocketChannel.open(); //非阻塞模式 ch.configureBlocking(false); //連線伺服器 boolean success = ch.connect(InetSocketAddress.createUnresolved("127.0.0.1", 7001)); //輪訓連線狀態, 如果連線還未建立就可以做一些別的工作 while (!ch.finishConnect()){ //dosomething else } //連線建立, 做正事 //do something; ``` ### ServerSocketChannel ServerSocketChannel與ServerSocket類似,只是可以執行在非阻塞模式下 下為一個通過ServerSocketChannel構建伺服器的簡單例子,主要體現了非阻塞模式,核心思想與ServerSocket類似 ```java ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.configureBlocking(false); ssc.bind(new InetSocketAddress(7001)); while (true){ SocketChannel sc = ssc.accept(); if(sc != null){ handle(sc); }else { Thread.sleep(1000); } } ``` ### SocketChannel 與 DatagramChannel SocketChannel 對應 Socket, 模擬TCP協議;DatagramChannel對應DatagramSocket, 模擬UDP協議 二者的使用與SeverSocketChannel大同小異,看API即可 ## 工具類 文體通道那裡我們提到過, 通過只能操作位元組緩衝區, 編解碼需要應用程式自己實現。如果我們想在通道上直接操作字元,我們就需要使用工具類Channels,工具類Channels提供了通道與流互相轉換、通道轉換為閱讀器書寫器的能力,具體API入下 ```java //通道 --> 輸入輸出流 public static OutputStream newOutputStream(final WritableByteChannel ch); public static InputStream newInputStream(final AsynchronousByteChannel ch); //輸入輸出流 --> 通道 public static ReadableByteChannel newChannel(final InputStream in); public static WritableByteChannel newChannel(final OutputStream out); //通道 --> 閱讀器書寫器 public static Reader newReader(ReadableByteChannel ch, String csName); public static Writer newWriter(WritableByteChannel ch, String csName); ``` 通過將通道轉換為閱讀器、書寫器我們就可以直接在通道上操作字元。 ```java RandomAccessFile f = new RandomAccessFile("/tmp/a.txt", "r"); FileChannel fc = f.getChannel(); //通道轉換為閱讀器,UTF-8編碼 Reader reader = Channels.newReader(fc, "UTF-8"); int i = 0, s = 0; char[] buff = new char[1024]; while ((i = reader.read(buff, s, 1024 - s)) != -1) { s += i; } for (i = 0; i < s; i++) { System.out.print(buff[i]); } ``` ## 總結 通道主要分為檔案通道和套接字通道。 對於檔案操作:如果是大檔案使用通道的檔案記憶體對映特性(MappedByteBuffer)來有利於提升傳輸效能, 否則我更傾向傳統的I/O流模式(字元API);對於套接字操作, 使用通道可以執行在非阻塞模式並且是可選擇的,利於構建高效能網路應用