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
"*.java")) {
for (Path entry : stream) {
n(eName());
}
}
再强调一下
ectoryStream
的第二个参数。这种形式是glob表达式,如
*.java代表的就是所有以".java"结尾的字符串
下面再说一下,实现FileVisitor接口的方式。这里得用到Files的一个静态方法
leTree(Path start,FileVisitor super Path> 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
(Path path,Iterable extends CharSequence>
options)
是不是一目了然。对这就是文件的按行读和按行写,用起来真的特别方便。在java1.7
之前,我在guava中看见过这些方法,有时候用guava的io其实根本不是为了它的source
和sink,而就是为了这几个方法。这两个方法的出现,反映了java的一种新态度,那就是
纳谏、虚心。优秀的命名、方便的工具类,这都是以前java官方中所欠缺的,无论是新的
Time包还是lamda表达式或是Coin项目,都是向社区学习、吸收优秀的社区资源,都
是将开发者的梦想变为现实、方便开发者。
不好意思扯远了。那就直接上代码了
Path source = ("c:");
Path target = ("c:");
//以防乱码最好设置一下文件的编码格式,在StandardCharsets中有很多种内置编
码格式
List
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 //一直轮询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 * 该函数是回调式中的核心函数 * 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 后,你就可以进行相应的处理了。需要强调的是,每次处理完一个 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就告诉你这么多了,有什么问题尽管提、有什么想法随时找 我交流。
版权声明:本文标题:Java NIO2教程 内容由热心网友自发贡献,该文观点仅代表作者本人,
转载请联系作者并注明出处:https://www.elefans.com/dianzi/1721901304a903976.html,
本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论