马上高考成绩快要出来了, 又免不了到了报志愿的时候, 现在小孩和家长如果真的很清楚的话, 志愿也没有什么好选的, 统一都推荐计算机. 实在上不了, 就自学吧.注意猪哥我说的不是指报理科的男生, 而是不论男女, 不论文理.
就连今天刚看的半泽直树, 也是一个自学开发做搜索引擎的例子. 这次就来看看NIO的异步内容.
- NIO的N之处
- Buffers类简介
- Channel类简介
- NIO的基础操作逻辑
- 缓冲区的更多细节
NIO的N之处
来看看NIO的所有新组件, 也就是Buffer, Channel和Selector以及辅助类Charset.
这里直接就看官网文档, 最正规核心. Java NIO的API有如下:
Buffers
, 表示存储数据的容器, 这些类定义在java.nio包内部
Charsets
, 表示一些解码器和编码器, 用于辅助字节和Unicode之间的转换, 主要用于字符处理. 这些类定义在java.nio.charset包中
Channels
, 表示连接到可以对其进行I/O操作的对象, 其实也可以认为Channel就代表那些可以进行I/O的对象, 比如文件, UDP, Socket等. 这些类定义在java.nio.channels中.
Selectors
, 还有与之搭配的Selector keys
, 与Channel中的一类selectable channels 合并起来实现非阻塞IO. 其实就是I/O多路复用. selector的所有API与Channel结合紧密, 所以也位于java.nio.channels中.
NIO的部分有阻塞也有非阻塞的部分, 非阻塞的部分需要使用支持Selector的Channel类来实现, 普通的Buffer-Channel则对应阻塞IO.
除了上边几个nio的子包之外, 还有一个子包就是上一次看过的java.nio.file包了.
Buffers类简介
Buffers实际上就是字面意思的缓冲区, 一个Buffers会由一个Channel来写入, 或者向Buffers中写入, 在最终会被写入到一个Channel中.
在java.nio中定义了所有基本类型对应的Buffer类型:
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
此外还有针对一个内存映射文件的MappedByteBuffer
.
这些具体Buffer类都继承自java.nio.Buffer类, 并且实现了Comparable<自身类型>
接口.
要创建一个缓冲区对象, 可以使用缓冲区对象的allocate()方法, 其中可以传入一个大小, 表示对应数据类型的多少大的一个空间:
//分配42字节的缓冲区
ByteBuffer buf = ByteBuffer.allocate(42);
//分配999个short长度的缓冲区空间
ShortBuffer sb = ShortBuffer.allocate(999);
读写缓冲区的方法如下:
get()
, 读入对应的一个数据类型的值
put()
, 写入一个对应数据类型的值
get(xxx[] dst)
, 读出一批数据到指定的目标数组
get(xxx[] dst, int offset, int length)
, 上一个方法的重载, 可以指定偏移量和长度
put(xxx[] src)
, 将指定目标数组的数据写入缓冲区
put(xxx[] src, int offset, int length)
, 上一个的重载, 可以指定偏移量和长度
channel.read(buffer)
, 用channel来向buffer中读出数据, 对于程序来说其实就是读出Channel中的内容. 这个方法返回读出的长度, 如果是-1, 则表示没有数据被读入.
channel.write(buffer)
, 用channel来向buffer中读出数据, 对于程序来说其实就是写入Channel中的内容. 同样返回写入数据的长度.
这其中的bytebuffer有一些特殊的方法, 就是将其中的内容作为任意的基本类型读出, 了解即可. 大多数IO其实都使用ByteBuffer, 很少有专门读其他类型的Buffer.
除了读写之外, 缓冲区有一个flip()方法很重要, 涉及到内部的状态变量.每个缓冲区的内部有重要的状态变量, 就是记录缓冲区的的状态, 这些状态包括position=已经写了多少数据/下一个字节放到缓冲区的哪里.
limit = 还有多少数据需要取出或者还有多少数据需要放入, postion一定会小于等于limit
capacity = 缓冲区的总长度
可以将缓冲区想象成为一个被包装过的数组, 那么position就是指向下一个可读取位置的索引, 而limit至少是已存在数据的最后一个索引. 而capacity就是整个数组的长度.
这里的IBM网站讲解的很详细.
在缓冲区向外输出的时候, 需要先调用flip()方法, 读完成后, 需要调用clear()方法, 这些方法都是重设内部的状态变量.
由此可见, 缓冲区的操作其实倒有点像C或者C++的基本I/O函数, 感觉还是万变不离其宗.
Channel类简介
Channel的文档见此..由于一个Channel对应一个I/O对象, 所以可以将Channel认为就是一个I/O对象. Channel对象可以来自于文件, UDP和TCP等网络通信等.
要创建Channel对象, 需要从一个I/O流中创建Channel对象, 常见的Channel对象有这么几种:
DatagramChannel
, 一看名字就知道, 用于UDP通信
ServerSocketChannel
, 服务端的TCP通信
SocketChannel
, 客户端的TCP通信
FileChannel
, 用于文件
SelectableChannel
SelectionKey
Selector
这三个是Select家族, 需要搭配使用来实现I/O多路复用.
AsynchronousChannelGroup
AsynchronousFileChannel
AsynchronousServerSocketChannel
AsynchronousSocketChannel
四个异步家族, 多路复用时候每个线程的I/O依然是阻塞, 这个异步可以让内部的I/O也不阻塞, 有点像Future.
现在先不涉及网络编程, 看看本地的FileChannel使用.
点开FileChannel的类, 可以发现通过FileInputStream.getChannel(), FileOutputStream.getChannel(), RandomAccessFile.getChannel()
可以创建FileChannel.
下边就可以来尝试一下编写NIO的读写操作了.
NIO的基础操作逻辑
NIO的基础操作逻辑如下:
- 通过流创建Channel对象
- 从Channel对象中将数据读入缓冲区
- 使用flip()让缓冲区处于就绪状态
- 使用缓冲区的数据, 比如打开另外一个Channel, 进行写入.
- 如果没有读完和写完, 重复2-3步骤
- .clear()清除缓冲区, 准备好下一次读入
- 关闭所有Channel
这里再来试验一下, 用NIO如何进行操作.写一个简单的复制文件的程序:
//从源文件中创建channel
FileInputStream fileInputStream = new FileInputStream("D:\\downloads\\music\\王菲\\2000《寓言》内地引进版\\寓言 内地引进版.cue");
FileChannel channel = fileInputStream.getChannel();
//从目标文件创建channel
FileChannel newChannel = new FileOutputStream(new File("new.txt")).getChannel();
ByteBuffer buffer = ByteBuffer.allocate(100);
while (channel.read(buffer) != -1) {
//做好写入准备
buffer.flip();
newChannel.write(buffer);
//写入完成后清除文件
buffer.clear();
}
channel.close();
newChannel.close();
程序里就是按照上边的逻辑来进行操作, 每次也可以将缓冲区的内容转换成数组进行操作. 这其实就是C语言基础的IO模式.
缓冲区的更多细节
这里就简单记录一下, 需要用到的时候回来再看:
- slice() 方法根据现有的缓冲区创建子缓冲区, 与原来缓冲区共享数据
- asReadOnlyBuffer()将缓冲区转换为只读缓冲区, 其实返回的是一个与原来缓冲区共享的缓冲区, 但只读
- ByteBuffer可以设置为直接或者间接缓冲区, 如果是直接缓冲区, JDK尽量会使用操作系统原生的方法直接来操作缓冲区
- 内存映射文件也可以作为缓冲区的来源, 搭配Channel使用, 比如:
MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE, 0, 1024 );
此外还有分散和聚集, 就是用缓冲区数组来当成缓冲区, 对于把数据分段处理很有效果.
到目前为止, 使用的NIO都是同步的, 也就是说read()和write()方法都会阻塞.
可见, 对于处理字节之类的, 使用NIO要方便很多, 如果要处理字符的话, NIO其实还不怎么方便, 因为固定长度的缓冲区很可能没法读出像UTF-8这种编码的完整字符. 不过nio为此也提供了Path, Paths和Files来进行辅助, 可以简单的操作一些不长的文本文件.
下一次就来看看NIO真正的全新之处, 就是类似于I/O多路复用的Selector, 然后也来看看异步的I/O操作也就是Async开头的那些类.