`

全面解读Java NIO工作原理(4)

    博客分类:
  • Java
阅读更多

 

JDK 1.4 中引入的新输入输出 (NIO) 库在标准 Java 代码中提供了高速的、面向块的 I/O。本实用教程从高级概念到底层的编程细节,非常详细地介绍了 NIO 库。您将学到诸如缓冲区和通道这样的关键 I/O 元素的知识,并考察更新后的库中的标准 I/O 是如何工作的。您还将了解只能通过 NIO 来完成的工作,如异步 I/O 和直接缓冲区。

Sky:

 

◆  连网和异步 I/O

概  述

连网是学习异步 I/O 的很好基础,而异步 I/O 对于在 Java 语言中执行任何输入/输出过程的人来说,

无疑都是必须具备的知识。NIO 中的连网与 NIO 中的其他任何操作没有什么不同 ― 它依赖通道和缓冲区,

而您通常使用 InputStream 和 OutputStream 来获得通道。

本节首先介绍异步 I/O 的基础 ― 它是什么以及它不是什么,然后转向更实用的、程序性的例子。

异步 I/O

异步 I/O 是一种 没有阻塞地 读写数据的方法。通常,在代码进行 read() 调用时,

代码会阻塞直至有可供读取的数据。同样,write() 调用将会阻塞直至数据能够写入。

另一方面,异步 I/O 调用不会阻塞。相反,您将注册对特定 I/O 事件的兴趣 ― 可读的数据的到达、

新的套接字连接,等等,而在发生这样的事件时,系统将会告诉您。

异步 I/O 的一个优势在于,它允许您同时根据大量的输入和输出执行 I/O。同步程序常常要求助于轮询,

或者创建许许多多的线程以处理大量的连接。使用异步 I/O,您可以监听任何数量的通道上的事件,

不用轮询,也不用额外的线程。

我们将通过研究一个名为 MultiPortEcho.java 的例子程序来查看异步 I/O 的实际应用。

这个程序就像传统的 echo server,它接受网络连接并向它们回响它们可能发送的数据。

不过它有一个附加的特性,就是它能同时监听多个端口,并处理来自所有这些端口的连接。

并且它只在单个线程中完成所有这些工作。

Selectors

本节的阐述对应于 MultiPortEcho 的源代码中的 go() 方法的实现,因此应该看一下源代码,

以便对所发生的事情有个更全面的了解。

异步 I/O 中的核心对象名为 Selector。Selector 就是您注册对各种 I/O 事件的兴趣的地方,

而且当那些事件发生时,就是这个对象告诉您所发生的事件。

所以,我们需要做的第一件事就是创建一个 Selector:

  1. Selector selector = Selector.open(); 

然后,我们将对不同的通道对象调用 register() 方法,以便注册我们对这些对象中发生的 I/O 事件的兴趣。register() 的第一个参数总是这个 Selector。

打开一个 ServerSocketChannel

为了接收连接,我们需要一个 ServerSocketChannel。事实上,

我们要监听的每一个端口都需要有一个 ServerSocketChannel 。对于每一个端口,我们打开一个 ServerSocketChannel,如下所示:

  1. ServerSocketChannel ssc = ServerSocketChannel.open();  
  2. ssc.configureBlocking( false );   
  3. ServerSocket ss = ssc.socket();  
  4. InetSocketAddress address = new InetSocketAddress( ports[i] );  
  5. ss.bind( address ); 

第一行创建一个新的 ServerSocketChannel ,最后三行将它绑定到给定的端口。

第二行将 ServerSocketChannel 设置为 非阻塞的 。

我们必须对每一个要使用的套接字通道调用这个方法,否则异步 I/O 就不能工作。

选择键

下一步是将新打开的 ServerSocketChannels 注册到 Selector上。为此我们使用 ServerSocketChannel.register() 方法,如下所示

  1. SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT ); 

register() 的第一个参数总是这个 Selector。第二个参数是 OP_ACCEPT,

这里它指定我们想要监听 accept 事件,也就是在新的连接建立时所发生的事件。

这是适用于 ServerSocketChannel 的唯一事件类型。

请注意对 register() 的调用的返回值。 SelectionKey 代表这个通道在此 Selector 上的这个注册。

当某个 Selector 通知您某个传入事件时,它是通过提供对应于该事件的 SelectionKey 来进行的

。SelectionKey 还可以用于取消通道的注册。

内部循环

现在已经注册了我们对一些 I/O 事件的兴趣,下面将进入主循环。

使用 Selectors 的几乎每个程序都像下面这样使用内部循环:

  1. int num = selector.select();   
  2. Set selectedKeys = selector.selectedKeys();  
  3. Iterator it = selectedKeys.iterator();   
  4. while (it.hasNext()) {  
  5.      SelectionKey key = (SelectionKey)it.next();  
  6.      // ... deal with I/O event ...} 

首先,我们调用 Selector 的 select() 方法。这个方法会阻塞,

直到至少有一个已注册的事件发生。当一个或者更多的事件发生时,

select() 方法将返回所发生的事件的数量。

接下来,我们调用 Selector 的 selectedKeys() 方法,

它返回发生了事件的 SelectionKey 对象的一个 集合。

我们通过迭代 SelectionKeys 并依次处理每个 SelectionKey 来处理事件。

对于每一个 SelectionKey,您必须确定发生的是什么 I/O 事件,以及这个事件影响哪些 I/O 对象。

监听新连接

程序执行到这里,我们仅注册了 ServerSocketChannel,

并且仅注册它们“接收”事件。为确认这一点,我们对 SelectionKey 调用readyOps() 方法,

并检查发生了什么类型的事件:

  1. if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {  
  2.         // Accept the new connection  
  3.        // ...  
  4. }  

可以肯定地说, readOps() 方法告诉我们该事件是新的连接。

接受新的连接

因为我们知道这个服务器套接字上有一个传入连接在等待,所以可以安全地接受它;

也就是说,不用担心 accept() 操作会阻塞:

  1. ServerSocketChannel ssc = (ServerSocketChannel)key.channel();  
  2. SocketChannel sc = ssc.accept(); 

下一步是将新连接的 SocketChannel 配置为非阻塞的。

而且由于接受这个连接的目的是为了读取来自套接字的数据,

所以我们还必须将 SocketChannel 注册到 Selector上,如下所示:

  1. sc.configureBlocking( false );  
  2. SelectionKey newKey = sc.register( selector, SelectionKey.OP_READ ); 

注意我们使用 register() 的 OP_READ 参数,

将 SocketChannel 注册用于 读取 而不是 接受 新连接。

删除处理过的 SelectionKey

在处理 SelectionKey 之后,我们几乎可以返回主循环了

。但是我们必须首先将处理过的 SelectionKey 从选定的键集合中删除

。如果我们没有删除处理过的键,那么它仍然会在主集合中以一个激活的键出现

,这会导致我们尝试再次处理它。我们调用迭代器的remove() 方法来删除处理过的 SelectionKey:

  1. it.remove(); 

现在我们可以返回主循环并接受从一个套接字中传入的数据(或者一个传入的 I/O 事件)了。

传入的 I/O

当来自一个套接字的数据到达时,它会触发一个 I/O 事件。

这会导致在主循环中调用 Selector.select(),并返回一个或者多个 I/O 事件。

这一次, SelectionKey 将被标记为 OP_READ 事件,如下所示:

  1. else if ((key.readyOps() & SelectionKey.OP_READ)     == SelectionKey.OP_READ) {  
  2.      // Read the data  
  3.      SocketChannel sc = (SocketChannel)key.channel();  
  4.      // ...} 

与以前一样,我们取得发生 I/O 事件的通道并处理它。在本例中,

由于这是一个 echo server,我们只希望从套接字中读取数据并马上将它发送回去。

每次返回主循环,我们都要调用 select 的 Selector()方法,并取得一组 SelectionKey。

每个键代表一个 I/O 事件。我们处理事件,从选定的键集中删除 SelectionKey,然后返回主循环的顶部。

这个程序有点过于简单,因为它的目的只是展示异步 I/O 所涉及的技术。

在现实的应用程序中,您需要通过将通道从 Selector 中删除来处理关闭的通道

。而且您可能要使用多个线程。这个程序可以仅使用一个线程,

因为它只是一个演示,但是在现实场景中,创建一个线程池来负责 I/O 事件处理中的耗时部分会更有意义。

字符集

根据 Sun 的文档,一个 Charset 是“十六位 Unicode 字符序列与字节序列之间的一个命名的映射”

。实际上,一个 Charset 允许您以尽可能最具可移植性的方式读写字符序列。

Java 语言被定义为基于 Unicode。然而在实际上,

许多人编写代码时都假设一个字符在磁盘上或者在网络流中用一个字节表示

。这种假设在许多情况下成立,但是并不是在所有情况下都成立,

而且随着计算机变得对 Unicode 越来越友好,这个假设就日益变得不能成立了。

在本节中,我们将看一下如何使用 Charsets 以适合现代文本格式的方式处理文本数据。

这里将使用的示例程序相当简单,不过,它触及了使用 Charset 的所有关键方面:为给定的字符编码创建 Charset,以及使用该 Charset 解码和编码文本数据。

编码/解码

要读和写文本,我们要分别使用 CharsetDecoder 和 CharsetEncoder。

将它们称为 编码器 和 解码器 是有道理的。一个 字符 不再表示一个特定的位模式,

而是表示字符系统中的一个实体。因此,由某个实际的位模式表示的字符必须以某种特定的 编码 来表示。

CharsetDecoder 用于将逐位表示的一串字符转换为具体的 char 值。

同样,一个 CharsetEncoder 用于将字符转换回位。

在下一个小节中,我们将考察一个使用这些对象来读写数据的程序。

处理文本的正确方式

现在我们将分析这个例子程序 UseCharsets.java。这个程序非常简单 ―

它从一个文件中读取一些文本,并将该文本写入另一个文件。但是它把该数据当作文本数据,

并使用 CharBuffer 来将该数句读入一个 CharsetDecoder 中。同样,

它使用 CharsetEncoder 来写回该数据。

我们将假设字符以 ISO-8859-1(Latin1) 字符集(这是 ASCII 的标准扩展)

的形式储存在磁盘上。尽管我们必须为使用 Unicode 做好准备,

但是也必须认识到不同的文件是以不同的格式储存的,而 ASCII 无疑是非常普遍的一种格式。

事实上,每种 Java 实现都要求对以下字符编码提供完全的支持:

• US-ASCII

• ISO-8859-1

• UTF-8

• UTF-16BE

• UTF-16LE

• UTF-16

示例程序

在打开相应的文件、将输入数据读入名为 inputData 的 ByteBuffer 之后,

我们的程序必须创建 ISO-8859-1 (Latin1) 字符集的一个实例:

  1. Charset latin1 = Charset.forName( "ISO-8859-1" ); 

然后,创建一个解码器(用于读取)和一个编码器 (用于写入):

  1. CharsetDecoder decoder = latin1.newDecoder();  
  2. CharsetEncoder encoder = latin1.newEncoder(); 

为了将字节数据解码为一组字符,我们把 ByteBuffer 传递给 CharsetDecoder,

结果得到一个 CharBuffer

  1. CharBuffer cb = decoder.decode( inputData ); 

如果想要处理字符,我们可以在程序的此处进行。但是我们只想无改变地将它写回,所以没有什么要做的。

要写回数据,我们必须使用 CharsetEncoder 将它转换回字节:

  1. ByteBuffer outputData = encoder.encode( cb ); 

在转换完成之后,我们就可以将数据写到文件中了。

结束语和参考资料

结束语

正如您所看到的, NIO 库有大量的特性。在一些新特性(例如文件锁定和字符集)

提供新功能的同时,许多特性在优化方面也非常优秀。

在基础层次上,通道和缓冲区可以做的事情几乎都可以用原来的面向流的类来完成

。但是通道和缓冲区允许以 快得多 的方式完成这些相同的旧操作 ― 事实上接近系统所允许的最大速度。

不过 NIO 最强大的长度之一在于,它提供了一种在 Java 语言中执行进行输入/输出的新的

(也是迫切需要的)结构化方式。随诸如缓冲区、通道和异步 I/O 这些概念性(且可实现的)

实体而来的,是我们重新思考 Java 程序中的 I/O过程的机会。这样,NIO 甚至为我们最熟悉的 I/O

过程也带来了新的活力,同时赋予我们通过和以前不同并且更好的方式执行它们的机会。

分享到:
评论

相关推荐

    dubbo最新全面深度解读

    本篇文章将深入探讨Dubbo的核心特性、工作原理以及实际应用,帮助开发者全面理解并掌握这一强大工具。 1. **Dubbo简介** Dubbo是基于Java的RPC框架,旨在提高服务的透明性和可扩展性。它提供了一种服务化的解决...

    Java底层知识点、源码解读,技术栈相关原理知识点、工具解读最佳实践、功能点实战,问题排查,开发技巧等

    8. **IO与NIO**: Java IO提供基于流的输入输出操作,而NIO(非阻塞I/O)引入了通道和缓冲区,提升了高并发场景下的性能。 9. **集合框架**: 遍历HashMap、ArrayList、LinkedList、TreeSet等集合的实现原理,掌握...

    JAVA程序员面试宝典 第4版-欧立奇

    - **I/O流与NIO**:I/O流是JAVA进行文件读写的基础,NIO则是一种新的输入输出方式。本章将对比这两种技术,并分析它们的优缺点。 - **JVM内存模型**:深入剖析JVM的内存布局,包括堆内存、栈内存等区域的作用及管理...

    Netty技术的全面解读

    这个全面解读将分为两个部分:《Netty权威指南》和《Netty In Action中文版--文字版》。 首先,让我们深入探讨《Netty权威指南》。这本书通常会涵盖Netty的基本概念,包括其设计理念、核心组件以及如何构建基于...

    java面试笔试题大汇总 ~很全面 -

    这份“Java面试笔试题大汇总”资料可能包含以上各个领域的经典题目和解析,通过系统学习和实践,可以帮助求职者全面提升Java技术水平,以应对各种面试挑战。CSDNBlog.htm和CSDNBlog_files两个文件可能包含了文章内容...

    Java网络高级编程源码人邮金勇华曲俊生

    书中可能详细解析了这些协议的工作原理,以及如何使用Java实现这些协议的客户端和服务器端。 5. **网络安全**:包括加密、认证和授权等,Java提供SSL/TLS支持,可以实现安全套接层的网络通信。此外,网络安全还包括...

    java超全面的面试总结

    Java作为世界上最受欢迎的编程语言之一,其面试题的深度和广度都相当广泛。这篇面试总结涵盖了Spring、MyBatis等关键框架的核心概念和技术,旨在帮助求职者充分准备技术面试。以下是一些重要的Java面试知识点: 1. ...

    JAVA高级工程师2

    虽然IO部分在此主题中提及较少,但作为全面的Java开发者,对于IO的理解同样重要,只是在本课程中可能不是重点。通过深入学习这些内容,开发者将能够提升自己的技术水平,更好地应对各种复杂项目的需求。

    Java教程文档

    这本书可能深入探讨Java的高级特性和技术,例如反射、注解、垃圾收集机制、内存管理、JVM(Java虚拟机)工作原理,以及性能优化策略。可能还会涵盖Java并发编程,包括线程池、同步机制、锁和并发容器的使用。此外,...

    Java开发典型模块大全(仅含程序源码)-20个Java项目

    8. **Web开发**:例如Servlet、JSP、Filter、Listener,以及MVC模式的实践,有助于理解Web应用程序的工作原理。 9. **GUI编程**:如Swing或JavaFX,用于创建桌面应用程序,理解事件处理、布局管理等。 10. **异常...

    java 面试 百度入职老哥整理 全是干货

    - Java中的网络IO模型,包括BIO、NIO和AIO的原理与区别。 6. Java安全 - Java中的安全加密相关知识点,包括了数据加密和安全通信的机制。 7. 操作系统和Linux相关知识 - 操作系统中进程、线程、同步机制等基础...

    java论文参考资料(供参考)

    附录中的英文翻译提供了对原理解读的国际视角,帮助读者更好地理解和应用这些知识。 1. **Java语言基础**: - 类与对象:Java是基于面向对象编程(OOP)的语言,其基本单位是类,通过类创建对象来实现数据和功能的...

    毕向东java基础ppt与源代码

    1. **Java概述**(01-Java概述.pdf):这部分内容主要介绍了Java语言的发展历程、特点和应用领域,包括Java的跨平台特性(Write Once, Run Anywhere),解释器和JVM(Java虚拟机)的工作原理,以及如何安装和配置...

    200+最常见Java面试题参考答案(嗯嗯).pdf

    为了能够帮助求职者更好地准备面试,这类面试题集和答案通常会涵盖Java的基础知识点、面向对象、集合框架、异常处理、多线程编程、JVM知识、IO/NIO、网络编程、数据库连接、设计模式、框架使用等重要部分。...

    Java2参考大全(java第四版)

    8. **Java虚拟机(JVM)**:探讨了JVM的工作原理,包括内存管理、垃圾收集以及性能优化,有助于开发者写出更高效的代码。 9. **泛型**:详细解读了Java泛型的使用,包括类型擦除、通配符和边界,提高了代码的类型...

    一些JAVA的基础教程书籍

    这篇教程集合将为你提供Java基础知识的全面学习,特别是针对网络应用程序开发的部分。以下是对这些书籍资源的详细解读: 1. **SL314_OH_GB.pdf** - 这个文件名可能代表"Oracle Help for Java Developers"的某个版本...

    java程序员的成长历程

    以下就是一篇关于“Java程序员的成长历程”的详细解读。 首先,Java初学者通常会从学习基础语法开始,包括变量、数据类型、控制结构(如if语句和循环)、类与对象的概念。理解这些基础知识是构建扎实编程技能的第一...

    java面试大全

    3. **多线程与并发编程**:Java提供了丰富的并发工具类,如synchronized、volatile、ThreadLocal、Lock、Future等,理解其工作原理和使用场景对于编写高效、安全的并发代码至关重要。 4. **JVM**:面试中常常会问到...

    Java核心技术 卷II 高级特性 原书第9版

    6. **垃圾收集与内存管理**:Java的自动内存管理机制,包括垃圾收集器的工作原理和内存分区,以及如何避免内存泄漏和提高应用性能。 7. **泛型**:泛型是Java 5引入的新特性,用于在编译时增强类型安全性,减少类型...

    【Java面试资料】-(机构内训资料)深圳-OPPO-Java高级

    8. **JVM内存模型**:理解堆内存、栈内存、方法区、本地方法栈的工作原理,以及垃圾回收(GC)机制。 9. **Spring框架**:包括依赖注入(DI)、AOP、事务管理、Spring Boot和Spring Cloud等相关知识,这些在企业级...

Global site tag (gtag.js) - Google Analytics