一、NIO三大組件
NIO的三大組件分別是Channel,Buffer與Selector
Java NIO系統的核心在于:通道(Channel)和緩沖區(Buffer)。通道表示打開到 IO 設備(例如:文件、套接字)的連接。若需要使用 NIO 系統,需要獲取用于連接 IO 設備的通道以及用于容納數據的緩沖區。然后操作緩沖區,對數據進行處理
簡而言之,通道負責傳輸,緩沖區負責存儲
常見的Channel有以下四種,其中FileChannel主要用于文件傳輸,其余三種用于網絡通信
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
Buffer有以下幾種,其中使用較多的是ByteBuffer
- ByteBuffer
- MappedByteBuffer
- DirectByteBuffer
- HeapByteBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
- CharBuffer
1、Selector
在使用Selector之前,處理socket連接還有以下兩種方法
1.使用多線程技術
為每個連接分別開辟一個線程,分別去處理對應的socke連接
這種方法存在以下幾個問題
- 內存占用高
- 每個線程都需要占用一定的內存,當連接較多時,會開辟大量線程,導致占用大量內存
- 線程上下文切換成本高
- 只適合連接數少的場景
- 連接數過多,會導致創建很多線程,從而出現問題
2.使用線程池技術
使用線程池,讓線程池中的線程去處理連接
這種方法存在以下幾個問題
- 阻塞模式下,線程僅能處理一個連接
- 線程池中的線程獲取任務(task)后,只有當其執行完任務之后(斷開連接后),才會去獲取并執行下一個任務
- 若socke連接一直未斷開,則其對應的線程無法處理其他socke連接
- 僅適合短連接場景
- 短連接即建立連接發送請求并響應后就立即斷開,使得線程池中的線程可以快速處理其他連接
3.使用選擇器
selector 的作用就是配合一個線程來管理多個 channel(fileChannel因為是阻塞式的,所以無法使用selector),獲取這些 channel 上發生的事件,這些 channel 工作在非阻塞模式下,當一個channel中沒有執行任務時,可以去執行其他channel中的任務。適合連接數多,但流量較少的場景
若事件未就緒,調用 selector 的 select() 方法會阻塞線程,直到 channel 發生了就緒事件。這些事件就緒后,select 方法就會返回這些事件交給 thread 來處理
2、ByteBuffer
使用案例
使用方式
- 向 buffer 寫入數據,例如調用 channel.read(buffer)
- 調用 flip() 切換至讀模式
- flip會使得buffer中的limit變為position,position變為0
- 從 buffer 讀取數據,例如調用 buffer.get()
- 調用 clear() 或者compact()切換至寫模式
- 調用clear()方法時position=0,limit變為capacity
- 調用compact()方法時,會將緩沖區中的未讀數據壓縮到緩沖區前面
- 重復以上步驟
使用ByteBuffer讀取文件中的內容
public class TestByteBuffer {public static void main(String[] args) {// 獲得FileChanneltry (FileChannel channel = new FileInputStream("stu.txt").getChannel()) {// 獲得緩沖區ByteBuffer buffer = ByteBuffer.allocate(10);int hasNext = 0;StringBuilder builder = new StringBuilder();while((hasNext = channel.read(buffer)) > 0) {// 切換模式 limit=position, position=0buffer.flip();// 當buffer中還有數據時,獲取其中的數據while(buffer.hasRemaining()) {builder.append((char)buffer.get());}// 切換模式 position=0, limit=capacitybuffer.clear();}System.out.println(builder.toString());} catch (IOException e) {}}
}
打印結果
核心屬性
0123456789abcdef
字節緩沖區的父類Buffer中有幾個核心屬性,如下
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
- capacity:緩沖區的容量。通過構造函數賦予,一旦設置,無法更改
- limit:緩沖區的界限。位于limit 后的數據不可讀寫。緩沖區的限制不能為負,并且不能大于其容量
- position:下一個讀寫位置的索引(類似PC)。緩沖區的位置不能為負,并且不能大于limit
- mark:記錄當前position的值。position被改變后,可以通過調用reset() 方法恢復到mark的位置。
以上四個屬性必須滿足以下要求
mark <= position <= limit <= capacity
核心方法
ut()方法
- put()方法可以將一個數據放入到緩沖區中。
- 進行該操作后,postition的值會+1,指向下一個可以放入的位置。capacity = limit ,為緩沖區容量的值。
flip()方法
- flip()方法會切換對緩沖區的操作模式,由寫->讀 / 讀->寫
- 進行該操作后
- 如果是寫模式->讀模式,position = 0 , limit 指向最后一個元素的下一個位置,capacity不變
- 如果是讀->寫,則恢復為put()方法中的值
get()方法
- get()方法會讀取緩沖區中的一個值
- 進行該操作后,position會+1,如果超過了limit則會拋出異常
- 注意:get(i)方法不會改變position的值
如果想通過get方法重復讀取數據
- 可以調用rewind方法講position重新置為0
- 或者調用get(int i)方法獲取索引 i 的內容,它不會移動讀指針
rewind()方法
- 該方法只能在讀模式下使用
- rewind()方法后,會恢復position、limit和capacity的值,變為進行get()前的值
clean()方法
- clean()方法會將緩沖區中的各個屬性恢復為最初的狀態,position = 0, capacity = limit
- 此時緩沖區的數據依然存在,處于“被遺忘”狀態,下次進行寫操作時會覆蓋這些數據
mark()和reset()方法
- mark()方法會將postion的值保存到mark屬性中
- reset()方法會將position的值改為mark中保存的值
compact()方法
此方法為ByteBuffer的方法,而不是Buffer的方法
- compact會把未讀完的數據向前壓縮,然后切換到寫模式
- 數據前移后,原位置的值并未清零,寫時會覆蓋之前的值
clear() VS compact()
clear只是對position、limit、mark進行重置,而compact在對position進行設置,以及limit、mark進行重置的同時,還涉及到數據在內存中拷貝(會調用arraycopy)。所以compact比clear更耗性能。但compact能保存你未讀取的數據,將新數據追加到為讀取的數據之后;而clear則不行,若你調用了clear,則未讀取的數據就無法再讀取到了
ByteBuffer.allocate() 與 ByteBuffer.allocateDirect()?
allocate()方法返回類型是? class java.nio .HeapByteBuffer
HeapByteBuffer?使用的是?java 堆內存,讀寫效率較低,受到GC的影響
allocateDirect()?方法返回類型是? class java.nio .DirectByteBuffer
DirectByteBuffer?使用的是直接內存, 讀寫效率高(少一次拷貝),不會受GC影響,但是分配的效率低(因為需要調用系統的分配內存相關接口),而且使用不當會造成內存泄漏
所以需要根據情況來判斷使用哪種方法進行模式切換
方法調用及演示
ByteBuffer調試工具類
需要先導入netty依賴
<dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>4.1.51.Final</version>
</dependency>
創建一個工具類,便于觀察ByteBuffer的內部結構
import java.nio.ByteBuffer;import io.netty.util.internal.MathUtil;
import io.netty.util.internal.StringUtil;
import io.netty.util.internal.MathUtil.*;/*** @author qingm* @date 2024/1/12 15:59*/
public class ByteBufferUtil {private static final char[] BYTE2CHAR = new char[256];private static final char[] HEXDUMP_TABLE = new char[256 * 4];private static final String[] HEXPADDING = new String[16];private static final String[] HEXDUMP_ROWPREFIXES = new String[65536 >>> 4];private static final String[] BYTE2HEX = new String[256];private static final String[] BYTEPADDING = new String[16];static {final char[] DIGITS = "0123456789abcdef".toCharArray();for (int i = 0; i < 256; i++) {HEXDUMP_TABLE[i << 1] = DIGITS[i >>> 4 & 0x0F];HEXDUMP_TABLE[(i << 1) + 1] = DIGITS[i & 0x0F];}int i;// Generate the lookup table for hex dump paddingsfor (i = 0; i < HEXPADDING.length; i++) {int padding = HEXPADDING.length - i;StringBuilder buf = new StringBuilder(padding * 3);for (int j = 0; j < padding; j++) {buf.append(" ");}HEXPADDING[i] = buf.toString();}// Generate the lookup table for the start-offset header in each row (up to 64KiB).for (i = 0; i < HEXDUMP_ROWPREFIXES.length; i++) {StringBuilder buf = new StringBuilder(12);buf.append(StringUtil.NEWLINE);buf.append(Long.toHexString(i << 4 & 0xFFFFFFFFL | 0x100000000L));buf.setCharAt(buf.length() - 9, '|');buf.append('|');HEXDUMP_ROWPREFIXES[i] = buf.toString();}// Generate the lookup table for byte-to-hex-dump conversionfor (i = 0; i < BYTE2HEX.length; i++) {BYTE2HEX[i] = ' ' + StringUtil.byteToHexStringPadded(i);}// Generate the lookup table for byte dump paddingsfor (i = 0; i < BYTEPADDING.length; i++) {int padding = BYTEPADDING.length - i;StringBuilder buf = new StringBuilder(padding);for (int j = 0; j < padding; j++) {buf.append(' ');}BYTEPADDING[i] = buf.toString();}// Generate the lookup table for byte-to-char conversionfor (i = 0; i < BYTE2CHAR.length; i++) {if (i <= 0x1f || i >= 0x7f) {BYTE2CHAR[i] = '.';} else {BYTE2CHAR[i] = (char) i;}}}/*** 打印所有內容* @param buffer*/public static void debugAll(ByteBuffer buffer) {int oldlimit = buffer.limit();buffer.limit(buffer.capacity());StringBuilder origin = new StringBuilder(256);appendPrettyHexDump(origin, buffer, 0, buffer.capacity());System.out.println("+--------+-------------------- all ------------------------+----------------+");System.out.printf("position: [%d], limit: [%d]\n", buffer.position(), oldlimit);System.out.println(origin);buffer.limit(oldlimit);}/*** 打印可讀取內容* @param buffer*/public static void debugRead(ByteBuffer buffer) {StringBuilder builder = new StringBuilder(256);appendPrettyHexDump(builder, buffer, buffer.position(), buffer.limit() - buffer.position());System.out.println("+--------+-------------------- read -----------------------+----------------+");System.out.printf("position: [%d], limit: [%d]\n", buffer.position(), buffer.limit());System.out.println(builder);}private static void appendPrettyHexDump(StringBuilder dump, ByteBuffer buf, int offset, int length) {if (MathUtil.isOutOfBounds(offset, length, buf.capacity())) {throw new IndexOutOfBoundsException("expected: " + "0 <= offset(" + offset + ") <= offset + length(" + length+ ") <= " + "buf.capacity(" + buf.capacity() + ')');}if (length == 0) {return;}dump.append(" +-------------------------------------------------+" +StringUtil.NEWLINE + " | 0 1 2 3 4 5 6 7 8 9 a b c d e f |" +StringUtil.NEWLINE + "+--------+-------------------------------------------------+----------------+");final int startIndex = offset;final int fullRows = length >>> 4;final int remainder = length & 0xF;// Dump the rows which have 16 bytes.for (int row = 0; row < fullRows; row++) {int rowStartIndex = (row << 4) + startIndex;// Per-row prefix.appendHexDumpRowPrefix(dump, row, rowStartIndex);// Hex dumpint rowEndIndex = rowStartIndex + 16;for (int j = rowStartIndex; j < rowEndIndex; j++) {dump.append(BYTE2HEX[getUnsignedByte(buf, j)]);}dump.append(" |");// ASCII dumpfor (int j = rowStartIndex; j < rowEndIndex; j++) {dump.append(BYTE2CHAR[getUnsignedByte(buf, j)]);}dump.append('|');}// Dump the last row which has less than 16 bytes.if (remainder != 0) {int rowStartIndex = (fullRows << 4) + startIndex;appendHexDumpRowPrefix(dump, fullRows, rowStartIndex);// Hex dumpint rowEndIndex = rowStartIndex + remainder;for (int j = rowStartIndex; j < rowEndIndex; j++) {dump.append(BYTE2HEX[getUnsignedByte(buf, j)]);}dump.append(HEXPADDING[remainder]);dump.append(" |");// Ascii dumpfor (int j = rowStartIndex; j < rowEndIndex; j++) {dump.append(BYTE2CHAR[getUnsignedByte(buf, j)]);}dump.append(BYTEPADDING[remainder]);dump.append('|');}dump.append(StringUtil.NEWLINE +"+--------+-------------------------------------------------+----------------+");}private static void appendHexDumpRowPrefix(StringBuilder dump, int row, int rowStartIndex) {if (row < HEXDUMP_ROWPREFIXES.length) {dump.append(HEXDUMP_ROWPREFIXES[row]);} else {dump.append(StringUtil.NEWLINE);dump.append(Long.toHexString(rowStartIndex & 0xFFFFFFFFL | 0x100000000L));dump.setCharAt(dump.length() - 9, '|');dump.append('|');}}public static short getUnsignedByte(ByteBuffer buffer, int index) {return (short) (buffer.get(index) & 0xFF);}
}
調用ByteBuffer的方法
public class TestByteBuffer {public static void main(String[] args) {ByteBuffer buffer = ByteBuffer.allocate(10);// 向buffer中寫入1個字節的數據buffer.put((byte)97);// 使用工具類,查看buffer狀態ByteBufferUtil.debugAll(buffer);// 向buffer中寫入4個字節的數據buffer.put(new byte[]{98, 99, 100, 101});ByteBufferUtil.debugAll(buffer);// 獲取數據buffer.flip();ByteBufferUtil.debugAll(buffer);System.out.println(buffer.get());System.out.println(buffer.get());ByteBufferUtil.debugAll(buffer);// 使用compact切換模式buffer.compact();ByteBufferUtil.debugAll(buffer);// 再次寫入buffer.put((byte)102);buffer.put((byte)103);ByteBufferUtil.debugAll(buffer);}
}
運行結果
// 向緩沖區寫入了一個字節的數據,此時postition為1 +--------+-------------------- all ------------------------+----------------+ position: [1], limit: [10]+-------------------------------------------------+| 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 61 00 00 00 00 00 00 00 00 00 |a......... | +--------+-------------------------------------------------+----------------+// 向緩沖區寫入四個字節的數據,此時position為5 +--------+-------------------- all ------------------------+----------------+ position: [5], limit: [10]+-------------------------------------------------+| 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 61 62 63 64 65 00 00 00 00 00 |abcde..... | +--------+-------------------------------------------------+----------------+// 調用flip切換模式,此時position為0,表示從第0個數據開始讀取 +--------+-------------------- all ------------------------+----------------+ position: [0], limit: [5]+-------------------------------------------------+| 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 61 62 63 64 65 00 00 00 00 00 |abcde..... | +--------+-------------------------------------------------+----------------+ // 讀取兩個字節的數據 97 98// position變為2 +--------+-------------------- all ------------------------+----------------+ position: [2], limit: [5]+-------------------------------------------------+| 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 61 62 63 64 65 00 00 00 00 00 |abcde..... | +--------+-------------------------------------------------+----------------+// 調用compact切換模式,此時position及其后面的數據被壓縮到ByteBuffer前面去了 // 此時position為3,會覆蓋之前的數據 +--------+-------------------- all ------------------------+----------------+ position: [3], limit: [10]+-------------------------------------------------+| 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 63 64 65 64 65 00 00 00 00 00 |cdede..... | +--------+-------------------------------------------------+----------------+// 再次寫入兩個字節的數據,之前的 0x64 0x65 被覆蓋 +--------+-------------------- all ------------------------+----------------+ position: [5], limit: [10]+-------------------------------------------------+| 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 63 64 65 66 67 00 00 00 00 00 |cdefg..... | +--------+-------------------------------------------------+----------------+
字符串與ByteBuffer的相互轉換
方法一
編碼:字符串調用getByte方法獲得byte數組,將byte數組放入ByteBuffer中
解碼:先調用ByteBuffer的flip方法,然后通過StandardCharsets的decoder方法解碼
public class Translate {public static void main(String[] args) {// 準備兩個字符串String str1 = "hello";String str2 = "";ByteBuffer buffer1 = ByteBuffer.allocate(16);// 通過字符串的getByte方法獲得字節數組,放入緩沖區中buffer1.put(str1.getBytes());ByteBufferUtil.debugAll(buffer1);// 將緩沖區中的數據轉化為字符串// 切換模式buffer1.flip();// 通過StandardCharsets解碼,獲得CharBuffer,再通過toString獲得字符串str2 = StandardCharsets.UTF_8.decode(buffer1).toString();System.out.println(str2);ByteBufferUtil.debugAll(buffer1);}
}
運行結果
+--------+-------------------- all ------------------------+----------------+
position: [5], limit: [16]+-------------------------------------------------+| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f 00 00 00 00 00 00 00 00 00 00 00 |hello...........|
+--------+-------------------------------------------------+----------------+
hello
+--------+-------------------- all ------------------------+----------------+
position: [5], limit: [5]+-------------------------------------------------+| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f 00 00 00 00 00 00 00 00 00 00 00 |hello...........|
+--------+-------------------------------------------------+----------------+
Copy
方法二
編碼:通過StandardCharsets的encode方法獲得ByteBuffer,此時獲得的ByteBuffer為讀模式,無需通過flip切換模式
解碼:通過StandardCharsets的decoder方法解碼
public class Translate {public static void main(String[] args) {// 準備兩個字符串String str1 = "hello";String str2 = "";// 通過StandardCharsets的encode方法獲得ByteBuffer// 此時獲得的ByteBuffer為讀模式,無需通過flip切換模式ByteBuffer buffer1 = StandardCharsets.UTF_8.encode(str1);ByteBufferUtil.debugAll(buffer1);// 將緩沖區中的數據轉化為字符串// 通過StandardCharsets解碼,獲得CharBuffer,再通過toString獲得字符串str2 = StandardCharsets.UTF_8.decode(buffer1).toString();System.out.println(str2);ByteBufferUtil.debugAll(buffer1);}
}
運行結果
+--------+-------------------- all ------------------------+----------------+
position: [0], limit: [5]+-------------------------------------------------+| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f |hello |
+--------+-------------------------------------------------+----------------+
hello
+--------+-------------------- all ------------------------+----------------+
position: [5], limit: [5]+-------------------------------------------------+| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f |hello |
+--------+-------------------------------------------------+----------------+
Copy
方法三
編碼:字符串調用getByte()方法獲得字節數組,將字節數組傳給ByteBuffer的wrap()方法,通過該方法獲得ByteBuffer。同樣無需調用flip方法切換為讀模式
解碼:通過StandardCharsets的decoder方法解碼
public class Translate {public static void main(String[] args) {// 準備兩個字符串String str1 = "hello";String str2 = "";// 通過StandardCharsets的encode方法獲得ByteBuffer// 此時獲得的ByteBuffer為讀模式,無需通過flip切換模式ByteBuffer buffer1 = ByteBuffer.wrap(str1.getBytes());ByteBufferUtil.debugAll(buffer1);// 將緩沖區中的數據轉化為字符串// 通過StandardCharsets解碼,獲得CharBuffer,再通過toString獲得字符串str2 = StandardCharsets.UTF_8.decode(buffer1).toString();System.out.println(str2);ByteBufferUtil.debugAll(buffer1);}
}
運行結果
+--------+-------------------- all ------------------------+----------------+
position: [0], limit: [5]+-------------------------------------------------+| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f |hello |
+--------+-------------------------------------------------+----------------+
hello
+--------+-------------------- all ------------------------+----------------+
position: [5], limit: [5]+-------------------------------------------------+| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f |hello |
+--------+-------------------------------------------------+----------------+
粘包與半包
現象
網絡上有多條數據發送給服務端,數據之間使用 \n 進行分隔
但由于某種原因這些數據在接收時,被進行了重新組合,例如原始數據有3條為
- Hello,world\n
- I’m Nyima\n
- How are you?\n
變成了下面的兩個 byteBuffer (粘包,半包)
- Hello,world\nI’m Nyima\nHo
- w are you?\n
出現原因
粘包
發送方在發送數據時,并不是一條一條地發送數據,而是將數據整合在一起,當數據達到一定的數量后再一起發送。這就會導致多條信息被放在一個緩沖區中被一起發送出去
半包
接收方的緩沖區的大小是有限的,當接收方的緩沖區滿了以后,就需要將信息截斷,等緩沖區空了以后再繼續放入數據。這就會發生一段完整的數據最后被截斷的現象
解決辦法
- 通過get(index)方法遍歷ByteBuffer,遇到分隔符時進行處理。注意:get(index)不會改變position的值
- 記錄該段數據長度,以便于申請對應大小的緩沖區
- 將緩沖區的數據通過get()方法寫入到target中
- 調用compact方法切換模式,因為緩沖區中可能還有未讀的數據
public class ByteBufferDemo {public static void main(String[] args) {ByteBuffer buffer = ByteBuffer.allocate(32);// 模擬粘包+半包buffer.put("Hello,world\nI'm Nyima\nHo".getBytes());// 調用split函數處理split(buffer);buffer.put("w are you?\n".getBytes());split(buffer);}private static void split(ByteBuffer buffer) {// 切換為讀模式buffer.flip();for(int i = 0; i < buffer.limit(); i++) {// 遍歷尋找分隔符// get(i)不會移動positionif (buffer.get(i) == '\n') {// 緩沖區長度int length = i+1-buffer.position();ByteBuffer target = ByteBuffer.allocate(length);// 將前面的內容寫入target緩沖區for(int j = 0; j < length; j++) {// 將buffer中的數據寫入target中target.put(buffer.get());}// 打印查看結果ByteBufferUtil.debugAll(target);}}// 切換為寫模式,但是緩沖區可能未讀完,這里需要使用compactbuffer.compact();}
}
運行結果
+--------+-------------------- all ------------------------+----------------+
position: [12], limit: [12]+-------------------------------------------------+| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 65 6c 6c 6f 2c 77 6f 72 6c 64 0a |Hello,world. |
+--------+-------------------------------------------------+----------------+
+--------+-------------------- all ------------------------+----------------+
position: [10], limit: [10]+-------------------------------------------------+| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 49 27 6d 20 4e 79 69 6d 61 0a |I'm Nyima. |
+--------+-------------------------------------------------+----------------+
+--------+-------------------- all ------------------------+----------------+
position: [13], limit: [13]+-------------------------------------------------+| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 6f 77 20 61 72 65 20 79 6f 75 3f 0a |How are you?. |
+--------+-------------------------------------------------+----------------+
二、文件編程
1、FileChannel
工作模式
FileChannel只能在阻塞模式下工作,所以無法搭配Selector
獲取
不能直接打開 FileChannel,必須通過 FileInputStream、FileOutputStream 或者 RandomAccessFile 來獲取 FileChannel,它們都有 getChannel 方法
- 通過 FileInputStream 獲取的 channel?只能讀
- 通過 FileOutputStream 獲取的 channel?只能寫
- 通過 RandomAccessFile 是否能讀寫根據構造 RandomAccessFile 時的讀寫模式決定
讀取
通過 FileInputStream 獲取channel,通過read方法將數據寫入到ByteBuffer中
read方法的返回值表示讀到了多少字節,若讀到了文件末尾則返回-1
int readBytes = channel.read(buffer);
Copy
可根據返回值判斷是否讀取完畢
while(channel.read(buffer) > 0) {// 進行對應操作...
}
寫入
因為channel也是有大小的,所以 write 方法并不能保證一次將 buffer 中的內容全部寫入 channel。必須需要按照以下規則進行寫入
?
// 通過hasRemaining()方法查看緩沖區中是否還有數據未寫入到通道中
while(buffer.hasRemaining()) {channel.write(buffer);
}
關閉
通道需要close,一般情況通過try-with-resource進行關閉,最好使用以下方法獲取strea以及channel,避免某些原因使得資源未被關閉
?
public class TestChannel {public static void main(String[] args) throws IOException {try (FileInputStream fis = new FileInputStream("stu.txt");FileOutputStream fos = new FileOutputStream("student.txt");FileChannel inputChannel = fis.getChannel();FileChannel outputChannel = fos.getChannel()) {// 執行對應操作...}}
}
位置
position
channel也擁有一個保存讀取數據位置的屬性,即position
long pos = channel.position();
可以通過position(int pos)設置channel中position的值
long newPos = ...;
channel.position(newPos);
設置當前位置時,如果設置為文件的末尾
- 這時讀取會返回 -1
- 這時寫入,會追加內容,但要注意如果 position 超過了文件末尾,再寫入時在新內容和原末尾之間會有空洞(00)
強制寫入
操作系統出于性能的考慮,會將數據緩存,不是立刻寫入磁盤,而是等到緩存滿了以后將所有數據一次性的寫入磁盤。可以調用?force(true)?方法將文件內容和元數據(文件的權限等信息)立刻寫入磁盤