文章目录
博文所在专栏里有更多相关内容,如IO模型概述、Reactor反应器模式等,欢迎阅读与交流。
文字来源于读书笔记及个人心得,可能有引用其他博文,若引用了你的文字请联系我,我会加上来源,或者删除相关内容。
二 Java NIO
在1.4之前,Java IO类库是阻塞IO,从1.4开始,才引入了新的异步IO类库,即Java New IO,简称Java NIO。相对的,1.4之前称为Old IO,OIO。
(一)NIO对比OIO
NIO
OIO
面向缓冲区:只要从channel读取数据到buffer(读取),就可以读取buffer的任意位置。(写入:从buffer写到channel)
面向流:面向字节流或字符流,按顺序读取字节
非阻塞:内核缓冲区准备数据时,用户read操作都不会阻塞用户线程
阻塞:整个IO过程都会阻塞用户线程
有选择器概念:基于底层操作系统的选择器的系统调用,系统开销更小,因为不必为每个网络连接创建线程
无选择器概念
(二)概述三个核心组件
Channel通道
在Java NIO中,同一个网络连接(文件描述符)用一个通道表示,所有Java NIO的IO操作都从通道开始,即可从通道读取,也可向通道写入。
对比OIO,同个网络连接需要关联输入流和输出流共两个流。
Selector选择器
用于NIO通道的注册,以及查询就绪状态的IO事件。一个选择器可监控/管理多个通道,比起OIO系统开销更小,因为不必为每个网络连接创建线程。
Buffer缓冲区
本质是一个内存块(数组),用于应用程序和通道的交互,如通道的读取:将数据从通道读取到缓冲区;通道的写入:将数据从缓冲区写入到通道。
(三)Buffer详解
1 Buffer类
Buffer类是个抽象类,有8个子类:ByteBuffer、ShortBuffer、CharBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer、MappedByteBuffer,前7种覆盖了所有能在IO传输的Java基本数据类型,MappedByteBuffer专门用于内存映射。
2 四个属性
(1)capacity:容量,buffer内部byte[]数据内存块的容量,该容量不是该数组可写入的字节容量,而是可写入的对象数量,如DoubleBuffer写入的对象时double类型,capacity为10时表示可写入10个double数据。
(2)position:位置,缓冲区中下一个要被读/写的元素的索引。不同模式的position最大值不同:
写模式(通道-缓冲区):初始值0,最大可写值为limit-1,position=limit表示缓冲区已无位置可写;
读模式:初始值0,最大可读值为limit-1,position=limit表示缓冲区已无可读数(我看的书说的是,“读模式下最大可读值是limit“,但position=limit时已经没有数据可读了,所以我认为正确的说法是,读模式limit最大可取值是limit,但最大可读值是limit-1)
(3)limit:上限,缓冲区中当前的数据量
(4)mark:用于临时存储当前position
3 重要方法
(1)allocate()创建缓冲区:
IntBuffer ib=IntBuffer.allocate(5);
System.out.println("position:"+ib.position());
System.out.println("limit:"+ib.limit());
System.out.println("capacity:"+ib.capacity());
System.out.println("------");
缓冲区初创建时为写入模式。
(2)put()写入到缓冲区:
for(int i=0;i<5;i++){
ib.put(i);
}
(3)flip()切换写模式为读模式:
设置limit为当前position、重置position为0、清除之前的mark标记
ib.flip();
(4)get()从缓冲区获取:
System.out.println(ib.get());
如果position=limit,即缓冲区数据读完了,此时若想进行写入,需调用clear()或compact()
(5)rewind()倒带重读:
重置position为0、清除之前的mark标记——与flip只差了对limit的处理,rewind不修改limit
ib.rewind();
System.out.println("-----after rewind-----");
System.out.println("position:"+ib.position());
System.out.println("limit:"+ib.limit());
System.out.println("capacity:"+ib.capacity());
(6)mark()和reset()
mark()表示暂存当前position值至mark属性,reset()将mark值恢复到position。
for(int i=0;ib.position()<ib.limit();i++){
if(i==2) {
ib.mark();
System.out.println("mark");
}
if(i==4) {
ib.reset();
System.out.println("reset");
}
System.out.println(ib.get());
System.out.println("position:"+ib.position());
System.out.println("limit:"+ib.limit());
System.out.println("capacity:"+ib.capacity());
System.out.println("------");
}
(7)clear()清空缓冲区/compact()压缩缓冲区
在缓冲区处于读模式时调用clear或compact,缓冲区会切换为写模式,不同在于clear会重置position为0,而compact会将所有未读元素复制到缓冲区起始处,并将position设置为未读元素的后一位,如有一个维度元素,则compact后position为1。
(四)Channel详解
Java NIO中一个连接就是一个channel,也就是一个文件描述符。而对于不同的网络传输协议类型在java中有不同的NIO channel实现。这里着重聚焦于以下四种:
- FileChannel文件通道:用于文件的数据读写
- SocketChannel套接字通道:用于Socket套接字TCP连接
- ServerSocketChannel服务器嵌套字通道(或服务器监听通道):允许我们监听TCP请求并为监听到的请求创建一个SocketChannel套接字通道
- DatagramChannel数据报通道:用于UDP协议的数据读写
1 FileChannel文件通道
用于从文件读取数据或者向文件写入数据。只有阻塞模式,无法设置为非阻塞
(1)获取FileChannel通道:通过文件流获取,或通过文件随机访问类获取。文件流、文件随机访问类、通道,都需要手动关闭
RandomAccessFile infile=new RandomAccessFile("C:\Desktop\消息累加器.png","rw");
FileChannel inChannel=infile.getChannel();
RandomAccessFile outfile=new RandomAccessFile("C:\Desktop\消息累加器-copy.png","rw");
FileChannel outChannel=outfile.getChannel();
(2)read(buf)读取通道(对通道来说是读取,对缓冲区来说是写入)
ByteBuffer buffer=ByteBuffer.allocate(1024);
int i=-1;
while((i=inChannel.read(buffer))!=-1){
//......
}
(3)write(buf)写入通道(对缓冲区来说是读取,需要切换为读模式)
while((i=inChannel.read(buffer))!=-1){
//第一次切换:翻转buf,切换为读模式
buffer.flip();
int outLength=0;
//将buf写入到输出通道
while((outLength= outChannel.write(buffer))!=0);
//第二次切换:清除buf,切换为写模式
buffer.clear();
}
(4)force()强制刷新到磁盘
在调用该方法之前,出于性能原因,操作系统可能会将数据缓存在内存中,而该方法能将所有未写入的数据从通道刷新到磁盘中。
outChannel.force(true);
(5)close关闭通道
channel.close();
(6)使用FileChannel完成文件复制的实例
public void FileChannelTest(){
FileInputStream fis=null;
FileChannel inChannel=null;
FileOutputStream fos=null;
FileChannel outChannel=null;
try {
fis=new FileInputStream("C:\Users\Simon\Desktop\消息累加器.png");
inChannel=fis.getChannel();
fos=new FileOutputStream("C:\Users\Simon\Desktop\消息累加器-copy.png");
outChannel=fos.getChannel();
ByteBuffer buffer=ByteBuffer.allocate(1024);
int i=-1;
while((i=inChannel.read(buffer))!=-1){
//第一次切换:翻转buf,切换为读模式
buffer.flip();
int outLength=0;
//将buf写入到输出通道
while((outLength= outChannel.write(buffer))!=0);
//第二次切换:清除buf,切换为写模式
buffer.clear();
}
//强制刷新到磁盘
outChannel.force(true);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
IOUtils.close(outChannel);
IOUtils.close(fos);
IOUtils.close(inChannel);
IOUtils.close(fis);
}
}
2 SocketChannel套接字通道
Java NIO中涉及网络连接的两个通道:
- SocketChannel负责连接的数据传输,对应OIO的Socket类,应用于服务端和客户端
- ServerSocketChannel负责连接的监听,对应OIO的ServerSocket类,应用于服务端
这两种通道都支持阻塞和非阻塞两种模式,调用configureBlocking(false)设置为非阻塞模式,传参true设置为阻塞模式。
示例
//该示例为运行过
@Test
public void socketChannelTest(){
try {
//1.获得套接字传输通道
SocketChannel socketChannel=SocketChannel.open();
//设置为非阻塞模式
socketChannel.configureBlocking(false);
//连接目标服务端的ip和端口
socketChannel.connect(new InetSocketAddress("127.0.0.1",80));
//不停自旋,直到连接真正建立起来
while (!socketChannel.finishConnect()){
}
//2.从套接字通道读取数据(需通道可读才行,通过Selector选择器判断是否可读)
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
//read方法是异步的。返回读取到的字节数,若返回-1表示对方已输出结束
int bytesRead=socketChannel.read(byteBuffer);
//3.写入套接字通道
//写入通道需读取缓冲区,因此现将缓冲区置为读模式
byteBuffer.flip();
socketChannel.write(byteBuffer);
//4.关闭套接字通道
//如果当前套接字通道是用来写入数据给对方服务器的,建议调用以下方法终止输出,向对方发送一个输出结束标志(-1)
socketChannel.shutdownOutput();
//调用close()关闭连接
IOUtils.close(socketChannel);
} catch (IOException e) {
e.printStackTrace();
}
}
3 DatagramChannel数据报通道
用于处理UDP的数据传输,使用UDP时,只需要知道服务器的IP和端口就可以传输数据
//未运行过
public void datagramChannelTest(){
try {
//1.获取数据报通道
DatagramChannel channel=DatagramChannel.open();
//设置为非阻塞模式
channel.configureBlocking(false);
//如果要接收数据,需绑定一个数据报监听接口
channel.bind(new InetSocketAddress(18080));
//2.从数据报中读取数据,通过Selector判断通道是否可读
//创建缓冲区
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
//从数据报通道读入,并写入缓冲区,返回数据发送端的连接地址(包括IP和端口)
SocketAddress clientAddr=channel.receive(byteBuffer);
//3.写入数据报通道
byteBuffer.flip();
channel.send(byteBuffer,new InetSocketAddress("127.0.0.1",18899));
byteBuffer.clear();
//4.关闭数据报通道
IOUtils.close(channel);
} catch (IOException e) {
e.printStackTrace();
}
}
(五)Selector选择器
简单说,选择器的使命是完成IO多路复用。
一般一个单线程处理一个选择器,一个选择器可监控多个通道的IO状况。
只有继承了抽象类SelectableChannel才能被选择/监控,例如FileChannel就不行。
选择器使用流程:
1、获取选择器示例
Selector selector=Selector.open();
2.将通道注册到选择器
channel.register(selector,SelectionKey.OP_ACCEPT);
channel.register(selector,SelectionKey.OP_ACCEPT|SelectionKey.OP_WRITE)
通道对象调用register方法完成在选择器上的注册,其中参数一是选择器对象,参数二是监听的IO事件类型(状态)。
监控同个通道的多个事件,使用“按位或”运算符“|”。
注册到选择器的通道必须是非阻塞模式,否则会抛出异常
四种IO事件类型:
- SelectionKey.OP_ACCEPT:接收就绪,某个Channel成功连接到另一个服务器
- SelectionKey.OP_CONNECT:连接就绪,某个ServerSocketChannel准备好接收新进入的连接
- SelectionKey.OP_READ,读就绪,一个有数据可读的通道
- SelectionKey.OP_WRITE:写就绪,等待写数据的通道
SelectableChannel可选择通道
只有继承了SelectableChannel抽象类的通道才能被选择器监控/选择
validOps()
一个通道不一定支持全部四种IO事件,
如ServerSocketChannel只支持OP_ACCEPT,而SocketChannel不支持OP_ACCEPT,
可通过channel.validOps()获取该通道支持的IO事件集合
SelectionKey选择键
即被选择器选中的指定状态下的通道。
3.选出感兴趣的IO就绪事件(选择键集合)
Select()
选择器的Select()方法可选出已注册且已就绪的IO事件,存储到selectedKeys
中,并返回被选中的通道的数量(通道数,不是事件数,准确说是上次select到这次select之间发生了就绪IO事件的通道数)。该方法为阻塞调用,会阻塞直到有已注册且已就绪的事件。
Select(long timeout)
与Select()类似,但指定了最长阻塞时间(毫秒数)
SelectNow()
与Select()类似,但非阻塞,不管有无就绪IO事件都立即返回
selectedKeys()
返回已注册且已就绪的选择键集合。
每个Selector内部都有两个SelectionKey集合(Set),一个是keys
,存储该选择器的所有选择键,一个是selectedKeys
,存储该选择器已注册且已就绪的选择键,通过选择键可以获得通道、通道的事件类型、选出该通道的选择键实例等。
这两个SelectionKey集合都是线程不安全的,都不可以直接手动添加元素进去,selectedKeys
可以执行remove操作,keys
不行。
4.判断选择键IO就绪类型
key.isAcceptable()、key.isConnectable()、key.isReadable()、key.isWritable()
其他
selector的wakeup()
用于唤醒阻塞的select()方法。
其他线程如果因为调用了selector.select()或者selector.select(long)这两个方法而阻塞,调用了selector.wakeup()之后,就会立即返回结果,并且返回的值!=0;
如果当前Selector没有阻塞在select方法上,那么本次 wakeup调用会在下一次select阻塞的时候生效。
SelectionKey的interestOps(int)
修改指定选择键的监听IO事件类型
示例
public void selectorTest(){
try {
//创建一个监听通道
ServerSocketChannel channel=ServerSocketChannel.open();
channel.configureBlocking(false);
channel.bind(new InetSocketAddress("127.0.0.1",8091));
//1.获取选择器实例
Selector selector=Selector.open();
//2.将通道注册到选择器
//注册到选择器的通道必须是非阻塞模式,否则会抛出异常
//一个通道不一定支持全部四种IO事件,如ServerSocketChannel只支持OP_ACCEPT,而SocketChannel不支持OP_ACCEPT
//可通过channel.validOps()获取该通道支持的IO事件集合
//监控同个通道的多个事件,使用“按位或”运算符“|”,如register(selector,SelectionKey.OP_ACCEPT|SelectionKey.OP_WRITE)
channel.register(selector,SelectionKey.OP_ACCEPT);
//3.选出感兴趣的IO就绪事件(选择键集合)
while(selector.select()>0){
//选择器集合不可添加元素,否则会报错
Set<SelectionKey> selectionKeys=selector.selectedKeys();
Iterator<SelectionKey> keyIterator=selectionKeys.iterator();
while(keyIterator.hasNext()){
SelectionKey key=keyIterator.next();
//根据具体的IO事件类型,执行对应的业务操作
if(key.isAcceptable()){
//IO事件:ServerSocketChannel服务器监听通道有新连接
//获取这个新连接(通道)
SocketChannel socketChannel=channel.accept();
socketChannel.configureBlocking(false);
//若该通道为数据输入通道,可使用可读事件注册到选择器
socketChannel.register(selector,SelectionKey.OP_READ);
}else if(key.isConnectable()){
//IO事件:传输通道连接成功
}else if(key.isReadable()){
//IO事件:传输通道可读
//获取这个新连接(通道)
SocketChannel socketChannel= (SocketChannel) key.channel();
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
int length=0;
while((length=socketChannel.read(byteBuffer))>0){
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(),0,length));
byteBuffer.clear();
}
socketChannel.close();
}else if(key.isWritable()){
//IO事件:传输通道可写
}
//处理完成后,移除选择键,以免被下次循环重复处理
keyIterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}