admin管理员组

文章数量:1567521

2024年7月25日发(作者:)

Java NIO教程 前言

阅读本文前,建议你先了解 旧I/O

NIO 是 New I/O 的缩写,要了解它真正的内涵,需要掌握的知识还是比较多的。我

努力在这几篇笔记里,勾勒出整个io的面貌。为大家的深入学习铺路。

I/O简史

想理解I/O的全部,java的I/O历史是必须要了解的。java的I/O历史也从一个侧面

反应了java的发展史。

JDK1.0-1.3

在这个时期的java中,基本上可以说没有完整的I/O支持。因为这一时期的java I/O

操作是阻塞的,所以I/O效率是较为底下的,基本上想要有比较好的I/O解决方案,基本

靠自己。这时期java在服务器端一直没有得到重用,和糟糕的I/O效率是有很大的关系的。

不但I/O弄的不好,而且一系列周边措施都没弄好。所支持的字符集编码有限,经常要进

行手工的编码工作。而且没有正则表达式,处理数据十分困难。

JDK1.4-1.6

2002年发布的java1.4中,非阻塞I/O以JSR-51的身份加入到java语言中。同时字

符集的编解码能力大大提升。而且有了基于perl实现的正则表达式类库。同时部分旧I/O

底层实现,也用新I/O的方式重写,使得旧I/O的性能也有了提升。终于java在服务器端

开始流行了起来。

与此同时,第三方也开始发力。谷歌发布了Guava类库,其中的I/O部分,极大的简

化了一些文件的操作和数据的传输。同时Trustin Lee领导编写的nio框架netty与mina

也广为流传开来,这对java nio的发展业是有着极大的推动力的。

JDK1.7至今

随着JSR-203的推出,是我们在java1.7中见到了NIO2。它为我们提供了必非阻塞

更加强大的异步I/O操作能力,同时提供了一系列极为方便的对文件系统和文件属性进行

操作的API。以及更加强大的网络I/O

I/O区别

阻塞I/O、非阻塞I/O、异步I/O之间到底有什么区别?为什么每一次的进步,都会促

使java I/O能力的极大提升?我们举一个种菜游戏的例子。

假如有一个种菜游戏(就跟之前的QQ农场类似),在玩家种菜以后,必须一直呆在那

个网页上,看着菜成熟,才可以收菜。这是极其浪费时间的,用户体验也一定不会好。这

个游戏后来进行了改版,玩家种菜之后不用再一直停留在那个网页上了,只是需要时不时

来看一遍,如果某一次查看时发现菜成熟了,就可以收菜了。当然,用户体验极大的提升

了,用户所浪费的时间也减少了,但是为了更加提升用户体验,游戏又进行了改版。玩家

种菜之后,不用再查看菜是否成熟了,等到菜成熟后,该游戏会自动给用户发送一个通知,

告诉他,菜已成熟、赶紧来收。这样用户的基本上再也不用浪费时间了。

刚刚例子中的三个游戏版本,代表了三种I/O。阻塞I/O:在数据没有读写完成之前,

CPU不可以进行下一步操作,这样CPU只好眼睁睁的在那里傻等。非阻塞I/O:在数据没

有读写完成之前,CPU可以离开,只需要每隔一段时间询问一次。异步I/O:在数据没有

读写完成之前,CPU可以离开也不用时不时的关心一下I/O,在数据读写完成时,主动通

知CPU。三种I/O之间的效率,高低立判。

新I/O

咱们以java1.4所提出的非阻塞I/O,为切入点,开始了解全貌。

• Channels

• Buffers

• Selectors

这三个类构成了非阻塞I/O的核心API。

Buffer译为缓存区,它是一块可以存储数据的内存。Channel有点像流,但它可读可

写、从本地I/O到网络I/O都可以,绝大多数NIO都从一个Channel开始的,数据可以

从Channel读到Buffer中,也可以从Buffer 写到Channel中。

非阻塞I/O中,CPU可以在数据没有读写完成之前离开,只需要每隔一段时间询问一

次。询问数据是否读写完成,需要的CPU能力是极小的,但如果CPU经常切换任务所需

要的保留现场和恢复现场的时间是较大的。所以可以就用一个线程来询问数据是否准备好。

一个线程在多个通道内询问数据是否准备好,就需要管理多个通道的方式,这就是

使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法

会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件。

说了这么多,我们该从什么地方开始呢? 咱们从java7的新文件系统开始吧

Java NIO教程 文件系统

在NIO.2的文件系统中,Path是一切操作的基础。Path准确来说,代表着文件系统

中的位置。可以代表一个目录(也就是通常所说的文件夹),也可以代表一个文件。

在新文件系统中,还有一个不得不说的就是Files。它是一个工具类,但是这个工具类

跟打了鸡血一样,强大到不可思议。以前需要写繁重代码或者需要调用第三方类库才能完

成的功能,现在只需一行。

下面的代码展示了Path的最基本操作-获取一个Path;并且通过我们得到的Path和

强大的工具类Files来创建一个文件或目录

Path dir0 = ("c:test");// 创建一个Path路径;Paths是工具类,用于产

生Path

Directory(dir0);// 创建单级目录;若已经存在 则抛出异常

Path dir1 = ("c:");

File(dir1);// 创建文件;若目录没有 则抛出异常

Path dir2 = ("c:testHelloWorld");

Directories(dir2);// 创建多级目录;若已经存在 不抛出异常

展示完最基础的创建功能后,再展示一点Path的高级功能——遍历目录

遍历目录有两种方式,一种是通过DirectoryStream类来遍历单层目录,另一种就是

通过实现FileVisitor接口来递归的遍历目录

重点强调一下这两者的区别。DirectoryStream只能遍历给定的目录下的文件或目录,

如果在给定的目录下还有子目录,那么DirectoryStream不会再向下遍历子目录中的内容。

而用实现FileVisitor接口的方式,则在给定的目录下若还有子目录,它会接着遍历子目录

中的内容(类似于深度优先搜索)

上代码 首先给出的是DirectoryStream的方式

Path dir3 = ("c:test");

// oryStream可以理解成对于Path的Iterable,返回在dir3中符合第二个

参数形式的Path集合

// 2.下面用到了一种try-with-resources的写法,该写法是在java1.7中引入的语法

糖(极力推荐大家用该写法,因为它可以帮助你正确的关闭资源)

try (DirectoryStream stream = ectoryStream(dir3,

"*.java")) {

for (Path entry : stream) {

n(eName());

}

}

再强调一下

ectoryStream

的第二个参数。这种形式是glob表达式,如

*.java代表的就是所有以".java"结尾的字符串

下面再说一下,实现FileVisitor接口的方式。这里得用到Files的一个静态方法

leTree(Path start,FileVisitor visitor)

第一眼就感觉好复杂,而且一看walkFileTree的二个参数就不是善茬,要实现它就得

实现下面的4个方法

• FileVisitResult postVisitDirectory(T dir, IOException exc)

• FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)

• FileVisitResult visitFile(T file, BasicFileAttributes attrs)

• FileVisitResult visitFileFailed(T file, IOException exc)

但好在java1.7已经提供了一个默认实现类SimpleFileVisitor,这样你就可以只用重

写你需要的方法了,下面的例子是打印一个目录下所有文件的名称(文件夹的名称不打印)

Path dir3 = ("c:test");

leTree(dir3, new SimpleFileVisitor() {

@Override

public FileVisitResult visitFile(Path file,

BasicFileAttributes attrs) throws IOException {

n(eName());

return UE;

}

});

两段困难的代码看完了,咱们再来的简单的。

文件的创建、删除、复制、移动、重命名等操作,都是我们平时常用的操作。以前想

写的话,真得下点功夫或者直接找个第三方类库。但是现在时代不同了,有了Files一切如

丝般顺滑。看看Files的文档吧,那里有许多你梦遗所求的方法。这里展示几个常用的。

先来一段删除文件或目录的

Path dir4 = ("c:testdel");

(dir4);// 删除文件或文件夹;删除文件夹时,若文件夹中还有文件或文件

夹则抛出异常

IfExists(dir4);// 如果存在该文件或文件夹则删除

删除很简单吧,下面让我们来看看复制

复制要考虑的情况就有点多了,复制我们主要用到的方法是

(Path source,Path options)

前两个参数分别为原位置和目的位置,第三个参数为复制选项。复制选项的意思,说

白了就是复制时发生了各种情况时,该如何处理。

CopyOption

其实也应该是让你写的,

但是java中内置了三种方式供你选择,它们分别是:

E_EXISTING:如果目的路径有同名文件则替换

_MOVE:若失败则回滚

_ATTRIBUTES:把源文件的文件属性一同复制给新文件

(readAttributes等方法可以读取文件属性)

给你个例子

Path source = ("c:");

Path target = ("c:");

(source, target, E_EXISTING);

再就说说文件的移动,基本上与复制同理,主要用到的方法是

(Path source,Path options)

同样,也需要复制选项,其实给文件重命名用的也是这招,下面的例子就展示了这点

Path source = ("c:");

Path target = ("c:");

(source, target, E_EXISTING);

下面要介绍的就是我最喜欢的内容了,给你们看两个方法

List lLines(Path path)

(Path path,Iterable

options)

是不是一目了然。对这就是文件的按行读和按行写,用起来真的特别方便。在java1.7

之前,我在guava中看见过这些方法,有时候用guava的io其实根本不是为了它的source

和sink,而就是为了这几个方法。这两个方法的出现,反映了java的一种新态度,那就是

纳谏、虚心。优秀的命名、方便的工具类,这都是以前java官方中所欠缺的,无论是新的

Time包还是lamda表达式或是Coin项目,都是向社区学习、吸收优秀的社区资源,都

是将开发者的梦想变为现实、方便开发者。

不好意思扯远了。那就直接上代码了

Path source = ("c:");

Path target = ("c:");

//以防乱码最好设置一下文件的编码格式,在StandardCharsets中有很多种内置编

码格式

List list = lLines(source, _8);

for (String s : list) {

n(s);

}

//在StandardOpenOption中有很多种内置文件打开方式可供选择

(target, list, _8,);

整个NIO.2中的新文件系统的主要内容,也就基本这么多(文件属性这块没太说,想

学的自己找找资料吧)最后咱们来的略显高端的就是文件系统的监测;主要用到的类是

ervice,该类在注册后,一直监控文件或目录的变化,若有相应的变

化发生,就返回一个事件。这在很多地方都是有用的,而且这个类性能不错

try (WatchService watcher = ault().newWatchService()) {

Path dir = ("c:test");

WatchKey key = er(watcher,

_DELETE);

for (;;) {

key = ();

for (WatchEvent event : ents()) {

n(());

}

boolean valid = ();

if (!valid) {

break;

}

}

}

关于NIO.2中的新文件系统就聊这么多,有什么问题欢迎讨论,若有什么错误,望不

吝赐教(赶紧告诉我,我好改,改晚了耽误其他人)

Java NIO教程 Buffer

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存,这块内存中有很

多可以存储byte(或int、char等)的小单元。这块内存被包装成NIO Buffer对象,并

提供了一组方法,用来方便的访问该块内存。

为了理解Buffer的工作原理,需要熟悉它的三个属性:

• capacity

• position

• limit

简单的解释这三个属性的含义可以概括为:capacity代表这块Buffer的容量,position

代表下一次读(或写)的位置,limit代表本次读(或写)的极限位置。这么简单说一下你

当然是听不懂的啦(这样一下你就听懂了,岂不是显得我很没有存在感)所以下面开始详

细的讲解。

capacity

作为一个内存块,Buffer有一个固定的大小值(这个大小是刚开始申请的),叫作

“capacity”.你只能往里写capacity个byte、int,char等类型。

position

当你要写数据到Buffer中时,position表示当前可写的位置。初始的position值为

0(也可以用过方法进行改变)。当一个byte、int等数据写到Buffer后, position会向

前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1.

当读取数据时,也是从某个特定位置读。当从Buffer的position处读取数据完成时,

position向前移动到下一个可读的位置。

limit

在写数据时,Buffer的limit表示你最多能往Buffer里写多少数据,position移动到

limit写操作停止。初始limit的值等于Buffer的capacity。当读取数据时, limit表示你

最多能读到多少数据,position移动到limit读操作停止。

无论在读数据时还是在写数据时,只要position超过了limit就会抛出异常。

控制position和limit的值

capacity的值是根据申请Buffer的大小和种类确定的,所以不能改变。而position

和limit就可以根据我的需要而改变了,首先介绍一下如何查看这三个属性的值:

()、on()、ty()这三个方法直观、方便,我们就不再

罗嗦。

接下来我们要着重看一下()这个方法一般用在写到读切换的时候。这个方法

的能力就是将

limit

设为

position

的值,再是将

position

设为0。

这么做的用处是什么呢?你想想,在写数据的时候从Buffer的开始处—

0位置

position位置

之间已经写满了数据,如果这时候我们想要从头开始读数据的话,就要将

position指向0,以便可以读取0位置的数据,然后逐个向下读取;但要读到什么位置为

止呢?如果整个Buffer都读完的话,刚才所写的最后一个单元以后的单元,都是空,读取

它们没有意义。所以读取到刚才所写的最后一个单元,是明智之举。而在将position置为

0之前,position值就是刚才所写的最后一个单元的位置。所以在写到读切换的时候,将

limit

设为

position

的值,再是将

position

设为0。

这个明白了以后一切都顺了。()是清空Buffer的方法,但它没有真正的清

除,只是将position置为0,将limit置为capacity;这样一来,你的写操作就可以将原

来的数据覆盖了。就是这么简单。()是将position置为0,这样一来就可以

将buffer再重新读一遍,当然你还可通过它干很多事。

其实还有很多有用、有趣的方法,看看api文档吧。

基础实例 和 btye与其他类型的转换

说了这么多理论,再不上代码就有人得骂街了,来个最基础的申请buffer和基本的读

写吧

ByteBuffer bb = te(48);

/*向ByteBuffer中put数据的时候,一下四种形式都可以

* put(byte b)

* put(byte[] src)

* put(byte[] src, int offset, int length)

* put(ByteBuffer src)

* 四种形式都会移动position指针

*/

(new byte[]{1,2,4,2,-13});

();

//hasRemaining()的作用是看看position到没到limit位置

while(aining()) {

n(());

}

还得来一段理论,再听我扯一会。Buffer共有类型有以下几种

• ByteBuffer

• CharBuffer

• DoubleBuffer

• FloatBuffer

• IntBuffer

• LongBuffer

• ShortBuffer

• MappedByteBuffer(这哥们儿有点特殊,以后单独再讲)

Buffer在nio中的主要作用就是与channel交互。但这几种类型中能与channel交互

的只有ByteBuffer(坑爹的吧!)所以在用其他类型Buffer的时候,一般都是先将

ByteBuffer转化为想用的类型,用的是Buffer()、

uffer()等方法进行转换。这种方式用术语来讲,叫做“产生其他种类

Buffer的视图”;意思就是底层是ByteBuffer,但看起来是其它种类的Buffer,可以用相

应的方法,但是视图发生了读写,底层的ByteBuffer也会发生变化。

下面这段是将ByteBuffer转化为CharBuffer视图的例子

ByteBuffer bb = te(1024);

//将ByteBuffer转化为CharBuffer视图后,再调用put,ByteBuffer中的position

指针不会移动

Buffer().put("Hello World");

//为了能正确的输出,这里改变了limit指针的位置,使之变到了字符数组的末尾

("Hello World".length()*);//字符数组长度*每个字符占的

字节数

while(aining()) {

(r());

}

/*也可用如下的方法输出

* while((c=r())!=0) {

* (c);

* }

*/

这段例子告诉我们,视图发生了读写,底层的ByteBuffer是有感知的,以及感知如何

展现出来。但真正用的时候,没这么蛮烦。因为学了后面的channel,就知道了,channel

直接就把整个ByteBuffer都拿走了,就不用这样一个个的输出了。而且一个个输出的话也

可以直接利用视图层,向下看

ByteBuffer bb = te(1024);

IntBuffer ib = uffer();

(new int[]{1,42,12,-12});

/*将ByteBuffer转化为IntBuffer视图后,再调用put,ByteBuffer中的position指

针不会移动

* 但是所生成的IntBuffer中的position会按正常方式移动

* 而且整个IntBuffer的capacity会按照byte 和 int 之间的所占字节大小比例而改

变*/

n("on = "+on());

n(" = "+());

n("ty = "+ty());

n("on = "+on());

n(" = "+());

n("ty = "+ty());

();

while(aining()) {

n(());

}

会了这些Buffer的知识就差不多了,就到这里了。多打打例子代码、多体会体会,就

可以洗洗睡了,拜拜

还是那句话,有问题及时告诉我

Java NIO教程 Channel

Channel是一个连接到数据源的通道。程序不能直接用Channel中的数据,必须让

Channel与BtyeBuffer交互数据,才能使用Buffer中的数据。

我们用FileChannel作为引子,开始逐步的了解NIO中的重要一环——Channel

FileChannel

有了前面的知识积累,我可以更快速的学习。FileChannel中常用的操作无非那么几

种,打开FileChannel、用BtyeBuffer从FileChannel中读数据、用BtyeBuffer向

FileChannel中写数据,下面这段代码就展示了这些

/*

* l是需要关闭的,所以这里用TWR方式确保Channel正确关闭

* 2.鼓励大家用这种方法打开通道(Path path,

options)

*/

try (FileChannel inChannel

= (("src/"),);

FileChannel outChannel

= (("src/"),);) {

ByteBuffer buf = te(48);

/*

* ()和read()方法是需要移动position和limit指针的

* 所以需要用()等方法,来保证读写正确

* ()方法是从通道读取到缓冲区中,读取的字节数量是n (n是buffer

中当前剩余的容量),

* 但是读取的数量是取决于通道的当前状态。例如:要读到文件末尾,不够

buffer的容量也就是 通道剩余<=n,

*

说加循环,

或者说ServerSocketChannel 当前只能读取准备好的,这很可能

* 另外read的方法返回当前读取的数量,一个int 可以根据他来设定while

* 如果返回-1,表示到了文件末尾

*/

int bytesRead = (buf);

while (bytesRead != -1) {

();

/*

*注意()是在while循环中调用的。

*因为无法保证write()方法一次能向FileChannel写入多少字节,

*因此需要重复调用write()方法,直到Buffer中已经没有尚未写入通道的字节。

*/

while (aining()) {

(buf);

}

();

bytesRead = (buf);

}

}

其实掌握了Buffer的知识后,学起FileChannel来挺容易的。而且再告诉你一点,就

是如果只是将一个数据源通过FileChannel,转移到另一个数据源,还有一种更加简单的

方法

try (FileChannel inChannel

= (("src/"),);

FileChannel outChannel

= (("src/"),);) {

//第二个参数表示,数据转移的起始位置,第三个参数表示转移的长度

//()表示通道的长度

erFrom(inChannel,0,());

//以下方式也可

erTo(0, (), outChannel);

}

这些以外,还有几个常用的方法,在这里要跟大家说一下

on() 返回FileChannel读写的当前位置

on(long newPosition) 设置FileChannel读写的当前位置

te(long size) 截取文件的前size个字节

(boolean metaData) 将通道里尚未写入磁盘的数据强制写到磁盘

上。出于性能方面的考虑,操作系统会将数据缓存在内存中,所以无法保证写入到

FileChannel里的数据一定会即时写到磁盘上。要保证这一点,需要调用force()方法。其

中的boolean类型的参数,指明是否同时将文件元数据(权限信息等)写到磁盘上。

下面理论上要介绍

SocketChannel ServerSocketChannel

等通道了,但是这些网络

通道和fileChannel不太一样,因为fileChannel竟然是阻塞的(NIO,你在跟我开玩笑吧!),

真的是阻塞的。而

SocketChannel ServerSocketChannel

等通道才是非阻塞的

(fileChannel是充话费送的吧!),所以它们的真正能力要配合Selector才能显示出来,

所以等到讲解Selector时,在一起讲。

那这样就结束了吗?当然不可能(这种看右边的滚动条就能发现的事实,就不要故弄

玄虚了吧! 咦,我怎么在自己吐槽自己?)下面开始讲讲java7中引进的

AsynchronousFileChannel,这回放心,它是异步的。

异步I/O (AIO)

其实利用java7之前的方式,也能做到,但是必须写大量的多线程代码,而且多路读

取也十分麻烦。除非程序写的十分强大,否则,自己写的异步I/O的速度只能是 慢

~~~~~~~~~

在java7的异步I/O中,主要有两种形式,

将来式

回调式

。这是在

rent中的并发工具,不会的话也没关系,在这里应该能大致的看懂。

将来式

这种方式是由主线程发起I/O操作并轮询等待结果。这里用了

接口,它的能力是不让当前线程阻塞。通过将I/O操作转移到

另一线程上,并在完成时返回结果,来达到异步的目的。

try (AsynchronousFileChannel inChannel = (

("src/"), );) {

ByteBuffer buffer = te(1024);

//read的第二个参数指定了channel的起始位置

Future result = (buffer, 0);

//一直轮询I/O操作是否完成

while (!()) {

// 做点别的

}

();

while (aining()) {

((char) ());

}

}

回调式

这种方式是预先制定好I/O成功或失败时的应对策略,等待I/O操作完成后就自动执

行该策略。所以必须得重写两个方法ted()和

().

try (AsynchronousFileChannel inChannel = (

("src/"), );) {

ByteBuffer buffer = te(1024);

/*

* (ByteBuffer dst,long position,

* A attachment, CompletionHandler handler)

* 该函数是回调式中的核心函数

* 1.首先讲最后一个参数,它的第二个泛型类型和第三个参数类型一致为A

* 该接口有两个待实现的方法,completed(...)和failed(...) 分别代指完成时和失败时

如何操作

* completed(Integer result, A attachment)的第一个参数是完成了多少个字节

* failed(Throwable exc, A attachment)的第一个参数是引起失败的异常类型

* 2.A 可以理解为在CompletionHandler的实现外部,要给实现内部什么信息

* 在下面的代码中,我传的A为buffer,以便实现的内部打印buffer信息,也可以传

递String类型等

* 3.前两个参数分别为与通道交互的byteBuffer和起始位置

*/

(buffer, 0, buffer,

new CompletionHandler() {

public void completed(Integer result,

ByteBuffer attachment) {

n(result);

();

while (aining()) {

((char) ());

}

}

public void failed(Throwable exception,

ByteBuffer attachment) {

n("failed"

+ sage());

}

});

// 做点别的

}

纵观这两种异步I/O实现方式,我自己总感觉,将来式总是询问数据是否到位,有股

非阻塞I/O的感觉。网络异步I/O也是运用将来式和回调式完成的,和文件I/O基本一致,

就不再磨叽。

但java7的I/O新内容绝不止这些,还有对网络多播的支持,还有通道组等等。想学

完?路还有很远、很远呢。

讲的就是这么多,如有问题联系我

Java NIO教程 Selector

这次我们开讲非阻塞I/O中的Selector,它需要配合非阻塞的TCP和UDP来使用。

首先我们先简单讲一下TCP和UDP的非阻塞通道。

非阻塞I/O通道

在上代码前我们先讲解一些最基本的知识。TCP和UDP共对应着三种通道,分别是:

SocketChannel、ServerSocketChannel、DatagramChannel 。它们都可以通过

()方法来初始化;同时对于SocketChannel来说,当一个新连接到达

ServerSocketChannel时,也会被创建(在代码中会有说明)。而且它们使用结束后都需

要被关闭。

首先让我们来看看SocketChannel的基本操作

//通过open()打开SocketChannel

SocketChannel socketChannel = ();

//绑定主机端口

t(new InetSocketAddress("127.0.0.1", 18888));

//设置成非阻塞模式

ureBlocking(false);

while(! Connect() ){

//做点其他事

}

// 利用SocketChannel进行数据操作

下面再来说说,如何用SocketChannel进行数据操作。它的数据读写和其他通道的读

写方式是完全一致的,只是要注意的是,在非阻塞模式下,read()和write()没有进行任何操

作就返回了,所以要在循环中调用,并注意返回值。

ByteBuffer buf = te(48);

while((buf)!=-1) {

();

while(aining()) {

(buf);

}

();

}

SocketChannel相当于传统I/O中的Socket,而ServerSocketChannel相当于

ServerSocket;而且整体形式都是一致的,都是利用多路复用思想,在服务器端收到连接

后,产生一个专门的Socket,与客户端进行数据传输。具体形式就是

"()"

在收到连接后,会返回一个SocketChannel,具体形式

见代码

ServerSocketChannel serverSocketChannel = ();

//绑定主机端口

().bind(new InetSocketAddress(9999));

ureBlocking(false);

while (true) {

//accept()在非阻塞模式中,若建立连接,则返回SocketChannel;否则返回null

SocketChannel socketChannel = ();

if (socketChannel != null) {

// 利用SocketChannel进行数据操作

}

}

而DatagramChannel则是跟DatagramPacket十分相似的,只不过数据包由当初的

byte数组换成了现在的ByteBuffer

DatagramChannel channel = ();

//绑定主机端口

().bind(new InetSocketAddress(9999));

ureBlocking(false);

ByteBuffer buf = te(48);

/*

* 1.因为UDP是无连接的网络协议,所以不能像TCP那样读取和写入,它是发送和接

收数据包。

* e()在非阻塞模式中,若没有收到数据包,则返回null;

* 若收到了,则将内容写入byteBuffer,将发送方的SocketAddress返回(其

中包含IP和端口)

* 3.如果Buffer容不下收到的数据,多出的数据将被丢弃

*/

while(e(buf)==null){

//做点其他事

}

();

//指定接收方的SocketAddress

(buf, new InetSocketAddress("127.0.0.1", 8888));

DatagramChannel还有一个特殊的地方,就是它可以“连接”到网络中的特定地址

的,十分类似于一个TCP连接。但由于UDP是无连接的,连接到特定地址并不会像TCP

通道那样创建一个真正的连接。而是锁住DatagramChannel ,让其只能从特定地址收发

数据。想实现这种功能,编写方式和TCP十分类似,就不写了,去看文档吧,讲解的十分

清楚。

Selector

现在开始进入我们今天的主题Selector

其实前言中已经简单的讲解过什么是Selector以及为什么要使用Selector了。这里

就不再重复了(我猜你已经忘了,回去再看一眼吧),咱们还是从最基础的创建开讲。

Selector的创建是通过调用()方法完成的(这部分都是用open()创建

的)

Selector注册

说完创建,就得说说如何让Channel和Selector配合使用了?一句话:“将channel

注册到selector上”这个动作是通过SelectionKey er(Selector sel,int

ops,Object att)方法完成的。

这里要强调一点,就是调用register的channel必须是非阻塞的。这就将FileChannel

排除在外(充话费送的就是不行)。

现在讲解register()中每一个参数的含义。第一个参数,就是要将channel注册到哪

个Selector。第二个参数,它是一个“interest集合”,意思是在通过Selector监听

Channel时对什么事件感兴趣,可以监听四种不同类型的事件,分别是Connect、Accept、

Read和Write;它们四个分别代表的含义是:

• Connect(_CONNECT):一个channel成功连接到另一个服务器

——“连接就绪”

• Accept(_ACCEPT):一个ServerSocketchannel准备好接收新进入

的连接——“接收就绪”

• Read(_READ):一个通道的可读数据已准备好——“读就绪”

• Write(_WRITE):一个通道的可写数据已准备好——“写就绪”

P.S:圆括号中的是要填在第二个参数ops位置上的int常量。我们把这四种叫做“感

兴趣事件”,后文会多次提到这个概念

如果你对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来,如下:

int ops = _READ | _WRITE;

register()方法的第三个参数为附加对象,它可有可无,是一个Object对象,它可以

作为每个通道的标识符,用以区别注册在同一个Selector上的其他通道;也可以附加其他

对象。

最后再来看看register()方法的返回值。返回值为SelectionKey对象,这是一个重要

的对象,接下来我们就主要讲解SelectionKey。

SelectionKey

当Selector发现某些channel中的感兴趣事件发生了,就会返回相对应channel的

SelectionKey对象。

SelectionKey对象包含着许多信息。比如所属通道的channel对象,通过

l()方法就可以得到;还有通道的附加对象,通过

ment()方法就可以得到;还可以得到通道那个感兴趣时间发生了通

过下面四种方法获得:

• boolean ptable()

• boolean ectable()

• boolean able()

• boolean able()

还可以获得更多信息,具体内容可以去看文档。

()

之前的创建、注册等准备都完成之后,就可以坐等准备好的数据到来了。这时候需要

知道有多少个通道感兴趣事件已经准备好了。这时候有下面三个方法帮你完成这项任务,

分别是

• int ()

• int (long timeout)

• int Now()

首先讲一下这三个方法准确的作用,它们都是返回有多少个通道已经变成就绪状态。

它们的区别是:

• select()是阻塞的,它会一直等到有通道准备就绪、

• select(long timeout)也是阻塞的,它会一直等到有通道准备就绪或者已经超出给定

的timeout时间并返回0。

• selectNow()是非阻塞的,如果没有通道就绪就直接返回0。

edKeys()

通过select()方法知道有若干个通道准备就绪,就可以调用下面的方法来返回相应若干

个通道的selectedKey了

Set selectedKeys = edKeys()

获得

selectedKeys

后,你就可以进行相应的处理了。需要强调的是,每次处理完一个

selectionKey之后需要将它在Set中删除,这样下次它准备好以后就可以再次添加到Set

中来。

现在关于Selector的知识基本上就讲解完了,让我们在一个

服务器端、客户端收发字

符串

的例子中结束本次的讲解吧。

客户端

public class HansClient {

// 定义检测SocketChannel的Selector对象

private Selector selector = null;

// 客户端SocketChannel

private SocketChannel sc = null;

public void init() throws IOException {

selector = ();

InetSocketAddress isa = new InetSocketAddress("127.0.0.1", 30000);

// 调用open静态方法创建连接到指定主机的SocketChannel

sc = (isa);

// 设置该sc以非阻塞方式工作

ureBlocking(false);

// 将SocketChannel对象注册到指定Selector

er(selector, _READ);

// 启动读取服务器端数据的线程

new ClientThread().start();

// 创建键盘输入流

Scanner scan = new Scanner();

while (tLine()) {

// 读取键盘输入

String line = ne();

// 将键盘输入的内容输出到SocketChannel中

(_(line));

}

}

// 定义读取服务器数据的线程

private class ClientThread extends Thread {

public void run() {

try {

while (() > 0) {

// 遍历每个有可用IO操作Channel对应的SelectionKey

for (SelectionKey sk : edKeys()) {

// 删除正在处理的SelectionKey

edKeys().remove(sk);

// 如果该SelectionKey对应的Channel中有可读的数据

if (able()) {

// 使用NIO读取Channel中的数据

SocketChannel sc = (SocketChannel) l();

ByteBuffer buff = te(1024);

String content = "";

while ((buff) > 0) {

(buff);

();

content += _(buff);

}

// 打印输出读取的内容

n("聊天信息:" + content);

}

}

}

} catch (IOException ex) {

tackTrace();

}

}

}

public static void main(String[] args) throws IOException {

new HansClient().init();

}

}

服务器端

public class HansServer {

// 用于检测所有Channel状态的Selector

private Selector selector = null;

public void init() throws IOException {

selector = ();

// 通过open方法来打开一个未绑定的ServerSocketChannel实例

ServerSocketChannel server = ();

InetSocketAddress isa = new InetSocketAddress("127.0.0.1", 30000);

// 将该ServerSocketChannel绑定到指定IP地址

().bind(isa);

// 设置ServerSocket以非阻塞方式工作

ureBlocking(false);

// 将server注册到指定Selector对象

er(selector, _ACCEPT);

while (() > 0) {

// 依次处理selector上的每个已选择的SelectionKey

for (SelectionKey sk : edKeys()) {

// 从selector上的已选择Key集中删除正在处理的SelectionKey

edKeys().remove(sk);

// 如果sk对应的通道包含客户端的连接请求

if (ptable()) {

// 调用accept方法接受连接,产生服务器端对应的SocketChannel

SocketChannel sc = ();

// 设置采用非阻塞模式

ureBlocking(false);

// 将该SocketChannel也注册到selector

er(selector, _READ);

}

// 如果sk对应的通道有数据需要读取

if (able()) {

// 获取该SelectionKey对应的Channel,该Channel中有可读的数据

SocketChannel sc = (SocketChannel) l();

// 定义准备执行读取数据的ByteBuffer

ByteBuffer buff = te(1024);

String content = "";

// 开始读取数据

try {

while ((buff) > 0) {

();

content += _(buff);

}

// 打印从该sk对应的Channel里读取到的数据

n("=====" + content);

}

// 如果捕捉到该sk对应的Channel出现了异常,即表明该Channel

// 对应的Client出现了问题,所以从Selector中取消sk的注册

catch (IOException ex) {

// 从Selector中删除指定的SelectionKey

();

if (l() != null) {

l().close();

}

}

// 如果content的长度大于0,即聊天信息不为空

if (() > 0) {

// 遍历该selector里注册的所有SelectKey

for (SelectionKey key : ()) {

// 获取该key对应的Channel

Channel targetChannel = l();

// 如果该channel是SocketChannel对象

if (targetChannel instanceof SocketChannel) {

// 将读到的内容写入该Channel中

SocketChannel

dest = (SocketChannel) targetChannel;

(_(content));

}

}

}

}

}

}

}

public static void main(String[] args) throws IOException {

new HansServer().init();

}

}

本次讲解就到这里了,本系列的讲解也就到这里了。如果你能看到这里我真的很开心。

有任何事都可以与我讨论。

Java NIO教程 MappedByteBuffer

之前跟大家说过,要讲MappedByteBuffer,现在我来履行承诺了。

首先从大体上讲一下MappedByteBuffer究竟是什么。从继承结构上来讲,

MappedByteBuffer继承自ByteBuffer,所以ByteBuffer有的能力它全有;像变动

position和limit指针啦、包装一个其他种类Buffer的视图啦,都可以。

“MappedByteBuffer”为何而来?吾辈心中亦有惑(熊猫人之谜的梗)用一个字来概括

就是快

为什么快?因为它使用

direct buffer

的方式读写文件内容,这种方式的学名叫做内存

映射。这种方式直接调用系统底层的缓存,没有JVM和系统之间的复制操作,所以效率大

大的提高了。而且由于它这么快,还可以用它来在进程(或线程)间传递消息,基本上能

达到和“共享内存页”相同的作用,只不过它是依托实体文件来运行的。

而且它还有另一种能力。就是它可以让我们读写那些因为太大而不能放进内存中的文

件。有了它,我们就可以假定整个文件都放在内存中(实际上,大文件放在内存和虚拟内

存中),基本上都可以将它当作一个特别大的数组来访问,这样极大的简化了对于大文件的

修改等操作。

下面我们开始介绍它的用法了

FileChannel提供了map方法来把文件映射为MappedByteBuffer:

MappedByteBuffer map(int mode,long position,long size); 可以把文件的从position

开始的size大小的区域映射为MappedByteBuffer,mode指出了可访问该内存映像文件

的方式,共有三种,分别为:

_ONLY(只读): 试图修改得到的缓冲区将导致抛出

ReadOnlyBufferException。

_WRITE(读/写): 对得到的缓冲区的更改最终将写入文件;但该

更改对映射到同一文件的其他程序不一定是可见的(无处不在的“一致性问题”又出现了)。

E(专用): 可读可写,但是修改的内容不会写入文件,只是buffer

自身的改变,这种能力称之为”copy on write”

再简单的说一下,MappedByteBuffer较之ByteBuffer新增的三个方法

• fore()缓冲区是READ_WRITE模式下,此方法对缓冲区内容的修改强行写入文件

• load()将缓冲区的内容载入内存,并返回该缓冲区的引用

• isLoaded()如果缓冲区的内容在物理内存中,则返回真,否则返回假

下面代码终于出场了

int length = 0x8FFFFFF;//一个byte占1B,所以共向文件中存128M的数据

try (FileChannel channel = (("src/"),

, );) {

MappedByteBuffer mapBuffer =

(_WRITE, 0, length);

for(int i=0;i

((byte)0);

}

for(int i = length/2;i

//像数组一样访问

n((i));

}

}

上面是MappedByteBuffer最基本的应用,而下面这段代码主要是测试它到底有多快,

import putStream;

import tputStream;

import putStream;

import tputStream;

import ption;

import ByteBuffer;

import annel;

import ;

import rdOpenOption;

public class TestMappedByteBuffer {

private static int length = 0x2FFFFFFF;//1G

private abstract static class Tester {

private String name;

public Tester(String name) {

= name;

}

public void runTest() {

(name + ": ");

long start = tTimeMillis();

test();

n(tTimeMillis()-start+" ms");

}

public abstract void test();

}

private static Tester[] testers = {

new Tester("Stream RW") {

public void test() {

try (FileInputStream fis = new FileInputStream(

"src/");

DataInputStream dis = new DataInputStream(fis);

FileOutputStream fos = new FileOutputStream(

"src/");

DataOutputStream dos = new DataOutputStream(fos);) {

byte b = (byte)0;

for(int i=0;i

yte(b);

();

}

while (()!= -1) {

}

} catch (IOException e) {

tackTrace();

}

}

},

new Tester("Mapped RW") {

public void test() {

try (FileChannel channel = (("src/"),

, );) {

MappedByteBuffer mapBuffer

(_WRITE, 0, length);

for(int i=0;i

((byte)0);

=

}

();

while(aining()) {

();

}

} catch (IOException e) {

tackTrace();

}

}

},

new Tester("Mapped PRIVATE") {

public void test() {

try (FileChannel channel = (("src/"),

, );) {

MappedByteBuffer mapBuffer = (E,

0, length);

for(int i=0;i

((byte)0);

}

();

while(aining()) {

();

}

} catch (IOException e) {

tackTrace();

}

}

}

};

public static void main(String[] args) {

for(Tester tester:testers) {

t();

}

}

}

先从整体上提一句上面的代码,runTest()是一个模板方法,并且引用了一个未实现的

test()方法;通过匿名内部类的实现,填充了测试内容。

再来说上面代码的测试结果。用传统流的方式,当然是最慢的,但应该是由于用的数

据量是1G,无法全部读入内存,所以它根本无法完成测试。剩下两种

_WRITE和E各有特点,首先说

_WRITE,它的速度每次差别较大,在0.6s和8s之间波动,而且很不稳

定。但E就稳得出奇,一直是1.1s到1.2s之间。但无论是哪个速度都

是十分惊人的。但是MappedByteBuffer也有不足,就是在数据量很小的时候,表现比较

糟糕,那是因为

direct buffer

的初始化时间较长,所以建议大家只有在数据量较大的时候,

在用MappedByteBuffer。

还要强调的一点是,MappedByteBuffer存在内存占用和文件关闭等不确定问题。被

MappedByteBuffer打开的文件只有在垃圾收集时才会被关闭,而这个点是不确定的。

javadoc里是这么说的:

A mapped byte buffer and the file mapping that it represents remain valid

until the buffer itself is garbage-collected. ——JavaDoc

关于MappedByteBuffer就告诉你这么多了,有什么问题尽管提、有什么想法随时找

我交流。

本文标签: 文件数据方法通道方式