Java IO vs NIO – Java IO与NIO

最后修改: 2020年 3月 19日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Handling input and output are common tasks for Java programmers. In this tutorial, we’ll look at the original java.io (IO) libraries and the newer java.nio (NIO) libraries and how they differ when communicating across a network.

处理输入和输出是Java程序员的常见任务。在本教程中,我们将了解原始的java.ioIO)库和较新的java.nioNIO)库以及它们在通过网络进行通信时有何区别。

2. Key Features

2.主要特点

Let’s start by looking at the key features of both packages.

让我们先来看看这两个软件包的主要特点。

2.1. IO – java.io

2.1. IO – java.io

The java.io package was introduced in Java 1.0, with Reader introduced in Java 1.1. It provides:

java.io包在Java 1.0中引入,Reader在Java 1.1中引入。它提供了

  • InputStream and OutputStream – that provide data one byte at a time
  • Reader and Writer – convenience wrappers for the streams
  • blocking mode – to wait for a complete message

2.2. NIO – java.nio

2.2.NIO–java.nio

The java.nio package was introduced in Java 1.4 and updated in Java 1.7 (NIO.2) with enhanced file operations and an ASynchronousSocketChannel. It provides:

java.nio包在Java 1.4中引入,并在Java 1.7(NIO.2)中更新,提供增强的文件操作ASynchronousSocketChannel 。它提供了

  • Buffer – to read chunks of data at a time
  • CharsetDecoder – for mapping raw bytes to/from readable characters
  • Channel – for communicating with the outside world
  • Selector – to enable multiplexing on a SelectableChannel and provide access to any Channels that are ready for I/O
  • non-blocking mode – to read whatever is ready

Now let’s take a look at how we use each of these packages when we send data to a server or read its response.

现在让我们来看看,当我们向服务器发送数据或读取其响应时,我们如何使用这些包。

3. Configure Our Test Server

3.配置我们的测试服务器

Here we’ll be using WireMock to simulate another server so that we can run our tests independently.

在这里,我们将使用WireMock来模拟另一个服务器,这样我们就可以独立地运行我们的测试。

We’ll configure it to listen for our requests and to send us responses just like a real web server would. We’ll also use a dynamic port so that we don’t conflict with any services on our local machine.

我们将配置它来监听我们的请求,并向我们发送响应,就像一个真正的网络服务器那样。我们还将使用一个动态端口,这样我们就不会与我们本地机器上的任何服务发生冲突。

Let’s add the Maven dependency for WireMock with test scope:

让我们在test范围内添加WireMock的Maven依赖。

<dependency>
    <groupId>com.github.tomakehurst</groupId>
    <artifactId>wiremock-jre8</artifactId>
    <version>2.26.3</version>
    <scope>test</scope>
</dependency>

In a test class, let’s define a JUnit @Rule to start WireMock up on a free port. We’ll then configure it to return us an HTTP 200 response when we ask for a predefined resource, with the message body as some text in JSON format:

在一个测试类中,让我们定义一个JUnit @Rule来启动WireMock的自由端口。然后,我们将配置它,当我们请求一个预定义的资源时,它将返回一个HTTP 200响应,消息体是一些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!\" }")));
}

Now that we have our mock server set up, we are ready to run some tests.

现在,我们已经建立了我们的模拟服务器,我们准备运行一些测试。

4. Blocking IO – java.io

4.阻塞式IO – java.io

Let’s look at how the original blocking IO model works by reading some data from a website. We’ll use a java.net.Socket to gain access to one of the operating system’s ports.

让我们通过从一个网站上读取一些数据来看看原始的阻塞式IO模型是如何工作的。我们将使用一个java.net.Socket来获得对操作系统的一个端口的访问。

4.1. Send a Request

4.1.发送一个请求

In this example, we will create a GET request to retrieve our resources. First, let’s create a Socket to access the port that our WireMock server is listening on:

在这个例子中,我们将创建一个GET请求来检索我们的资源。首先,让我们创建一个Socket来访问我们的WireMock服务器所监听的端口

Socket socket = new Socket("localhost", wireMockRule.port())

For normal HTTP or HTTPS communication, the port would be 80 or 443. However, in this case, we use wireMockRule.port() to access the dynamic port we set up earlier.

对于正常的HTTP或HTTPS通信,该端口将是80或443。然而,在这种情况下,我们使用wireMockRule.port() 来访问我们先前设置的动态端口。

Now let’s open an OutputStream on the socket, wrapped in an OutputStreamWriter and pass it to a PrintWriter to write our message. And let’s make sure we flush the buffer so that our request is sent:

现在让我们在套接字上打开一个OutputStream,包裹在OutputStreamWriter中,并把它传递给PrintWriter来写我们的消息。让我们确保我们刷新了缓冲区,以便我们的请求被发送。

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. Wait for the Response

4.2.等待回应

Let’s open an InputStream on the socket to access the response, read the stream with a BufferedReader, and store it in a StringBuilder:

让我们在套接字上打开一个InputStream以访问响应,用BufferedReader读取流,并将其存储在StringBuilder中。

InputStream serverInput = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(serverInput));
StringBuilder ourStore = new StringBuilder();

Let’s use reader.readLine() to block, waiting for a complete line, then append the line to our store. We’ll keep reading until we get a null, which indicates the end of the stream:

让我们使用reader.readLine()来阻塞,等待一个完整的行,然后将该行追加到我们的商店。我们将继续读取,直到我们得到一个null,,这表示流的结束。

for (String line; (line = reader.readLine()) != null;) {
   ourStore.append(line);
   ourStore.append(System.lineSeparator());
}

5. Non-Blocking IO – java.nio

5.非阻塞性IO – java.nio

Now, let’s look at how the nio package’s non-blocking IO model works with the same example.

现在,让我们来看看nio 包的非阻塞IO模型如何在同一个例子中发挥作用。

This time, we’ll create a java.nio.channel.SocketChannel to access the port on our server instead of a java.net.Socket, and pass it an InetSocketAddress.

这一次,我们将创建一个java.nio.channel.SocketChannel来访问我们服务器上的端口,而不是一个java.net.Socket,并给它传递一个InetSocketAddress

5.1. Send a Request

5.1.发送一个请求

First, let’s open our SocketChannel:

首先,让我们打开我们的SocketChannel

InetSocketAddress address = new InetSocketAddress("localhost", wireMockRule.port());
SocketChannel socketChannel = SocketChannel.open(address);

And now, let’s get a standard UTF-8 Charset to encode and write our message:

现在,让我们得到一个标准的UTF-8字符集来编码和编写我们的信息。

Charset charset = StandardCharsets.UTF_8;
socket.write(charset.encode(CharBuffer.wrap("GET " + REQUESTED_RESOURCE + " HTTP/1.0\r\n\r\n")));

5.2. Read the Response

5.2.阅读回复

After we send the request, we can read the response in non-blocking mode, using raw buffers.

在我们发送请求后,我们可以在非阻塞模式下,使用原始缓冲区读取响应。

Since we’ll be processing text, we’ll need a ByteBuffer for the raw bytes and a CharBuffer for the converted characters (aided by a CharsetDecoder):

由于我们将处理文本,我们需要一个ByteBuffer来处理原始字节,以及一个CharBuffer来处理转换后的字符(由CharsetDecoder辅助)。

ByteBuffer byteBuffer = ByteBuffer.allocate(8192);
CharsetDecoder charsetDecoder = charset.newDecoder();
CharBuffer charBuffer = CharBuffer.allocate(8192);

Our CharBuffer will have space left over if the data is sent in a multi-byte character set.

如果数据以多字节字符集发送,我们的CharBuffer将有剩余的空间。

Note that if we need especially fast performance, we can create a MappedByteBuffer in native memory using ByteBuffer.allocateDirect(). However, in our case, using allocate() from the standard heap is fast enough.

请注意,如果我们需要特别快的性能,我们可以使用ByteBuffer.allocateDirect()在本地内存中创建一个MappedByteBuffer/a>。然而,在我们的案例中,从标准堆中使用allocate()已经足够快了。

When dealing with buffers, we need to know how big the buffer is (the capacity), where we are in the buffer (the current position), and how far we can go (the limit).

在处理缓冲区时,我们需要知道缓冲区有多大(容量),我们在缓冲区的位置(当前位置),以及我们能走多远(极限)。

So, let’s read from our SocketChannel, passing it our ByteBuffer to store our data. Our read from the SocketChannel will finish with our ByteBuffer‘s current position set to the next byte to write to (just after the last byte written), but with its limit unchanged:

因此,让我们SocketChannel读取,将我们的ByteBuffer传递给它,以存储我们的数据。我们从SocketChannel结束时,我们的ByteBuffer当前位置被设置为要写入的下一个字节(就在写入的最后一个字节之后),但其限制没有改变

socketChannel.read(byteBuffer)

Our SocketChannel.read() returns the number of bytes read that could be written into our buffer. This will be -1 if the socket was disconnected.

我们的SocketChannel.read()返回读取的可写入我们缓冲区的字节数。如果套接字被断开,这将是-1。

When our buffer doesn’t have any space left because we haven’t processed all its data yet, then SocketChannel.read() will return zero bytes read but our buffer.position() will still be greater than zero.

当我们的缓冲区没有任何剩余空间时,因为我们还没有处理它的所有数据,那么SocketChannel.read()将返回读取的零字节,但我们的buffer.position()仍将大于零。

To make sure that we start reading from the right place in the buffer, we’ll use Buffer.flip() to set our ByteBuffer‘s current position to zero and its limit to the last byte that was written by the SocketChannel. We’ll then save the buffer contents using our storeBufferContents method, which we’ll look at later. Lastly, we’ll use buffer.compact() to compact the buffer and set the current position ready for our next read from the SocketChannel.

为了确保我们从缓冲区的正确位置开始读取,我们将使用Buffer.flip()将我们的ByteBuffer的当前位置设置为零,并将其极限设置为SocketChannel的最后一个字节。然后我们将使用storeBufferContents方法保存缓冲区内容,我们将在后面讨论这个问题。最后,我们将使用 buffer.compact() 来压缩缓冲区,并设置当前位置,以便我们从 SocketChannel 的下一次读取。

Since our data may arrive in parts, let’s wrap our buffer-reading code in a loop with termination conditions to check if our socket is still connected or if we’ve been disconnected but still have data left in our buffer:

由于我们的数据可能是分批到达的,让我们把我们的缓冲区读取代码包在一个带有终止条件的循环中,以检查我们的套接字是否仍在连接,或者我们已经被断开连接但缓冲区中仍有数据。

while (socketChannel.read(byteBuffer) != -1 || byteBuffer.position() > 0) {
    byteBuffer.flip();
    storeBufferContents(byteBuffer, charBuffer, charsetDecoder, ourStore);
    byteBuffer.compact();
}

And let’s not forget to close() our socket (unless we opened it in a try-with-resources block):

我们不要忘记close()我们的套接字(除非我们在try-with-resources块中打开它)。

socketChannel.close();

5.3. Storing Data From Our Buffer

5.3.从我们的缓冲区存储数据

The response from the server will contain headers, which may make the amount of data exceed the size of our buffer. So, we’ll use a StringBuilder to build our complete message as it arrives.

服务器的响应将包含头信息,这可能使数据量超过我们的缓冲区大小。因此,我们将使用一个StringBuilder来建立我们完整的消息,因为它到达了。

To store our message, we first decode the raw bytes into characters in our CharBuffer. Then we’ll flip the pointers so that we can read our character data, and append it to our expandable StringBuilder. Lastly, we’ll clear the CharBuffer ready for the next write/read cycle.

为了存储我们的信息,我们首先将原始字节解码成我们的CharBuffer中的字符。然后,我们将翻转指针,以便读取我们的字符数据,并将其追加到我们的可扩展的StringBuilder中。最后,我们将清除CharBuffer,为下一个写/读周期做好准备。

So now, let’s implement our complete storeBufferContents() method passing in our buffers, CharsetDecoder, and StringBuilder:

所以现在,让我们实现完整的storeBufferContents()方法,传入我们的缓冲区、CharsetDecoderStringBuilder

void storeBufferContents(ByteBuffer byteBuffer, CharBuffer charBuffer, 
  CharsetDecoder charsetDecoder, StringBuilder ourStore) {
    charsetDecoder.decode(byteBuffer, charBuffer, true);
    charBuffer.flip();
    ourStore.append(charBuffer);
    charBuffer.clear();
}

6. Conclusion

6.结语

In this article, we’ve seen how the original java.io model blocks, waits for a request and uses Streams to manipulate the data it receives.

在这篇文章中,我们已经看到了原始java.io模型如何阻塞、等待请求并使用Streams来操作它收到的数据。

In contrast, the java.nio libraries allow for non-blocking communication using Buffers and Channels and can provide direct memory access for faster performance. However, with this speed comes the additional complexity of handling buffers.

相比之下,java.nio库允许使用Buffers和Channels进行非阻塞通信,并且可以提供直接的内存访问以获得更快的性能。然而,随着这一速度的提高,处理缓冲区的复杂性也随之增加。

As usual, the code for this article is available over on GitHub.

像往常一样,本文的代码可以在GitHub上找到