JAVA NIO - Buffer
在之前提到JAVA NIO中,引入了Buffer、Channel 、Selectors.
- Buffer:
- 定義:
Java NIO Buffers用於和NIO Channel交互。 我們從Channel中讀取數據到buffers裏,從Buffer把數據寫入到Channels. Buffer本質上就是一塊內存區,可以用來寫入數據,並在稍後讀取出來。 這塊內存被NIO Buffer包裹起來,對外提供一系列的讀寫方便開發的接口。
- 屬性:
- 定義:
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;//標誌位
private int position = 0;//當前遊標(指針)位置
private int limit;//最大容量
private int capacity;//當前容量
- 交互步驟:
1.申明緩存區大小(直接緩衝區(allocateDirect)或非直接緩衝區(allocate))1
2.把數據寫入buffer;
3.調用flip;
4.從Buffer中讀取數據;
5.調用buffer.clear()或者buffer.compact()。
理解:
Buffer緩衝區實質上就是一塊內存,用於寫入數據,也供後續再次讀取數據。
這塊內存被NIO Buffer管理,並提供一系列的方法用於更簡單的操作這塊內存。
position和limit的具體含義取決於當前buffer的模式。capacity在兩種模式下都表示容量。
(其實這裏有點像切片的概念,指針、界限、最大容量)
-
容量(Capacity)
作爲一塊內存,buffer有一個固定的大小,叫做capacit(容量)。也就是最多隻能寫入容量值得字節,整形等數據。一旦buffer寫滿了就需要清空已讀數據以便下次繼續寫入新的數據
-
上限(Limit)
在寫模式,limit的含義是我們所能寫入的最大數據量,它等同於buffer的容量。 一旦切換到讀模式,limit則代表我們所能讀取的最大數據量,他的值等同於寫模式下position的位置。換句話說,您可以讀取與寫入數量相同的字節數(限制設置爲寫入的字節數,由位置標記)。(0位至 之前寫模式將指針挪到的 索引位)
-
位置(Position)
當寫入數據到Buffer的時候需要從一個確定的位置開始,默認初始化時這個位置position爲0,一旦寫入了數據比如一個字節,整形數據,那麼position的值就會指向數據之後的一個單元,position最大可以到capacity-1. 當從Buffer讀取數據時,也需要從一個確定的位置開始。buffer從寫入模式變爲讀取模式時,position會歸零,每次讀取後,position向後移動。
buffer 中的絕大多數操作都是圍繞以上三個屬性進行操作的,
舉例 :
- 標記當前指針位置/回退到指針位
/**
* Sets this buffer's mark at its position.
*
* @return This buffer
*/
public final Buffer mark() {
mark = position;
return this;
}
/**
回退都指針位置
*/
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
- clear
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
應該明確認知clear只是改變有了遊標位置等,並不會真實的清空了數據。
好比之前dubbo泛化調用中的destroy不會清除zk上的節點,而只是不watch,T_T。
- flip
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
數據存入:
當聲明一組緩存區時,pos爲0位。
每次讀、寫時,pos會+1滑動至下一個索引位;
JAVA NIO - Channel
在之前提到JAVA NIO中,引入了Buffer、Channel 、Selectors.
-
Channel:
-
定義:
Java NIO Buffers用於和NIO Channel交互,而Channel等價於之前的**DMA**,但不存在總線問題。 Channel表示IO源於目標打開的連接。Channel類似傳統的‘流’ ,但Channel本身不能直接訪問數據,
-
交互步驟:
1.Channel只能與Buffer進行交互,且本身不存儲數據。
2.按照通信目標分爲FileChannel(本地,File理解爲FD)、SocketChannel(tcp client)、ServerSocketChannel(tcp server)、DatagramChannel(udp)每種類型交互存在差異;
-
-
通道獲取方式:
- 可使用API直接對已有的流、socket進行get(),如本地I/O中的FileInputStream、FileOutputStream、RandomAccessFile合網絡IO中的Socket、ServerSocket、DatagramSocket。
- 對各個通道進行open()調用。
- Files中的newByteChannel()
- 通道與緩衝區的交互支持 Scatter(分散讀取) 與 Gather (Gather)1
Pipe 不做介紹,簡單理解爲Go裏面的 <-Channel 以及 ->Channel 單向
JAVA NIO - Select(ableChannel)
在之前提到JAVA NIO中,引入了Buffer、Channel 、Selectors.
-
Channel:
-
定義:
Java NIO Select(ableChannel)是多路複用器。用於監控相應I/O的狀態
-
分類:
- java.nio.channels.Channel 接口:
- SelectableChannel
- SocketChannel
- ServerSocketChannel
- DatagramChannel
- Pipe.SinkChannel
- Pipe.SourceChannel
- SelectableChannel
- java.nio.channels.Channel 接口:
-
-
交互方式:
- 當調用register(Selector sel , int pos) 將通道組成選擇器時,選擇器對通道的監聽事件,通過params2進行指定。
- 可監聽到的事件類型有。
– 1.讀 SelectionKey.OP_READ(1)
– 2.寫 SelectionKey.OP_WRITE(4)
– 3.連接 SelectionKey.OP_CONNECT(8)
– 4.接收 SelectionKey.OP_ACCEPT(16) - 通道與緩衝區的交互支持 Scatter(分散讀取) 與 Gather (Gather)2
- 若註冊 不止一個監聽事件,在可以通過使用“|”操作符拼接
SelectionKey keyWithRW =serverSocketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
零拷貝(無CPU拷貝)
- 零拷貝是網絡編程的關鍵,很多性能都是圍繞該點。
- JAVA中,常用的零拷貝有mmap(內存映射)和sendFile。於操作系統而言,在內核緩衝區之間,沒有數據是重複的(只有Kernel buffer 一份數據)。
@Test
public void testCopy() throws IOException {
RandomAccessFile file = new RandomAccessFile(new File("test.txt"), "rw");
byte[] arr = new byte[(int) file.length()];
file.read(arr);
Socket socket = new ServerSocket(8080).accept();
socket.getOutputStream().write(arr);
}
數據一共經過4次拷貝,線程經過3次切換。
mmap優化
- mmap通過內存映射,將文件映射都內存緩衝區,同時,用戶空間可以共享內核空間的數據。這樣,在進行網絡傳輸時,就可以減少內核空間到用戶控件的零拷貝次。
sendFile 優化
- Linux2.1 版本提供了sendFile 函數,其基本原理如下:數據根本不通過用戶態,直接從內核緩衝區進入到SocketBuffer,同時,由於和用戶態無關,就減少了一次上下文切換,2.4中會從kernal buffer直接拷貝到協議棧。
mmap與sendFile區別
1)mmap適合小數據量讀寫,sendFile適合大文件傳輸。
2)mmap需要四次上下文切換,3次數據拷貝;sendFile需要3次上下文切換,最少2次數據拷貝。
- sendFile可以利用DMA方式,減少CPU拷貝,mmap則不能(必須由內核拷貝到socket緩衝區)
理論概念整體較爲雜亂,構建了相應demo,對ApI進行了嘗試調用,並做了一發簡易的羣聊整理NIO知識,完整代碼 https://github.com/LikeElephantintheforest/netty NIO-Group-chat 分支,客戶端telnet實現。
簡介:
- 客戶端可進行註冊至服務端
- 服務端可打印客戶端上行數據
- 服務端可將該消息準發至其他客戶端
較爲重要的API使用:
//1--服務端註冊Selector , 監聽客戶端連接事件。
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//2---通過該selector監聽OP事件,可得知客戶端建連。
boolean haveAccept = selector.select(10000) > 0;
//3----當客戶端連接建立成功服務端知曉後,可對當前socket進行可讀事件監聽
if (e.isAcceptable()) {
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(Boolean.FALSE);
//be notified by OP_ACCEPT event,then register OP_READ on socket
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println(String.format("IP: %s sign up ", socketChannel.getRemoteAddress()));
}
//4-----當客戶端有數據寫入,服務端判定有可讀時,將該消息廣播至所有其他已註冊的客戶端。
if (acceptedMsg) {
for (SelectionKey select : selector.keys()) {
//except self
if (e != select) {
if (select.channel() instanceof SocketChannel) {
SocketChannel clientSocketChannel = (SocketChannel) select.channel();
clientSocketChannel.configureBlocking(Boolean.FALSE);
try {
clientSocketChannel.write(ByteBuffer.wrap(receiveMsg.getBytes()));
} catch (IOException ex) {
//
ex.printStackTrace();
}
}
}
}
}
·
1:字節緩衝區要麼是直接的,要麼是非直接的。
2:如果爲直接字節緩衝區,則java虛擬機會盡最大努力直接在此緩衝區上執行本機I/O操作。再每次調用os的一個本機I/O操作時,虛擬機都會盡量避免將緩衝區的內容複製到中間的緩衝區中。
3:直接緩衝區的內容可以駐留在常規的垃圾回收堆之外,因爲對應用程序造成的內存需求影響並不明顯。
4:非直緩衝區必然存在讀寫時用戶態與內核態的拷貝,程序無法直接通過內核態交交互OS。
5:直接緩衝區通過形成物理內存映射文件,交互操作系統物理內存,不做拷貝。 ↩︎ ↩︎·
分散讀取(Scatter Reads):是指從Channel中讀取的數據“分散”到多個buffer中。(有序)
聚集寫入(Gathering Writes):是指將多個Buffer中的數據“聚集”到Channel中。 ↩︎