一篇文章读不懂:IO vs. NIO

存储 存储软件
处理输入和输出是Java程序员的常见任务,本教程中,我们将介绍 原始的 java.io (IO) 库和较新的 java.nio (NIO) 库 以及它们在通过网络进行通信时的区别。.

[[320131]]

1. 概览

处理输入和输出是Java程序员的常见任务,本教程中,我们将介绍 原始的 java.io (IO) 库和较新的 java.nio (NIO) 库 以及它们在通过网络进行通信时的区别。.

2. 关键特性

让我们先来看看这两个包的关键特性。

2.1. IO – java.io

java.io 包是在Java 1.0引入的,而Reader 则是在 Java 1.1中引入。它提供:

  • InputStream 和 OutputStream – 一次提供一个字节的数据。
  • Reader 和 Writer – 包装流
  • 阻塞模式(blocking mode) – 等待完整的消息

2.2. NIO – java.nio

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

  • Buffer – 一个读取数据块
  • CharsetDecoder – 用于将原始字节映射到可读字符/从可读字符映射原始字节
  • Channel – 与外界沟通
  • Selector – 在 SelectableChannel 上启用多路复用,并提供对任何准备好进行I/O的 Channels 的访问
  • 非阻塞模式(non-blocking mode) – 读取任何准备好的东西

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

3. 配置测试服务器

在这里,我们将使用 WireMock 来模拟另一台服务器,以便我们可以独立运行测试。

配置这台服务器来监听请求,并像真正的web服务器一样向我们发送响应。同时我们还将使用动态端口,这样就不会与本地计算机上的任何服务冲突。

让我们添加WireMock Maven依赖项到 test scope:

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

  1. <dependency> 
  2.     <groupId>com.github.tomakehurst</groupId> 
  3.     <artifactId>wiremock-jre8</artifactId> 
  4.     <version>2.26.3</version> 
  5.     <scope>test</scope> 
  6. </dependency> 

 

在测试类中,让我们定义一个 JUnit*@Rule* 来在空闲端口上启动 WireMock 。然后,我们将对其进行配置,使其在要求预定义资源时返回一个 HTTP 200 响应,消息体为 JSON 格式的文本:

  1. @Rule public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort()); 
  2.   
  3. private String REQUESTED_RESOURCE = "/test.json"
  4.   
  5. @Before 
  6. public void setup() { 
  7.     stubFor(get(urlEqualTo(REQUESTED_RESOURCE)) 
  8.       .willReturn(aResponse() 
  9.       .withStatus(200) 
  10.       .withBody("{ \"response\" : \"It worked!\" }"))); 

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

4. Blocking IO – java.io

我们可通过从网站上读取一些数据来了解原始的阻塞IO模型是如何工作的,例如:使用一个 java.net.Socket 来访问操作系统的一个端口。

4.1. 发送请求(Request)

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

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

对于普通的 HTTP 或 HTTPS 通信,端口应该是 80 或 443 。但是,在本例中,我们使用wireMockRule.port() 来访问前面设置的动态端口。现在,我们在套接字上打开一个 OutputStream ,包装在 OutputStreamWriter 中,并将其传递给 PrintWiter 来编写我们的消息。确保刷新缓冲区以便发送我们的请求:

  1. OutputStream clientOutput = socket.getOutputStream(); 
  2. PrintWriter writer = new PrintWriter(new OutputStreamWriter(clientOutput)); 
  3. writer.print("GET " + TEST_JSON + " HTTP/1.0\r\n\r\n"); 
  4. writer.flush(); 

4.2. 等待响应(Response)

打开套接字上的 InputStream 来获取响应,使用 BufferedReader 读取流,并将其存储在 StringBuilder 中:

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

我们使用 reader.readLine() 来阻塞,等待一个完整的行,然后将该行追加到我们的存储中。我们将一直读取,直到得到一个空值,它指示流的结尾:

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

5. Non-Blocking IO – java.nio

现在,让我们看看 NIO包 的非阻塞IO模型是如何与同一个例子一起工作的。

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

5.1. 发送 Request

首先, 打开 SocketChannel:

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

现在,让我们使用一个标准的UTF-8字符集 来编码和编写我们的消息:

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

5.2. 读取 Response

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

既然要处理文本,那么我们需要一个 ByteBuffer 来处理原始字节,一个CharBuffer 用来转换字符(借助 CharsetDecoder):

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

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

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

在处理缓冲区时,我们需要知道缓冲区有多大(capacity),我们在缓冲区中的位置(current position),以及我们能走多远(limit)。

所以,我们从SocketChannel中读取,将它传递给 ByteBuffer 来存储我们的数据。从 SocketChannel读取将以 ByteBuffer的当前位置为下一个要写入的字节(就在写入最后一个字节之后)结束,但其限制(limit)不变:

  1. socketChannel.read(byteBuffer) 

Our SocketChannel.read() 返回可以写入缓冲区的读取字节数 ,如果断开连接,则会变成 -1.

当缓冲区由于尚未处理其所有数据而没有剩余空间时,SocketChannel.read() 将返回读取的零字节,但buffer.position() 仍将大于零。

确保从缓冲区的正确位置开始读取, 我们将使用 Buffer.flip() 来设置 ByteBuffer 的当前位置为0 以及它对 SocketChannel 写入的最后一个字节的限制。然后,我们将使用 storeBufferContents 方法保存缓冲区内容,稍后我们将查看该方法。最后,使用 buffer.compact() 压缩缓冲区并设置当前位置,以便下次从 SocketChannel 读取。

由于数据可能部分到达,需要用终止条件将缓冲区读取代码包装成一个循环,以检查套接字是否仍然连接,或者是否已断开连接,但缓冲区中仍有数据:

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

别忘了关闭套接字(除非我们在try with resources块中打开它):

  1. socketChannel.close(); 

5.3. Buffer存储数据

来自服务器的响应将包含头,这可能会使数据量超过缓冲区的大小。因此,我们将使用StringBuilder在消息到达时构建完整的消息。为了存储我们的消息,我们首先将原始字节解码为我们的 CharBuffer 中的字符。然后翻转指针,以便读取字符数据,并将其附加到可扩展的 StringBuilder. 最后,清除CharBuffer以准备下一个写/读循环。现在,让我们实现传入缓冲区的完整 storeBufferContents() 方法,CharsetDecoder 和 StringBuilder:

  1. void storeBufferContents(ByteBuffer byteBuffer, CharBuffer charBuffer,  
  2.   CharsetDecoder charsetDecoder, StringBuilder ourStore) { 
  3.     charsetDecoder.decode(byteBuffer, charBuffer, true); 
  4.     charBuffer.flip(); 
  5.     ourStore.append(charBuffer); 
  6.     charBuffer.clear(); 

6. 总结

本文中, 我们已经看到原始java.io模型如何阻塞,等待请求,并使用 Streams 来操作它接收到的数据。相反,java.nio库允许使用Buffers和Channels进行非阻塞通信,并且可以提供直接内存访问以获得更快的性能。然而,这种速度带来了处理缓冲区的额外复杂性。

在本文中,我们看到了原始 java.io 模型如何阻塞,如何等待请求并使用Streams来处理它接收到的数据。相反,java.nio库允许使用Buffers和Channels进行非阻塞通信,并且可以提供直接内存访问以获得更快的性能。然而,这种速度带来了处理缓冲区的额外复杂性。

一如既往, 代码 over on GitHub.

 

责任编辑:武晓燕 来源: 锅外的大佬
相关推荐

2023-05-08 08:21:15

JavaNIO编程

2017-09-05 08:52:37

Git程序员命令

2020-10-23 07:56:04

Java中的IO流

2020-10-09 08:15:11

JsBridge

2020-02-27 21:24:31

JavaAIOBIO

2019-09-24 14:19:12

PythonC语言文章

2023-07-30 15:18:54

JavaScript属性

2021-05-18 08:30:42

JavaScript 前端JavaScript时

2023-09-06 14:57:46

JavaScript编程语言

2021-01-26 23:46:32

JavaScript数据结构前端

2022-08-04 09:39:39

Kubernetes声明式系统

2021-03-05 18:04:15

JavaScript循环代码

2021-03-09 14:04:01

JavaScriptCookie数据

2024-01-30 13:47:45

2021-02-26 20:01:57

SVG滤镜元素

2014-08-08 15:22:20

2023-07-28 07:14:13

2021-09-27 09:18:30

ListIterato接口方法

2022-10-08 15:07:06

ChatOps运维

2020-06-03 11:06:26

DNS域名缓存
点赞
收藏

51CTO技术栈公众号