一篇文章讀不懂:IO vs. NIO
1. 概覽
處理輸入和輸出是Java程序員的常見任務(wù),本教程中,我們將介紹 原始的 java.io (IO) 庫(kù)和較新的 java.nio (NIO) 庫(kù) 以及它們?cè)谕ㄟ^網(wǎng)絡(luò)進(jìn)行通信時(shí)的區(qū)別。.
2. 關(guān)鍵特性
讓我們先來(lái)看看這兩個(gè)包的關(guān)鍵特性。
2.1. IO – java.io
java.io 包是在Java 1.0引入的,而Reader 則是在 Java 1.1中引入。它提供:
- InputStream 和 OutputStream – 一次提供一個(gè)字節(jié)的數(shù)據(jù)。
- Reader 和 Writer – 包裝流
- 阻塞模式(blocking mode) – 等待完整的消息
2.2. NIO – java.nio
java.nio 包在Java 1.4中被引入 并在 Java 1.7 (NIO.2) 更新了,其中包含 增強(qiáng)的文件操作 和 ASynchronousSocketChannel。它提供 :
- Buffer – 一個(gè)讀取數(shù)據(jù)塊
- CharsetDecoder – 用于將原始字節(jié)映射到可讀字符/從可讀字符映射原始字節(jié)
- Channel – 與外界溝通
- Selector – 在 SelectableChannel 上啟用多路復(fù)用,并提供對(duì)任何準(zhǔn)備好進(jìn)行I/O的 Channels 的訪問
- 非阻塞模式(non-blocking mode) – 讀取任何準(zhǔn)備好的東西
現(xiàn)在,讓我們看看在向服務(wù)器發(fā)送數(shù)據(jù)或讀取其響應(yīng)時(shí)如何使用這些包。
3. 配置測(cè)試服務(wù)器
在這里,我們將使用 WireMock 來(lái)模擬另一臺(tái)服務(wù)器,以便我們可以獨(dú)立運(yùn)行測(cè)試。
配置這臺(tái)服務(wù)器來(lái)監(jiān)聽請(qǐng)求,并像真正的web服務(wù)器一樣向我們發(fā)送響應(yīng)。同時(shí)我們還將使用動(dòng)態(tài)端口,這樣就不會(huì)與本地計(jì)算機(jī)上的任何服務(wù)沖突。
讓我們添加WireMock Maven依賴項(xiàng)到 test scope:
Let's add the Maven dependency for WireMock with test scope:
- <dependency>
- <groupId>com.github.tomakehurst</groupId>
- <artifactId>wiremock-jre8</artifactId>
- <version>2.26.3</version>
- <scope>test</scope>
- </dependency>
在測(cè)試類中,讓我們定義一個(gè) JUnit*@Rule* 來(lái)在空閑端口上啟動(dòng) WireMock 。然后,我們將對(duì)其進(jìn)行配置,使其在要求預(yù)定義資源時(shí)返回一個(gè) HTTP 200 響應(yīng),消息體為 JSON 格式的文本:
- @Rule public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort());
- private String REQUESTED_RESOURCE = "/test.json";
- @Before
- public void setup() {
- stubFor(get(urlEqualTo(REQUESTED_RESOURCE))
- .willReturn(aResponse()
- .withStatus(200)
- .withBody("{ \"response\" : \"It worked!\" }")));
- }
現(xiàn)在已經(jīng)建立了模擬服務(wù)器,我們準(zhǔn)備運(yùn)行一些測(cè)試。
4. Blocking IO – java.io
我們可通過從網(wǎng)站上讀取一些數(shù)據(jù)來(lái)了解原始的阻塞IO模型是如何工作的,例如:使用一個(gè) java.net.Socket 來(lái)訪問操作系統(tǒng)的一個(gè)端口。
4.1. 發(fā)送請(qǐng)求(Request)
在這個(gè)例子中,我們將創(chuàng)建一個(gè)GET請(qǐng)求來(lái)檢索資源。首先,創(chuàng)建一個(gè) Socket 來(lái)訪問我們的WireMock服務(wù)器正在監(jiān)聽的端口:
- Socket socket = new Socket("localhost", wireMockRule.port()
對(duì)于普通的 HTTP 或 HTTPS 通信,端口應(yīng)該是 80 或 443 。但是,在本例中,我們使用wireMockRule.port() 來(lái)訪問前面設(shè)置的動(dòng)態(tài)端口?,F(xiàn)在,我們?cè)谔捉幼稚洗蜷_一個(gè) OutputStream ,包裝在 OutputStreamWriter 中,并將其傳遞給 PrintWiter 來(lái)編寫我們的消息。確保刷新緩沖區(qū)以便發(fā)送我們的請(qǐng)求:
- OutputStream clientOutput = socket.getOutputStream();
- PrintWriter writer = new PrintWriter(new OutputStreamWriter(clientOutput));
- writer.print("GET " + TEST_JSON + " HTTP/1.0\r\n\r\n");
- writer.flush();
4.2. 等待響應(yīng)(Response)
打開套接字上的 InputStream 來(lái)獲取響應(yīng),使用 BufferedReader 讀取流,并將其存儲(chǔ)在 StringBuilder 中:
- InputStream serverInput = socket.getInputStream();
- BufferedReader reader = new BufferedReader(new InputStreamReader(serverInput));
- StringBuilder ourStore = new StringBuilder();
我們使用 reader.readLine() 來(lái)阻塞,等待一個(gè)完整的行,然后將該行追加到我們的存儲(chǔ)中。我們將一直讀取,直到得到一個(gè)空值,它指示流的結(jié)尾:
- for (String line; (line = reader.readLine()) != null;) {
- ourStore.append(line);
- ourStore.append(System.lineSeparator());
- }
5. Non-Blocking IO – java.nio
現(xiàn)在,讓我們看看 NIO包 的非阻塞IO模型是如何與同一個(gè)例子一起工作的。
這次,我們將創(chuàng)建一個(gè) java.nio.channel.SocketChannel 來(lái)訪問服務(wù)器上的端口,而不是java.net.Socket,并向它傳遞一個(gè)InetSocketAddress。
5.1. 發(fā)送 Request
首先, 打開 SocketChannel:
- InetSocketAddress address = new InetSocketAddress("localhost", wireMockRule.port());
- SocketChannel socketChannel = SocketChannel.open(address);
現(xiàn)在,讓我們使用一個(gè)標(biāo)準(zhǔn)的UTF-8字符集 來(lái)編碼和編寫我們的消息:
- Charset charset = StandardCharsets.UTF_8;
- socket.write(charset.encode(CharBuffer.wrap("GET " + REQUESTED_RESOURCE + " HTTP/1.0\r\n\r\n")));
5.2. 讀取 Response
發(fā)送請(qǐng)求后,我們可以使用原始緩沖區(qū)以非阻塞模式讀取響應(yīng)。
既然要處理文本,那么我們需要一個(gè) ByteBuffer 來(lái)處理原始字節(jié),一個(gè)CharBuffer 用來(lái)轉(zhuǎn)換字符(借助 CharsetDecoder):
- ByteBuffer byteBuffer = ByteBuffer.allocate(8192);
- CharsetDecoder charsetDecoder = charset.newDecoder();
- CharBuffer charBuffer = CharBuffer.allocate(8192);
如果數(shù)據(jù)是以多字節(jié)字符集發(fā)送的,CharBuffer 將有剩余空間。
注意,如果需要特別快的性能,我們可以使用 ByteBuffer.allocateDirect() 在本機(jī)內(nèi)存中創(chuàng)建一個(gè)MappedByteBuffer。然而,在我們的例子中,從標(biāo)準(zhǔn)堆中使用 allocate() 已經(jīng)足夠快了。
在處理緩沖區(qū)時(shí),我們需要知道緩沖區(qū)有多大(capacity),我們?cè)诰彌_區(qū)中的位置(current position),以及我們能走多遠(yuǎn)(limit)。
所以,我們從SocketChannel中讀取,將它傳遞給 ByteBuffer 來(lái)存儲(chǔ)我們的數(shù)據(jù)。從 SocketChannel讀取將以 ByteBuffer的當(dāng)前位置為下一個(gè)要寫入的字節(jié)(就在寫入最后一個(gè)字節(jié)之后)結(jié)束,但其限制(limit)不變:
- socketChannel.read(byteBuffer)
Our SocketChannel.read() 返回可以寫入緩沖區(qū)的讀取字節(jié)數(shù) ,如果斷開連接,則會(huì)變成 -1.
當(dāng)緩沖區(qū)由于尚未處理其所有數(shù)據(jù)而沒有剩余空間時(shí),SocketChannel.read() 將返回讀取的零字節(jié),但buffer.position() 仍將大于零。
確保從緩沖區(qū)的正確位置開始讀取, 我們將使用 Buffer.flip() 來(lái)設(shè)置 ByteBuffer 的當(dāng)前位置為0 以及它對(duì) SocketChannel 寫入的最后一個(gè)字節(jié)的限制。然后,我們將使用 storeBufferContents 方法保存緩沖區(qū)內(nèi)容,稍后我們將查看該方法。最后,使用 buffer.compact() 壓縮緩沖區(qū)并設(shè)置當(dāng)前位置,以便下次從 SocketChannel 讀取。
由于數(shù)據(jù)可能部分到達(dá),需要用終止條件將緩沖區(qū)讀取代碼包裝成一個(gè)循環(huán),以檢查套接字是否仍然連接,或者是否已斷開連接,但緩沖區(qū)中仍有數(shù)據(jù):
- while (socketChannel.read(byteBuffer) != -1 || byteBuffer.position() > 0) {
- byteBuffer.flip();
- storeBufferContents(byteBuffer, charBuffer, charsetDecoder, ourStore);
- byteBuffer.compact();
- }
別忘了關(guān)閉套接字(除非我們?cè)趖ry with resources塊中打開它):
- socketChannel.close();
5.3. Buffer存儲(chǔ)數(shù)據(jù)
來(lái)自服務(wù)器的響應(yīng)將包含頭,這可能會(huì)使數(shù)據(jù)量超過緩沖區(qū)的大小。因此,我們將使用StringBuilder在消息到達(dá)時(shí)構(gòu)建完整的消息。為了存儲(chǔ)我們的消息,我們首先將原始字節(jié)解碼為我們的 CharBuffer 中的字符。然后翻轉(zhuǎn)指針,以便讀取字符數(shù)據(jù),并將其附加到可擴(kuò)展的 StringBuilder. 最后,清除CharBuffer以準(zhǔn)備下一個(gè)寫/讀循環(huán)。現(xiàn)在,讓我們實(shí)現(xiàn)傳入緩沖區(qū)的完整 storeBufferContents() 方法,CharsetDecoder 和 StringBuilder:
- void storeBufferContents(ByteBuffer byteBuffer, CharBuffer charBuffer,
- CharsetDecoder charsetDecoder, StringBuilder ourStore) {
- charsetDecoder.decode(byteBuffer, charBuffer, true);
- charBuffer.flip();
- ourStore.append(charBuffer);
- charBuffer.clear();
- }
6. 總結(jié)
本文中, 我們已經(jīng)看到原始java.io模型如何阻塞,等待請(qǐng)求,并使用 Streams 來(lái)操作它接收到的數(shù)據(jù)。相反,java.nio庫(kù)允許使用Buffers和Channels進(jìn)行非阻塞通信,并且可以提供直接內(nèi)存訪問以獲得更快的性能。然而,這種速度帶來(lái)了處理緩沖區(qū)的額外復(fù)雜性。
在本文中,我們看到了原始 java.io 模型如何阻塞,如何等待請(qǐng)求并使用Streams來(lái)處理它接收到的數(shù)據(jù)。相反,java.nio庫(kù)允許使用Buffers和Channels進(jìn)行非阻塞通信,并且可以提供直接內(nèi)存訪問以獲得更快的性能。然而,這種速度帶來(lái)了處理緩沖區(qū)的額外復(fù)雜性。
一如既往, 代碼 over on GitHub.