本文轉(zhuǎn)載自互聯(lián)網(wǎng)
創(chuàng)新互聯(lián)公司基于成都重慶香港及美國(guó)等地區(qū)分布式IDC機(jī)房數(shù)據(jù)中心構(gòu)建的電信大帶寬,聯(lián)通大帶寬,移動(dòng)大帶寬,多線BGP大帶寬租用,是為眾多客戶提供專業(yè)服務(wù)器托管報(bào)價(jià),主機(jī)托管價(jià)格性價(jià)比高,為金融證券行業(yè)服務(wù)器托管德陽,ai人工智能服務(wù)器托管提供bgp線路100M獨(dú)享,G口帶寬及機(jī)柜租用的專業(yè)成都idc公司。
本系列文章將整理到我在GitHub上的《Java面試指南》倉(cāng)庫(kù),更多精彩內(nèi)容請(qǐng)到我的倉(cāng)庫(kù)里查看
https://github.com/h3pl/Java-Tutorial
喜歡的話麻煩點(diǎn)下Star哈
文章將同步到我的個(gè)人博客:
www.how2playlife.com
本文是微信公眾號(hào)【Java技術(shù)江湖】的《不可輕視的Java網(wǎng)絡(luò)編程》其中一篇,本文部分內(nèi)容來源于網(wǎng)絡(luò),為了把本文主題講得清晰透徹,也整合了很多我認(rèn)為不錯(cuò)的技術(shù)博客內(nèi)容,引用其中了一些比較好的博客文章,如有侵權(quán),請(qǐng)聯(lián)系作者。
該系列博文會(huì)告訴你如何從計(jì)算機(jī)網(wǎng)絡(luò)的基礎(chǔ)知識(shí)入手,一步步地學(xué)習(xí)Java網(wǎng)絡(luò)基礎(chǔ),從socket到nio、bio、aio和netty等網(wǎng)絡(luò)編程知識(shí),并且進(jìn)行實(shí)戰(zhàn),網(wǎng)絡(luò)編程是每一個(gè)Java后端工程師必須要學(xué)習(xí)和理解的知識(shí)點(diǎn),進(jìn)一步來說,你還需要掌握Linux中的網(wǎng)絡(luò)編程原理,包括IO模型、網(wǎng)絡(luò)編程框架netty的進(jìn)階原理,才能更完整地了解整個(gè)Java網(wǎng)絡(luò)編程的知識(shí)體系,形成自己的知識(shí)框架。
為了更好地總結(jié)和檢驗(yàn)?zāi)愕膶W(xué)習(xí)成果,本系列文章也會(huì)提供部分知識(shí)點(diǎn)對(duì)應(yīng)的面試題以及參考答案。
如果對(duì)本系列文章有什么建議,或者是有什么疑問的話,也可以關(guān)注公眾號(hào)【Java技術(shù)江湖】聯(lián)系作者,歡迎你參與本系列博文的創(chuàng)作和修訂。
本文將介紹 Java NIO 中三大組件 Buffer、Channel、Selector的使用。
本來要一起介紹 非阻塞 IO和 JDK7 的 異步 IO的,不過因?yàn)橹暗奈恼抡娴奶L(zhǎng)了,有點(diǎn)影響讀者閱讀,所以這里將它們放到另一篇文章中進(jìn)行介紹。
一個(gè) Buffer 本質(zhì)上是內(nèi)存中的一塊,我們可以將數(shù)據(jù)寫入這塊內(nèi)存,之后從這塊內(nèi)存獲取數(shù)據(jù)。
java.nio 定義了以下幾個(gè) Buffer 的實(shí)現(xiàn),這個(gè)圖讀者應(yīng)該也在不少地方見過了吧。

其實(shí)核心是最后的 ByteBuffer,前面的一大串類只是包裝了一下它而已,我們使用最多的通常也是 ByteBuffer。
我們應(yīng)該將 Buffer 理解為一個(gè)數(shù)組,IntBuffer、CharBuffer、DoubleBuffer 等分別對(duì)應(yīng) int[]、char[]、double[] 等。
MappedByteBuffer 用于實(shí)現(xiàn)內(nèi)存映射文件,也不是本文關(guān)注的重點(diǎn)。
我覺得操作 Buffer 和操作數(shù)組、類集差不多,只不過大部分時(shí)候我們都把它放到了 NIO 的場(chǎng)景里面來使用而已。下面介紹 Buffer 中的幾個(gè)重要屬性和幾個(gè)重要方法。
就像數(shù)組有數(shù)組容量,每次訪問元素要指定下標(biāo),Buffer 中也有幾個(gè)重要屬性:position、limit、capacity。

最好理解的當(dāng)然是 capacity,它代表這個(gè)緩沖區(qū)的容量,一旦設(shè)定就不可以更改。比如 capacity 為 1024 的 IntBuffer,代表其一次可以存放 1024 個(gè) int 類型的值。一旦 Buffer 的容量達(dá)到 capacity,需要清空 Buffer,才能重新寫入值。
position 和 limit 是變化的,我們分別看下讀和寫操作下,它們是如何變化的。
position的初始值是 0,每往 Buffer 中寫入一個(gè)值,position 就自動(dòng)加 1,代表下一次的寫入位置。讀操作的時(shí)候也是類似的,每讀一個(gè)值,position 就自動(dòng)加 1。
從寫操作模式到讀操作模式切換的時(shí)候( flip),position 都會(huì)歸零,這樣就可以從頭開始讀寫了。
Limit:寫操作模式下,limit 代表的是最大能寫入的數(shù)據(jù),這個(gè)時(shí)候 limit 等于 capacity。寫結(jié)束后,切換到讀模式,此時(shí)的 limit 等于 Buffer 中實(shí)際的數(shù)據(jù)大小,因?yàn)?Buffer 不一定被寫滿了。

每個(gè) Buffer 實(shí)現(xiàn)類都提供了一個(gè)靜態(tài)方法
allocate(int capacity) 幫助我們快速實(shí)例化一個(gè) Buffer。如:
ByteBuffer byteBuf = ByteBuffer.allocate(1024);
IntBuffer intBuf = IntBuffer.allocate(1024);
LongBuffer longBuf = LongBuffer.allocate(1024);
// ...
另外,我們經(jīng)常使用 wrap 方法來初始化一個(gè) Buffer。
public static ByteBuffer wrap(byte[] array) {
...
}
各個(gè) Buffer 類都提供了一些 put 方法用于將數(shù)據(jù)填充到 Buffer 中,如 ByteBuffer 中的幾個(gè) put 方法:
// 填充一個(gè) byte 值
public abstract ByteBuffer put(byte b);
// 在指定位置填充一個(gè) int 值
public abstract ByteBuffer put(int index, byte b);
// 將一個(gè)數(shù)組中的值填充進(jìn)去
public final ByteBuffer put(byte[] src) {...}
public ByteBuffer put(byte[] src, int offset, int length) {...}
上述這些方法需要自己控制 Buffer 大小,不能超過 capacity,超過會(huì)拋 java.nio.BufferOverflowException 異常。
對(duì)于 Buffer 來說,另一個(gè)常見的操作中就是,我們要將來自 Channel 的數(shù)據(jù)填充到 Buffer 中,在系統(tǒng)層面上,這個(gè)操作我們稱為 讀操作,因?yàn)閿?shù)據(jù)是從外部(文件或網(wǎng)絡(luò)等)讀到內(nèi)存中。
int num = channel.read(buf);
上述方法會(huì)返回從 Channel 中讀入到 Buffer 的數(shù)據(jù)大小。
前面介紹了寫操作,每寫入一個(gè)值,position 的值都需要加 1,所以 position 最后會(huì)指向最后一次寫入的位置的后面一個(gè),如果 Buffer 寫滿了,那么 position 等于 capacity(position 從 0 開始)。
如果要讀 Buffer 中的值,需要切換模式,從寫入模式切換到讀出模式。注意,通常在說 NIO 的讀操作的時(shí)候,我們說的是從 Channel 中讀數(shù)據(jù)到 Buffer 中,對(duì)應(yīng)的是對(duì) Buffer 的寫入操作,初學(xué)者需要理清楚這個(gè)。
調(diào)用 Buffer 的 flip()方法,可以從寫入模式切換到讀取模式。其實(shí)這個(gè)方法也就是設(shè)置了一下 position 和 limit 值罷了。
public final Buffer flip() {
limit = position; // 將 limit 設(shè)置為實(shí)際寫入的數(shù)據(jù)數(shù)量
position = 0; // 重置 position 為 0
mark = -1; // mark 之后再說
return this;
}
對(duì)應(yīng)寫入操作的一系列 put 方法,讀操作提供了一系列的 get 方法:
// 根據(jù) position 來獲取數(shù)據(jù)
public abstract byte get();
// 獲取指定位置的數(shù)據(jù)
public abstract byte get(int index);
// 將 Buffer 中的數(shù)據(jù)寫入到數(shù)組中
public ByteBuffer get(byte[] dst)
附一個(gè)經(jīng)常使用的方法:
new String(buffer.array()).trim();
當(dāng)然了,除了將數(shù)據(jù)從 Buffer 取出來使用,更常見的操作是將我們寫入的數(shù)據(jù)傳輸?shù)?Channel 中,如通過 FileChannel 將數(shù)據(jù)寫入到文件中,通過 SocketChannel 將數(shù)據(jù)寫入網(wǎng)絡(luò)發(fā)送到遠(yuǎn)程機(jī)器等。對(duì)應(yīng)的,這種操作,我們稱之為 寫操作。
int num = channel.write(buf);
除了 position、limit、capacity 這三個(gè)基本的屬性外,還有一個(gè)常用的屬性就是 mark。
mark 用于臨時(shí)保存 position 的值,每次調(diào)用 mark() 方法都會(huì)將 mark 設(shè)值為當(dāng)前的 position,便于后續(xù)需要的時(shí)候使用。
public final Buffer mark() {
mark = position;
return this;
}
那到底什么時(shí)候用呢?考慮以下場(chǎng)景,我們?cè)?position 為 5 的時(shí)候,先 mark() 一下,然后繼續(xù)往下讀,讀到第 10 的時(shí)候,我想重新回到 position 為 5 的地方重新來一遍,那只要調(diào)一下 reset() 方法,position 就回到 5 了。
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
rewind():會(huì)重置 position 為 0,通常用于重新從頭讀寫 Buffer。
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
clear():有點(diǎn)重置 Buffer 的意思,相當(dāng)于重新實(shí)例化了一樣。
通常,我們會(huì)先填充 Buffer,然后從 Buffer 讀取數(shù)據(jù),之后我們?cè)僦匦峦锾畛湫碌臄?shù)據(jù),我們一般在重新填充之前先調(diào)用 clear()。
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
compact():和 clear() 一樣的是,它們都是在準(zhǔn)備往 Buffer 填充新的數(shù)據(jù)之前調(diào)用。
前面說的 clear() 方法會(huì)重置幾個(gè)屬性,但是我們要看到,clear() 方法并不會(huì)將 Buffer 中的數(shù)據(jù)清空,只不過后續(xù)的寫入會(huì)覆蓋掉原來的數(shù)據(jù),也就相當(dāng)于清空了數(shù)據(jù)了。
而 compact() 方法有點(diǎn)不一樣,調(diào)用這個(gè)方法以后,會(huì)先處理還沒有讀取的數(shù)據(jù),也就是 position 到 limit 之間的數(shù)據(jù)(還沒有讀過的數(shù)據(jù)),先將這些數(shù)據(jù)移到左邊,然后在這個(gè)基礎(chǔ)上再開始寫入。很明顯,此時(shí) limit 還是等于 capacity,position 指向原來數(shù)據(jù)的右邊。
所有的 NIO 操作始于通道,通道是數(shù)據(jù)來源或數(shù)據(jù)寫入的目的地,主要地,我們將關(guān)心 java.nio 包中實(shí)現(xiàn)的以下幾個(gè) Channel:

這里不是很理解這些也沒關(guān)系,后面介紹了代碼之后就清晰了。還有,我們最應(yīng)該關(guān)注,也是后面將會(huì)重點(diǎn)介紹的是 SocketChannel 和 ServerSocketChannel。
Channel 經(jīng)常翻譯為通道,類似 IO 中的流,用于讀取和寫入。它與前面介紹的 Buffer 打交道,讀操作的時(shí)候?qū)?Channel 中的數(shù)據(jù)填充到 Buffer 中,而寫操作時(shí)將 Buffer 中的數(shù)據(jù)寫入到 Channel 中。


至少讀者應(yīng)該記住一點(diǎn),這兩個(gè)方法都是 channel 實(shí)例的方法。
我想文件操作對(duì)于大家來說應(yīng)該是最熟悉的,不過我們?cè)谡f NIO 的時(shí)候,其實(shí) FileChannel 并不是關(guān)注的重點(diǎn)。而且后面我們說非阻塞的時(shí)候會(huì)看到,F(xiàn)ileChannel 是不支持非阻塞的。
這里算是簡(jiǎn)單介紹下常用的操作吧,感興趣的讀者瞄一眼就是了。
初始化:
FileInputStream inputStream = new FileInputStream(new File("/data.txt"));
FileChannel fileChannel = inputStream.getChannel();
當(dāng)然了,我們也可以從 RandomAccessFile#getChannel 來得到 FileChannel。
讀取文件內(nèi)容:
ByteBuffer buffer = ByteBuffer.allocate(1024);
int num = fileChannel.read(buffer);
前面我們也說了,所有的 Channel 都是和 Buffer 打交道的。
寫入文件內(nèi)容:
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("隨機(jī)寫入一些內(nèi)容到 Buffer 中".getBytes());
// Buffer 切換為讀模式
buffer.flip();
while(buffer.hasRemaining()) {
// 將 Buffer 中的內(nèi)容寫入文件
fileChannel.write(buffer);
}
我們前面說了,我們可以將 SocketChannel 理解成一個(gè) TCP 客戶端。雖然這么理解有點(diǎn)狹隘,因?yàn)槲覀冊(cè)诮榻B ServerSocketChannel 的時(shí)候會(huì)看到另一種使用方式。
打開一個(gè) TCP 連接:
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("https://www.javadoop.com", 80));
當(dāng)然了,上面的這行代碼等價(jià)于下面的兩行:
// 打開一個(gè)通道
SocketChannel socketChannel = SocketChannel.open();
// 發(fā)起連接
socketChannel.connect(new InetSocketAddress("https://www.javadoop.com", 80));
SocketChannel 的讀寫和 FileChannel 沒什么區(qū)別,就是操作緩沖區(qū)。
// 讀取數(shù)據(jù)
socketChannel.read(buffer);
// 寫入數(shù)據(jù)到網(wǎng)絡(luò)連接中
while(buffer.hasRemaining()) {
socketChannel.write(buffer);
}
不要在這里停留太久,先繼續(xù)往下走。
之前說 SocketChannel 是 TCP 客戶端,這里說的 ServerSocketChannel 就是對(duì)應(yīng)的服務(wù)端。
ServerSocketChannel 用于監(jiān)聽機(jī)器端口,管理從這個(gè)端口進(jìn)來的 TCP 連接。
// 實(shí)例化
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 監(jiān)聽 8080 端口
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
while (true) {
// 一旦有一個(gè) TCP 連接進(jìn)來,就對(duì)應(yīng)創(chuàng)建一個(gè) SocketChannel 進(jìn)行處理
SocketChannel socketChannel = serverSocketChannel.accept();
}
這里我們可以看到 SocketChannel 的第二個(gè)實(shí)例化方式
到這里,我們應(yīng)該能理解 SocketChannel 了,它不僅僅是 TCP 客戶端,它代表的是一個(gè)網(wǎng)絡(luò)通道,可讀可寫。
ServerSocketChannel 不和 Buffer 打交道了,因?yàn)樗⒉粚?shí)際處理數(shù)據(jù),它一旦接收到請(qǐng)求后,實(shí)例化 SocketChannel,之后在這個(gè)連接通道上的數(shù)據(jù)傳遞它就不管了,因?yàn)樗枰^續(xù)監(jiān)聽端口,等待下一個(gè)連接。
UDP 和 TCP 不一樣,DatagramChannel 一個(gè)類處理了服務(wù)端和客戶端。
科普一下,UDP 是面向無連接的,不需要和對(duì)方握手,不需要通知對(duì)方,就可以直接將數(shù)據(jù)包投出去,至于能不能送達(dá),它是不知道的
監(jiān)聽端口:
DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9090));
ByteBuffer buf = ByteBuffer.allocate(48);
channel.receive(buf);
發(fā)送數(shù)據(jù):
String newData = "New String to write to file..."
+ System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.put(newData.getBytes());
buf.flip();
int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com", 80));
NIO 三大組件就剩 Selector 了,Selector 建立在非阻塞的基礎(chǔ)之上,大家經(jīng)常聽到的 多路復(fù)用在 Java 世界中指的就是它,用于實(shí)現(xiàn)一個(gè)線程管理多個(gè) Channel。
讀者在這一節(jié)不能消化 Selector 也沒關(guān)系,因?yàn)楹罄m(xù)在介紹非阻塞 IO 的時(shí)候還得說到這個(gè),這里先介紹一些基本的接口操作。
首先,我們開啟一個(gè) Selector。你們愛翻譯成 選擇器也好, 多路復(fù)用器也好。
Selector selector = Selector.open();
將 Channel 注冊(cè)到 Selector 上。前面我們說了,Selector 建立在非阻塞模式之上,所以注冊(cè)到 Selector 的 Channel 必須要支持非阻塞模式, FileChannel 不支持非阻塞,我們這里討論最常見的 SocketChannel 和 ServerSocketChannel。
// 將通道設(shè)置為非阻塞模式,因?yàn)槟J(rèn)都是阻塞模式的
channel.configureBlocking(false);
// 注冊(cè)
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
register 方法的第二個(gè) int 型參數(shù)(使用二進(jìn)制的標(biāo)記位)用于表明需要監(jiān)聽哪些感興趣的事件,共以下四種事件:
SelectionKey.OP_READ
對(duì)應(yīng) 00000001,通道中有數(shù)據(jù)可以進(jìn)行讀取
SelectionKey.OP_WRITE
對(duì)應(yīng) 00000100,可以往通道中寫入數(shù)據(jù)
SelectionKey.OP_CONNECT
對(duì)應(yīng) 00001000,成功建立 TCP 連接
SelectionKey.OP_ACCEPT
對(duì)應(yīng) 00010000,接受 TCP 連接
我們可以同時(shí)監(jiān)聽一個(gè) Channel 中的發(fā)生的多個(gè)事件,比如我們要監(jiān)聽 ACCEPT 和 READ 事件,那么指定參數(shù)為二進(jìn)制的 000 1000 1即十進(jìn)制數(shù)值 17 即可。
注冊(cè)方法返回值是 SelectionKey實(shí)例,它包含了 Channel 和 Selector 信息,也包括了一個(gè)叫做 Interest Set 的信息,即我們?cè)O(shè)置的我們感興趣的正在監(jiān)聽的事件集合。
調(diào)用 select() 方法獲取通道信息。用于判斷是否有我們感興趣的事件已經(jīng)發(fā)生了。
Selector 的操作就是以上 3 步,這里來一個(gè)簡(jiǎn)單的示例,大家看一下就好了。之后在介紹非阻塞 IO 的時(shí)候,會(huì)演示一份可執(zhí)行的示例代碼。
Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
// 判斷是否有事件準(zhǔn)備好
int readyChannels = selector.select();
if(readyChannels == 0) continue;
// 遍歷
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();
}
}
對(duì)于 Selector,我們還需要非常熟悉以下幾個(gè)方法:
select()
調(diào)用此方法,會(huì)將 上次 select 之后的準(zhǔn)備好的 channel 對(duì)應(yīng)的 SelectionKey 復(fù)制到 selected set 中。如果沒有任何通道準(zhǔn)備好,這個(gè)方法會(huì)阻塞,直到至少有一個(gè)通道準(zhǔn)備好。
selectNow()
功能和 select 一樣,區(qū)別在于如果沒有準(zhǔn)備好的通道,那么此方法會(huì)立即返回 0。
select(long timeout)
看了前面兩個(gè),這個(gè)應(yīng)該很好理解了,如果沒有通道準(zhǔn)備好,此方法會(huì)等待一會(huì)
wakeup()
這個(gè)方法是用來喚醒等待在 select() 和 select(timeout) 上的線程的。如果 wakeup() 先被調(diào)用,此時(shí)沒有線程在 select 上阻塞,那么之后的一個(gè) select() 或 select(timeout) 會(huì)立即返回,而不會(huì)阻塞,當(dāng)然,它只會(huì)作用一次。
到此為止,介紹了 Buffer、Channel 和 Selector 的常見接口。
Buffer 和數(shù)組差不多,它有 position、limit、capacity 幾個(gè)重要屬性。put() 一下數(shù)據(jù)、flip() 切換到讀模式、然后用 get() 獲取數(shù)據(jù)、clear() 一下清空數(shù)據(jù)、重新回到 put() 寫入數(shù)據(jù)。
Channel 基本上只和 Buffer 打交道,最重要的接口就是 channel.read(buffer) 和 channel.write(buffer)。
Selector 用于實(shí)現(xiàn)非阻塞 IO,這里僅僅介紹接口使用,后續(xù)請(qǐng)關(guān)注非阻塞 IO 的介紹。
(全文完)
網(wǎng)站欄目:Java網(wǎng)絡(luò)編程與NIO詳解4:淺析NIO包中的Buffer、Channel和Selector
文章出自:http://chinadenli.net/article38/ppdcpp.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供定制開發(fā)、企業(yè)網(wǎng)站制作、、網(wǎng)站收錄、定制網(wǎng)站、Google
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)