一、Java IO 和 系统 IO 不匹配

  在大多数情况下,Java 应用程序并非真的受着 I/O 的束缚。操作系统并非不能快速传送数据,让 Java 有事可做;相反,是 JVM 自身在 I/O 方面效率欠佳。操作系统与 Java 基于流的 I/O模型有些不匹配。操作系统要移动的是大块数据(缓冲区),这往往是在硬件直接存储器存取(DMA)的协助下完成的。而 JVM 的 I/O 操作类喜欢操作小块数据——单个字节、几行文本。结果,操作系统送来整缓冲区的数据,java.io 包的流数据类再花大量时间把它们拆成小块,往往拷贝一个小块就要往返于几层对象。操作系统喜欢整卡车地运来数据,java.io 类则喜欢一铲子一铲子地加工数据。有了 NIO,就可以轻松地把一卡车数据备份到您能直接使用的地方(ByteBuffer 对象)。

  这并不是说使用传统的 I/O 模型无法移动大量数据——当然可以(现在依然可以)。具体地说,RandomAccessFile类在这方面的效率就不低,只要坚持使用基于数组的read()write()方法。

  这些方法与底层操作系统调用相当接近,尽管必须保留至少一份缓冲区拷贝。

  为了解决这一问题,java.nio 软件包提供了新的抽象。具体地说,就是 ChannelSelector类。

二、 缓冲区

(一)缓冲区操作

缓冲区,以及缓冲区如何工作,是所有 I/O 的基础。所谓“输入/输出”讲的无非就是把数据移进或移出缓冲区。

进程执行 I/O 操作,归结起来,也就是向操作系统发出请求,让它要么把缓冲区里的数据排干(写),要么用数据把缓冲区填满(读)。进程使用这一机制处理所有数据进出操作。操作系统内部处理这一任务的机制,其复杂程度可能超乎想像,但就概念而言,却非常直白易懂。图 1-1 简单描述了数据从外部磁盘向运行中的进程的内存区域移动的过程。进程使用read()系统调用,要求其缓冲区被填满。内核随即向磁盘控制硬件发出命令,要求其从磁盘读取数据。磁盘控制器把数据直接写入内核内存缓冲区,这一步通过 DMA 完成,无需主CPU协助。一旦磁盘控制器把缓冲区装满,内核即把数据从内核空间的临时缓冲区拷贝到进程执行read()调用时指定的缓冲区。

图 1-1. I/O 缓冲区操作简图

JVM 就是常规进程,驻守于用户空间。用最重要的是,所有 I/O 都直接或间接通过内核空间。当进程请求 I/O 操作的时候,它执行一个系统调用(有时称为陷阱)将控制权移交给内核。C/C++程序员所熟知的底层函数open()read()write()close()要做的无非就是建立和执行适当的系统调用。当内核以这种方式被调用,它随即采取任何必要步骤,找到进程所需数据,并把数据传送到用户空间内的指定缓冲区。内核试图对数据进行高速缓存或预读取,因此进程所需数据可能已经在内核空间里了。如果是这样,该数据只需简单地拷贝出来即可。如果数据不在内核空间,则进程被挂起,内核着手把数据读进内存。

为什么不直接让磁盘控制器把数据送到用户空间的缓冲区呢?这样做有几个问题。首先,硬件通常不能直接访问用户空间。其次,像磁盘这样基于块存储的硬件设备操作的是固定大小的数据块,而用户进程请求的可能是任意大小的或非对齐的数据块。在数据往来于用户空间与存储设备的过程中,内核负责数据的分解、再组合工作,因此充当着中间人的角色。

(二) 发散/汇聚

许多操作系统能把组装/分解过程进行得更加高效。根据发散/汇聚的概念,进程只需一个系统调用,就能把一连串缓冲区地址传递给操作系统。然后,内核就可以顺序填充或排干多个缓冲区,读的时候就把数据发散到多个用户空间缓冲区,写的时候再从多个缓冲区把数据汇聚起来(图1-2)。

图 1-2. 三个缓冲区的发散读操作

这样用户进程就不必多次执行系统调用(那样做可能代价不菲),内核也可以优化数据的处理过程,因为它已掌握待传输数据的全部信息。如果系统配有多个 CPU,甚至可以同时填充或排干多个缓冲区。

三、 流I/O

并非所有 I/O 都像前几节讲的是面向块的,也有流 I/O,其原理模仿了通道。I/O 字节流必须顺序存取,常见的例子有TTY(控制台)设备、打印机端口和网络连接。

流的传输一般(也不必然如此)比块设备慢,经常用于间歇性输入。多数操作系统允许把流置于非块模式,这样,进程可以查看流上是否有输入,即便当时没有也不影响它干别的。这样一种能力使得进程可以在有输入的时候进行处理,输入流闲置的时候执行其他功能。

比非块模式再进一步,就是就绪性选择。就绪性选择与非块模式类似(常常就是建立在非块模式之上),但是把查看流是否就绪的任务交给了操作系统。操作系统受命查看一系列流,并提醒进程哪些流已经就绪。这样,仅仅凭借操作系统返回的就绪信息,进程就可以使用相同代码和单一线程,实现多活动流的多路传输。这一技术广泛用于网络服务器领域,用来处理数量庞大的网络连接。就绪性选择在大容量缩放方面是必不可少的。

四、Socket通道

DatagramChannelSocketChannel实现定义读和写功能的接口而ServerSocketChannel不实现。ServerSocketChannel负责监听传入的连接和创建新的SocketChannel对象,它本身从不传输数据。

socket和socket通道之间的关系。之前的章节中有写道,通道是一个连接I/O服务导管并提供与该服务交互的方法。就某个socket而言,它不会再次实现与之对应的socket通道类中的socket协议 API,而java.net中已经存在的socket通道都可以被大多数协议操作重复使用。

全部socket通道类(DatagramChannelSocketChannelServerSocketChannel)在被实例化时都会创建一个对等socket对象。这些是我们所熟悉的来自java.net的类(SocketServerSocketDatagramSocket),它们已经被更新以识别通道。对等socket可以通过调用socket()方法从一个通道上获取。此外,这三个java.net类现在都有getChannel()方法。

虽然每个socket通道(在java.nio.channels包中)都有一个关联的java.net socket对象,却并非所有的socket都有一个关联的通道。如果您用传统方式(直接实例化)创建了一个Socket对象,它就不会有关联的SocketChannel并且它的getChannel()方法将总是返回null

五、ServerSocketChannel

让我们从最简单的ServerSocketChannel来开始对socket通道类的讨论。以下是ServerSocketChannel的完整 API:

public abstract class ServerSocketChannel extends AbstractSelectableChannel {
public static ServerSocketChannel open() throws IOException
public abstract ServerSocket socket();
public abstract ServerSocket accept() throws IOException;
public final int validOps()
}

ServerSocketChannel是一个基于通道的socket监听器。它同我们所熟悉的java.net.ServerSocket执行相同的基本任务,不过它增加了通道语义,因此能够在非阻塞模式下运行。

用静态的open()工厂方法创建一个新的ServerSocketChannel对象,将会返回同一个未绑定的java.net.ServerSocket关联的通道。该对等ServerSocket可以通过在返回的ServerSocketChannel上调用socket()方法来获取。作为ServerSocketChannel的对等体被创建的ServerSocket对象依赖通道实现。这些socket关联的SocketImpl能识别通道。通道不能被封装在随意的socket对象外面。

由于ServerSocketChannel没有bind()方法,因此有必要取出对等的socket并使用它来绑定到一个端口以开始监听连接。我们也是使用对等ServerSocket的API来根据需要设置其他的socket选项。

ServerSocketChannel ssc = ServerSocketChannel.open();
ServerSocket serverSocket = ssc.socket();
// 监听端口1234
serverSocket.bind (new InetSocketAddress(1234));

同它的对等体java.net.ServerSocket一样,ServerSocketChannel也有accept()方法。一旦您创建了一个ServerSocketChannel并用对等socket绑定了它,然后您就可以在其中一个上调用accept()。如果您选择在ServerSocket上调用accept()方法,那么它会同任何其他的ServerSocket表现一样的行为:总是阻塞并返回一个java.net.Socket对象。如果您选择在ServerSocketChannel上调用accept()方法则会返回SocketChannel类型的对象,返回的对象能够在非阻塞模式下运行。假设系统已经有一个安全管理器(security manager),两种形式的方法调用都执行相同的安全检查。

如果以非阻塞模式被调用,当没有传入连接在等待时,ServerSocketChannel.accept()会立即返回null。正是这种检查连接而不阻塞的能力实现了可伸缩性并降低了复杂性。可选择性也因此得到实现。我们可以使用一个选择器实例来注册一个ServerSocketChannel 对象以实现新连接到达时自动通知的功能。例 3-7 演示了如何使用一个非阻塞的accept()方法:

/*
*例 3-7 使用ServerSocketChannel的非阻塞accept()方法
*/
package com.ronsoft.books.nio.channels;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.net.InetSocketAddress;
/**
* Test nonblocking accept() using ServerSocketChannel.
* Start this program, then "telnet localhost 1234" to
* connect to it.
*
* @author Ron Hitchens (ron@ronsoft.com)
*/
public class ChannelAccept {
public static final String GREETING = "Hello I must be going.\r\n"; public static void main (String [] argv) throws Exception {
int port = 1234; //默认端口
if (argv.length > 0) {
port = Integer.parseInt(argv[0]);
}
ByteBuffer buffer = ByteBuffer.wrap (GREETING.getBytes());
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind (new InetSocketAddress(port));
ssc.configureBlocking(false);
while(true) {
System.out.println ("Waiting for connections");
SocketChannel sc = ssc.accept();
if (sc == null) {
// no connections, snooze a while
Thread.sleep (2000);
} else {
System.out.println ("Incoming connection from: " + sc.socket().getRemoteSocketAddress());
buffer.rewind();
sc.write(buffer);
sc.close();
}
}
}
}

前面列出的最后一个方法validOps()是同选择器一起使用的。

Reference:

Java nio入门教程详解(三)

Java nio 笔记:系统IO、缓冲区、流IO、socket通道的更多相关文章

  1. Java开发笔记(九十一)IO流处理简单的数据压缩

    前面介绍的文件I/O,不管是写入文本还是写入对象,文件中的数据基本是原来的模样,用记事本之类的文本编辑软件都能浏览个大概.这么存储数据,要说方便确实方便,只是不够经济划算,原因有二:其一,写入的数据可 ...

  2. JAVA NIO 中的 zerocopy 技术提高IO性能

    关于一篇更详细更好的介绍 ZeroCopy技术的文章,可参考:JAVA IO 以及 NIO 理解 这篇文章介绍了 zerocopy技术来提高Linux平台上的IO密集型的JAVA应用程序的性能. ze ...

  3. 【Java nio】java nio笔记

    缓冲区操作:缓冲区,以及缓冲区如何工作,是所有I/O的基础.所谓“输入/输出”讲的无非就是把数据移出货移进缓冲区.进程执行I/O操作,归纳起来也就是向操作系统发出请求,让它要么把缓冲区里的数据排干,要 ...

  4. Java NIO笔记(一):NIO介绍

    Java NIO即Java Non-blocking IO(Java非堵塞I/O),由于是在Jdk1.4之后添加的一套新的操作I/O工具包,所以通常会被叫做Java New IO.NIO是为提供I/O ...

  5. Java NIO (二) 缓冲区(Buffer)

    缓冲区(Buffer):一个用于特定基本数据类型的容器,由 java.nio 包定义的,所有缓冲区都是 Buffer 抽象类的子类. Java NIO 中的Buffer 主要用于和NIO中的通道(Ch ...

  6. Java学习笔记40(缓冲流)

    缓冲流: 在读写文件的各种流中,最令人烦恼的就是效率问题, 而缓冲流的目的就是提高读写效率 字节输出缓冲流: package demo; import java.io.BufferedOutputSt ...

  7. Java NIO(二)缓冲区

    概念 缓冲区:一个用于特定基本数据类型的容器,由java.nio包定义的所有缓冲区都是Buffer抽象类的子类.其作用于与NIO的通道进行交互,数据从通道读入缓冲区,数据从缓冲区写入通道 Buffer ...

  8. Java NIO之Buffer(缓冲区)

    ​ Java NIO中的缓存区(Buffer)用于和通道(Channel)进行交互.数据是从通道读入缓冲区,从缓冲区写入到通道中的. ​ 缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存.这 ...

  9. Java NIo 笔记001

    1. Channel Channel接口只提供了两个方法: package java.nio.channels; public interface Channel { public boolean i ...

随机推荐

  1. python 动态加载module、class、function

    python作为一种动态解释型语言,在实现各种框架方面具有很大的灵活性. 最近在研究python web框架,发现各种框架中需要显示的定义各种路由和Handler的映射,如果想要实现并维护复杂的web ...

  2. Finally 与 return

    网上有很多人探讨Java中异常捕获机制try...catch...finally块中的finally语句是不是一定会被执行?很多人都说不是,当然他们的回答是正确的,经过我试验,至少有两种情况下fina ...

  3. C++ 中static 使用大全

    /// 静态全局变量 :只能在当前cpp中访问到  static int s_global = 0; void funcA() {      /// 静态局部变量 (函数静态变量) 初始化过一次就不会 ...

  4. java ReentrantReadWriteLock

    // read and write lock is mutual exclusion lock //Listing 7-3. Using ReadWriteLock to Satisfy a Dict ...

  5. 用GitLab搭建自己的私有GitHub

    相信很多技术人员都知道有个github造福开发人员的git(分布式版本管理工具)代码管理社区,可以说现在git是开发人员的必备技能之一 本周有个朋友公司需要一个类似github的代码仓库管理系统,本人 ...

  6. 深入GetMessage和PeekMessage

    http://blog.csdn.net/fireseed/article/details/2176 http://www.cnblogs.com/sadier/articles/100948.htm ...

  7. c语言作业

  8. nrf51822裸机教程-IIC

    关于IIC总线的核心有以下几点: :时钟线高电平期间必须保持数据线不变. :时钟线低电平期间可以改变数据. :时钟线和数据线上都要接上拉电阻,以使总线不工作时,两根线的电平都处于高电平状态. :每个传 ...

  9. Spark学习笔记(一)

    1.调度 分为FIFO和FAIR两种模式 创建调度池:sc.setLocalProperty("spark.scheduler.pool", "pool6") ...

  10. php比较加赋值语句

    $a=-2;if ($a < 0 && $a = 1) { echo $a;} 输出1 右面的$a=1可不是条件哦,而是赋值