java io与nio学习

16 篇文章 0 订阅
12 篇文章 0 订阅
[size=large][color=red]输入/输出:概念性描述[/color][/size]
I/O 简介
I/O ? 或者输入/输出 ?[color=red] 指的是计算机与外部世界或者一个程序与计算机的其余部分的之间的接口。[/color]它对于任何计算机系统都非常关键,因而所有 I/O 的主体实际上是内置在操作系统中的。单独的程序一般是让系统为它们完成大部分的工作。
在 Java 编程中,直到最近一直使用 流 的方式完成 I/O。[size=large][color=red]所有 I/O 都被视为单个的字节的移动,通过一个称为 Stream 的对象一次移动一个字节。[/color][/size]流 I/O 用于与外部世界接触。它也在内部使用,用于将对象转换为字节,然后再转换回对象。
NIO 与原来的 I/O 有同样的作用和目的,但是它使用不同的方式? [color=red]块 I/O[/color]。正如您将在本教程中学到的,[b]块 I/O 的效率可以比流 I/O 高许多[/b]。

[size=large][color=red]为什么要使用 NIO[/color][/size]
[size=large][color=blue]NIO 的创建目的是为了让 Java 程序员可以实现高速 I/O 而无需编写自定义的本机代码。[/color][/size]NIO 将最耗时的 I/O 操作[color=red](即填充和提取缓冲区)转移回操作系统[/color],因而可以极大地提高速度。

[size=large][color=red]流与块的比较[/color][/size]

[color=red]原来的 I/O 库(在 java.io.*中) 与 NIO 最重要的区别是数据打包和传输的方式。[/color]正如前面提到的,[size=large][b]原来的 I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。[/b][/size]

面向流 的 I/O 系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。为流式数据创建过滤器非常容易。链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的 I/O 通常相当慢。
一个面向块的I/O系统以块的形式处理数据。[color=red]每一个操作都在一步中产生或者消费一个数据块。[/color]按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。

[color=red]
同步和异步区别:有无通知(是否轮询)
堵塞和非堵塞区别:操作结果是否等待(是否马上又返回值),只是设计方式的不同。
[/color]

NIO有一个主要的类Selector,这个类似一个观察者,只要我们把需要探知的SocketChannel告诉Selector,我们接着做别的事情,当有事件发生时,他会通知我们,[color=red]传回一组SelectionKey,我们读取这些Key,就会获得我们刚刚注册过的SocketChannel,然后,我们从这个Channel中读取数据,接着我们可以处理这些数据。[/color]


[size=large][color=red]集成的 I/O[/color][/size]
在 JDK 1.4 中原来的 I/O 包和 NIO 已经很好地集成了。 [color=red]java.io.* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。[/color]例如, java.io.* 包中的一些类包含以块的形式读写数据的方法,这使得即使在更面向流的系统中,处理速度也会更快。
也可以用 NIO 库实现标准 I/O 功能。例如,可以容易地使用块 I/O 一次一个字节地移动数据。但是正如您会看到的,NIO 还提供了原 I/O 包中所没有的许多好处。
Java IO

在Client/Server模型中,Server往往需要同时处理大量来自Client的访问请求,因此Server端需采用支持高并发访问的架构。[size=large][b]一种简单而又直接的解决方案是“one-thread-per-connection”。这是一种基于阻塞式I/O的多线程模型。[/b][/size][color=red]在该模型中,Server为每个Client连接创建一个处理线程,每个处理线程阻塞式等待可能达到的数据,一旦数据到达,则立即处理请求、返回处理结果并再次进入等待状态。[/color]由于每个Client连接有一个单独的处理线程为其服务,因此可保证良好的响应时间。但当系统负载增大(并发请求增多)时,Server端需要的线程数会增加,这将成为系统扩展的瓶颈所在。(Java IO与NIO的区别)


[img]http://dl2.iteye.com/upload/attachment/0097/1101/ad9b4108-f8a7-3c29-bd67-cffe1774615b.jpg[/img]

[size=large][b]Java NIO[/b][/size]

[b]Java NIO不但引入了全新的高效的I/O机制,同时引入了基于Reactor设计模式的多路复用异步模式。[/b]NIO包中主要包含以下几种抽象数据类型。
[color=red]* Channel(通道)[/color]:NIO把它支持的I/O对象抽象为Channel。它模拟了通信连接,类似于原I/O中的流(Stream),用户可以通过它读取和写入数据。目前已知的实例类有SocketChannel、ServerSocketChannel、DatagramChannel、FileChannel等。
[color=red]* Buffer(缓冲区)[/color]:Buffer是一块连续的内存区域,一般作为Channel收发数据的载体出现。所有数据都通过Buffer对象来处理。
[color=red]* Selector(选择器)[/color]:Selector类提供了监控一个和多个通道当前状态的机制。只要Channel向Selector注册了某种特定的事件,Selector就会监听这些事件是否会发生,一旦发生某个事件,便会通知对应的Channel。使用选择器,借助单一线程,就可对数量庞大的活动I/O通道实施监控和维护。


[img]http://dl2.iteye.com/upload/attachment/0097/1103/43bb5446-e5eb-3588-9fa1-485cd45f2d12.jpg[/img]

[color=red]Java NIO的服务端只需启动一个专门的线程来处理所有的IO事件,这种通信模型是怎么实现的呢?Java NIO采用了双向通道(channel)进行数据传输,而不是单向的流(stream),在通道上可以注册我们感兴趣的事件。[/color]一共有以下四种事件:

事件名 对应值
服务端接收客户端连接事件 SelectionKey.OP_ACCEPT(16)
客户端连接服务端事件 SelectionKey.OP_CONNECT(8)
读事件 SelectionKey.OP_READ(1)
写事件 SelectionKey.OP_WRITE(4)


[size=large][b]通道和缓冲区[/b][/size]
概述
[color=red]通道和缓冲区是NIO中的核心对象,几乎在每一个 I/O 操作中都要使用它们。[/color]
[b]通道是对原 I/O 包中的流的模拟。[/b]到任何目的地(或来自任何地方)的所有数据都必须通过一个 Channel 对象。[color=red]一个 Buffer 实质上是一个容器对象。发送给一个通道的所有对象都必须首先放到缓冲区中;同样地,从通道中读取的任何数据都要读到缓冲区中。[/color]

在本节中,您会了解到 NIO 中通道和缓冲区是如何工作的。
[size=large][b]什么是缓冲区?[/b][/size]
[color=red]Buffer 是一个对象, 它包含一些要写入或者刚读出的数据。[/color] 在 NIO 中加入 Buffer 对象,体现了新库与原 I/O 的一个重要区别。在面向流的 I/O 中,您将数据直接写入或者将数据直接读到 [color=red]Stream[/color] 对象中。
在 NIO 库中,[color=red]所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。任何时候访问 NIO 中的数据,您都是将它放到缓冲区中。[/color]
[color=red]缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。但是一个缓冲区不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。[/color]
缓冲区类型
最常用的缓冲区类型是 ByteBuffer。[color=red]一个 ByteBuffer 可以在其底层字节数组上进行 get/set 操作(即字节的获取和设置)。[/color]
ByteBuffer 不是 NIO 中唯一的缓冲区类型。事实上,[color=red]对于每一种基本 Java 类型都有一种缓冲区类型:[/color]
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
每一个 Buffer 类都是 Buffer 接口的一个实例。 除了 ByteBuffer,每一个 Buffer 类都有完全一样的操作,只是它们所处理的数据类型不一样。因为大多数标准 I/O 操作都使用 ByteBuffer,所以它具有所有共享的缓冲区操作以及一些特有的操作。
现在您可以花一点时间运行 UseFloatBuffer.java,它包含了类型化的缓冲区的一个应用例子。
什么是通道?
[color=red]Channel是一个对象,可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做个比较,通道就像是流。[/color]
正如前面提到的,所有数据都通过 Buffer 对象来处理。[color=red]您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。[/color]
通道类型
通道与流的不同之处在于[color=red]通道是双向的。[/color]而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类), [color=red]而 通道 可以用于读、写或者同时用于读写。[/color]
因为它们是双向的,所以通道可以比流更好地反映底层操作系统的真实情况。特别是在 UNIX 模型中,底层操作系统通道是双向的。

[b]从理论到实践:NIO 中的读和写[/b]
概述
读和写是 I/O 的基本过程。[color=red]从一个通道中读取很简单:只需创建一个缓冲区,然后让通道将数据读到这个缓冲区中。写入也相当简单:创建一个缓冲区,用数据填充它,然后让通道用这些数据来执行写入操作。[/color]
在本节中,我们将学习有关在 Java 程序中读取和写入数据的一些知识。我们将回顾 NIO 的主要组件(缓冲区、通道和一些相关的方法),看看它们是如何交互以进行读写的。在接下来的几节中,我们将更详细地分析这其中的每个组件以及其交互。
从文件中读取
在我们第一个练习中,我们将从一个文件中读取一些数据。如果使用原来的 I/O,那么我们只需创建一个 FileInputStream 并从它那里读取。而在 NIO 中,情况稍有不同:我们首先从 FileInputStream 获取一个 Channel 对象,然后使用这个通道来读取数据。
[color=red]在 NIO 系统中,任何时候执行一个读操作,您都是从通道中读取,但是您不是 直接 从通道读取。因为所有数据最终都驻留在缓冲区中,所以您是从通道读到缓冲区中。[/color]
因此读取文件涉及三个步骤:
(1) 从 FileInputStream 获取 Channel
(2) 创建 Buffer
(3) 将数据从 Channel 读到 Buffer 中
现在,让我们看一下这个过程。
三个容易的步骤
第一步是获取通道。我们从 FileInputStream 获取通道:


FileInputStream fin = new FileInputStream( "readandshow.txt" );
FileChannel fc = fin.getChannel();


下一步是创建缓冲区:


ByteBuffer buffer = ByteBuffer.allocate( 1024 );


最后,需要将数据从通道读到缓冲区中,如下所示:


fc.read( buffer );


您会注意到,我们不需要告诉通道要读 多少数据 到缓冲区中。每一个缓冲区都有复杂的内部统计机制,它会跟踪已经读了多少数据以及还有多少空间可以容纳更多的数据。我们将在 缓冲区内部细节 中介绍更多关于缓冲区统计机制的内容。
写入文件
在 NIO 中写入文件类似于从文件中读取。首先从 FileOutputStream 获取一个通道:


FileOutputStream fout = new FileOutputStream( "writesomebytes.txt" );
FileChannel fc = fout.getChannel();


下一步是创建一个缓冲区并在其中放入一些数据 - 在这里,数据将从一个名为 message 的数组中取出,这个数组包含字符串 "Some bytes" 的 ASCII 字节(本教程后面将会解释 buffer.flip() 和 buffer.put() 调用)。


ByteBuffer buffer = ByteBuffer.allocate( 1024 );

for (int i=0; i<message.length; ++i) {
buffer.put( message[i] );
}
buffer.flip();


最后一步是写入缓冲区中:


fc.write( buffer );


注意在这里同样不需要告诉通道要写入多数据。缓冲区的内部统计机制会跟踪它包含多少数据以及还有多少数据要写入。

缓冲区内部细节
概述
本节将介绍 NIO 中两个重要的缓冲区组件:[color=red]状态变量和访问方法[/color] (accessor)。
状态变量是前一节中提到的"内部统计机制"的关键。[color=red]每一个读/写操作都会改变缓冲区的状态。[/color]通过记录和跟踪这些变化,缓冲区就可能够内部地管理自己的资源。
在从通道读取数据时,数据被放入到缓冲区。在有些情况下,可以将这个缓冲区直接写入另一个通道,但是在一般情况下,您还需要查看数据。这是使用 访问方法 get() 来完成的。同样,如果要将原始数据放入缓冲区中,就要使用访问方法 put()。
在本节中,您将学习关于 NIO 中的状态变量和访问方法的内容。我们将描述每一个组件,并让您有机会看到它的实际应用。虽然 NIO 的内部统计机制初看起来可能很复杂,但是您很快就会看到大部分的实际工作都已经替您完成了。您可能习惯于通过手工编码进行簿记 ― 即使用字节数组和索引变量,现在它已在 NIO 中内部地处理了。
状态变量
可以用三个值指定缓冲区在任意时刻的状态:
position
limit
capacity
[color=red]这三个变量一起可以跟踪缓冲区的状态和它所包含的数据。[/color]我们将在下面的小节中详细分析每一个变量,还要介绍它们如何适应典型的读/写(输入/输出)进程。在这个例子中,我们假定要将数据从一个输入通道拷贝到一个输出通道。
Position
您可以回想一下,[color=red]缓冲区实际上就是美化了的数组。[/color]在从通道读取时,您将所读取的数据放到底层的数组中。 [color=red]position 变量跟踪已经写了多少数据。更准确地说,它指定了下一个字节将放到数组的哪一个元素中。[/color]因此,如果您从通道中读三个字节到缓冲区中,那么缓冲区的 position 将会设置为3,指向数组中第四个元素。
同样,在写入通道时,您是从缓冲区中获取数据。 position 值跟踪从缓冲区中获取了多少数据。更准确地说,它指定下一个字节来自数组的哪一个元素。因此如果从缓冲区写了5个字节到通道中,那么缓冲区的 position 将被设置为5,指向数组的第六个元素。
Limit
limit 变量表明还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。
position 总是小于或者等于 limit。
Capacity
缓冲区的 capacity 表明可以储存在缓冲区中的最大数据容量。实际上,它指定了底层数组的大小 ― 或者至少是指定了准许我们使用的底层数组的容量。
limit 决不能大于 capacity。
观察变量
我们首先观察一个新创建的缓冲区。出于本例子的需要,我们假设这个缓冲区的 总容量 为8个字节。 Buffer 的状态如下所示:

[img]http://dl2.iteye.com/upload/attachment/0097/1071/da7dfa99-5846-3b8d-ba5e-6160e468f659.gif[/img]

回想一下 ,limit 决不能大于 capacity,此例中这两个值都被设置为 8。我们通过将它们指向数组的尾部之后(如果有第8个槽,则是第8个槽所在的位置)来说明这点。

[img]http://dl2.iteye.com/upload/attachment/0097/1073/d5782137-fca6-3254-a90b-4fd31d1df15e.gif[/img]

position 设置为0。如果我们读一些数据到缓冲区中,那么下一个读取的数据就进入 slot 0 。如果我们从缓冲区写一些数据,从缓冲区读取的下一个字节就来自 slot 0 。 position 设置如下所示:

[img]http://dl2.iteye.com/upload/attachment/0097/1075/dc7055e5-08ed-3491-81f0-418133033993.gif[/img]

由于 capacity 不会改变,所以我们在下面的讨论中可以忽略它。
第一次读取
现在我们可以开始在新创建的缓冲区上进行读/写操作。首先从输入通道中读一些数据到缓冲区中。第一次读取得到三个字节。它们被放到数组中从 position 开始的位置,这时 position 被设置为 0。读完之后,position 就增加到 3,如下所示:

[img]http://dl2.iteye.com/upload/attachment/0097/1077/6c139000-fb0a-31fb-a765-11010a21089e.gif[/img]

limit 没有改变。
第二次读取
在第二次读取时,我们从输入通道读取另外两个字节到缓冲区中。这两个字节储存在由 position 所指定的位置上, position 因而增加 2:

[img]http://dl2.iteye.com/upload/attachment/0097/1083/80ae1453-b122-3c54-a1e5-39d191dc989a.gif[/img]

limit 没有改变。
flip
现在我们要将数据写到输出通道中。在这之前,我们必须调用 flip() 方法。这个方法做两件非常重要的事:
[color=red]它将 limit 设置为当前 position。
它将 position 设置为 0。[/color]
前一小节中的图显示了在 flip 之前缓冲区的情况。下面是在 flip 之后的缓冲区:

[img]http://dl2.iteye.com/upload/attachment/0097/1089/86d4ee28-8ef6-3a26-b2b2-e0a7537503ba.gif[/img]

我们现在可以将数据从缓冲区写入通道了。[color=red] position 被设置为 0,这意味着我们得到的下一个字节是第一个字节。 limit 已被设置为原来的 position,这意味着它包括以前读到的所有字节,并且一个字节也不多。[/color]
第一次写入
在第一次写入时,我们从缓冲区中取四个字节并将它们写入输出通道。这使得 position 增加到 4,而 limit 不变,如下所示:

[img]http://dl2.iteye.com/upload/attachment/0097/1091/dec4b0fc-e8dc-35b1-9d84-39db6e6e113f.gif[/img]

第二次写入
我们只剩下一个字节可写了。 limit在我们调用 flip() 时被设置为 5,并且 position 不能超过 limit。所以最后一次写入操作从缓冲区取出一个字节并将它写入输出通道。这使得 position 增加到 5,并保持 limit 不变,如下所示:

[img]http://dl2.iteye.com/upload/attachment/0097/1093/94f911ac-0930-3434-8343-4da5f9c2df61.gif[/img]

clear
最后一步是调用缓冲区的 clear() 方法。这个方法重设缓冲区以便接收更多的字节。 Clear 做两种非常重要的事情:
它将 limit 设置为与 capacity 相同。
它设置 position 为 0。
下图显示了在调用 clear() 后缓冲区的状态:

[img]http://dl2.iteye.com/upload/attachment/0097/1095/220f18c5-efb2-3831-a34c-ba180f21cba8.gif[/img]

缓冲区现在可以接收新的数据了。
访问方法
到目前为止,我们只是使用缓冲区将数据从一个通道转移到另一个通道。然而,程序经常需要直接处理数据。例如,您可能需要将用户数据保存到磁盘。在这种情况下,您必须将这些数据直接放入缓冲区,然后用通道将缓冲区写入磁盘。
或者,您可能想要从磁盘读取用户数据。在这种情况下,您要将数据从通道读到缓冲区中,然后检查缓冲区中的数据。
在本节的最后,我们将详细分析如何使用 ByteBuffer 类的 get() 和 put() 方法直接访问缓冲区中的数据。

Selectors
本节的阐述对应于 MultiPortEcho 的源代码中的 go() 方法的实现,因此应该看一下源代码,以便对所发生的事情有个更全面的了解。
异步 I/O 中的核心对象名为 Selector。Selector 就是您注册对各种 I/O 事件的兴趣的地方,而且当那些事件发生时,就是这个对象告诉您所发生的事件。
所以,我们需要做的第一件事就是创建一个 Selector:


Selector selector = Selector.open();


然后,我们将对不同的通道对象调用 register() 方法,以便注册我们对这些对象中发生的 I/O 事件的兴趣。register() 的第一个参数总是这个 Selector。
打开一个 ServerSocketChannel
为了接收连接,我们需要一个 ServerSocketChannel。事实上,我们要监听的每一个端口都需要有一个 ServerSocketChannel 。对于每一个端口,我们打开一个 ServerSocketChannel,如下所示:


ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking( false );

ServerSocket ss = ssc.socket();
InetSocketAddress address = new InetSocketAddress( ports[i] );
ss.bind( address );


第一行创建一个新的 ServerSocketChannel ,最后三行将它绑定到给定的端口。第二行将 ServerSocketChannel 设置为 非阻塞的 。我们必须对每一个要使用的套接字通道调用这个方法,否则异步 I/O 就不能工作。

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


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


register() 的第一个参数总是这个 Selector。第二个参数是 OP_ACCEPT,这里它指定我们想要监听 accept 事件,也就是在新的连接建立时所发生的事件。这是适用于 ServerSocketChannel 的唯一事件类型。
请注意对 register() 的调用的返回值。 SelectionKey 代表这个通道在此 Selector 上的这个注册。当某个 Selector 通知您某个传入事件时,它是通过提供对应于该事件的 SelectionKey 来进行的。SelectionKey 还可以用于取消通道的注册。
内部循环
现在已经注册了我们对一些 I/O 事件的兴趣,下面将进入主循环。使用 Selectors 的几乎每个程序都像下面这样使用内部循环:


int num = selector.select();

Set selectedKeys = selector.selectedKeys();
Iterator it = selectedKeys.iterator();

while (it.hasNext()) {
SelectionKey key = (SelectionKey)it.next();
// ... deal with I/O event ...
}


首先,我们调用 Selector 的 select() 方法。这个方法会阻塞,直到至少有一个已注册的事件发生。当一个或者更多的事件发生时, select() 方法将返回所发生的事件的数量。
接下来,我们调用 Selector 的 selectedKeys() 方法,它返回发生了事件的 SelectionKey 对象的一个 集合 。
我们通过迭代 SelectionKeys 并依次处理每个 SelectionKey 来处理事件。对于每一个 SelectionKey,您必须确定发生的是什么 I/O 事件,以及这个事件影响哪些 I/O 对象。
监听新连接
程序执行到这里,我们仅注册了 ServerSocketChannel,并且仅注册它们“接收”事件。为确认这一点,我们对 SelectionKey 调用 readyOps() 方法,并检查发生了什么类型的事件:


if ((key.readyOps() & SelectionKey.OP_ACCEPT)
== SelectionKey.OP_ACCEPT) {

// Accept the new connection
// ...
}


转自:http://www.ibm.com/developerworks/cn/education/java/j-nio/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值