目录
NIO中重要组件
1、channel:通道
1.1、理解
Java中NIO的channel类似于流操作,
但和流有不同:
-通道既可以从通道中读取数据,又可以写数据到通道;但流的读写通常是单向的
-通道中的数据需要先读到一个buffer,或者先写到buffer。
1.2、通道的使用
channel示例:
1.3、channel主要实现类
SocketChannel:通过TCP来读写网络中的数据,一般是客户端的实现
ServerSocketChannel:监听新进来的TCP连接,对每一个新连接创建一个SocketChannel,一般是服务端的实现。
DatagramChannel:通过UDP读写网络中的数据通道。
FileChannel:用于读取、写入等操作文件的通道。
FileChanle使用示例:
RandomAccessFile randomAccessFile = new RandomAccessFile("/Users/gongdezhe/Desktop/download/test", "rw");
//可以进行读写操作
FileChannel fileChannel = randomAccessFile.getChannel();
//写数据
byte[] bytes = "hello".getBytes();
//将数据交给buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(40);
byteBuffer.put(bytes); //将数据写入到buffer
byteBuffer.flip();
//写入通道
fileChannel.write(byteBuffer);
//关闭通道
fileChannel.close();
2、Buffer:缓存
2.1、理解
Java中NIO的Buffer用于和Channel交互
Java对NIO中的内存缓存包装成Buffer对象,提供了一系列方法,方便开始使用
2.2、基本使用
以读数据为例:
1、从channel将数据写入buffer
2、调用buffer.flip()进行读写切换
3、从buffer中读取数据
4、对 buffer清空clear()和compact()
2.3、Buffer 的底层实现
Buffer底层是通过特定类型(long,double...)的数组存储数据
Buffer底层是借助4个指针来操作的
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
capacity:buffer是有固定大小的,即capacity表示底层数组的大小
position:取决于读写模式。
写操作:将数据写入buffer,每写入数据position会移动一位,初始值为0,最大值为capacity
读操作:当读写模式切换后,position会被重置为0,读取数据时,position移动表示下一个读取的位置。
limit:写模式下,表示最多往buffer中写入的数据,最大写入的位置capacity,此时limit=capacity;
读模式下,limit表示最多能读取到的数据。
mark:
读写过程中指针变化
2.4、Buffer 类型:
Buffer是一个抽象类,其实现的子类有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer
buffer缓冲区的分配:Buffer分配空间分两种,一种是在堆上开辟的空间,一种是在堆外开辟的空间(零拷贝技术)
Buffer对象的创建,以ByteBuffer为例介绍:
ByteBuffer allocate(int capacity):在堆上创建指定大小的缓冲区
ByteBuffer wrap(byte[] array):通过byte数组创建一个缓冲区
ByteBuffer wrap(byte[] array,int offset, int length),截取bytebuffer部分内容创建缓冲区
ByteBuffer allocateDirect(int capacity) 在堆外内存创建一个指定大小的缓冲区
Buffer中的方法
2.5、Buffer读写操作
向buffer中写数据:
1、从channel写到buffer
channel.read(buffer);
2、通过buffer的put方法
buffer.put()
flip()读写模式切换:通过flip操作将limit置为position,然后将position置为0
从buffer中读数据:
有两种方式:
1、从buffer中读取到channel
channel.write(buffer)
2、通过buffer的get操作处理
buffer.get()
3、selector:复用器
3.1、理解
作用是检查一个或者多个NIO Channel的状态是否是可读、可写..,可以实现单线程管理多个channel,也可以管理多个网络请求
3.2、Selector的使用
3.2.1、创建selector的实例:Selector selector = Selector.open();
3.2.2、将通道注册到复用器上
//将socketChannel设置为非阻塞
socketChannel.configureBlocking(false);
//将socketChannel注册到Selector实例中,并关注可连接事件
socketChannel.register(selector,SelectionKey.OP_CONNECT);
注册的channel是非阻塞的,
SelectableChannel抽象类中提供了configureBlocking设置当前的通道是阻塞的还是非阻塞的
3.2.3、使用复用器来监听事件是否完成
//等待系统返回就绪事件
selector.select();
select方法是会阻塞的,直至内核监听到注册的感兴趣事件发生才返回。
3.2.4、遍历感兴趣事件集合
selector.selectedKeys().iterator()
通过selectedKeys返回的是SelectionKey的set集合,存放的是感兴趣事件的已准备就绪的
3.2.5、如果还有关注的事件,就跳转至第3步继续循环等待
3.2.6、最终关闭复用器selector
3.3、SelectableChannel抽象类介绍
SelectableChannel抽象类称之为可选择的通道SelectableChannel是具有阻塞的和非阻塞的两种模式。在非阻塞模式下,永远不会阻塞IO操作,将会使用selector作为异步支持,即read和write操作都不会阻塞,可以立即返回;新创建的SelectableChannel总是处于阻塞的,需要手动调换用configureBlocking设置为false,如果选用selector多路复用器必须使channel为非阻塞的,将channel设置为非阻塞之后,是不能改变,直至将SelectionKey注销掉。
SocketChannel、ServerSocketChannel、DatagramChannel都是SelectableChannel的子类,都是可以来设置为非阻塞的。
FileChannel是不能设置非阻塞的,其不是SelectableChannel的子类实现。
3.4、SelectionKey抽象类的介绍
3.4.1、理解
SelectionKey表示的是一个特定的通道对象和一个特定的复用器对象之间的注册关系
SelectionKey中对4个事件用4个常量表示
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4
3.4.2、SelectionKey中提供的方法:
public abstract SelectableChannel channel();
//返回该SelectionKey对应通道
public abstract Selector selector();
//返回该SelectionKey注册的选择器
public abstract boolean isValid();
//判断该SelectionKey是否有效
public abstract void cancel();
//撤销该SelectionKey
public abstract int interestOps();
//返回SelectionKey的关注操作符
//设置该SelectionKey的关注键,返回更改后新的SelectionKey
public abstract SelectionKey interestOps(int ops);
public abstract int readyOps();
//返回SelectionKey的预备操作符
//这里readyOps()方法返回的是该SelectionKey的预备操作符
//判断该SelectionKey的预备操作符是否是OP_READ
public final boolean isReadable() {
return (readyOps() & OP_READ) != 0;
}
//判断该SelectionKey的预备操作符是否是OP_WRITE
public final boolean isWritable() {
return (readyOps() & OP_WRITE) != 0;
}
//判断该SelectionKey的预备操作符是否是OP_CONNECT
public final boolean isConnectable() {
return (readyOps() & OP_CONNECT) != 0;
}
//判断该SelectionKey的预备操作符是否是OP_ACCEPT
public final boolean isAcceptable() {
return (readyOps() & OP_ACCEPT) != 0;
}
//设置SelectionKey的附件
public final Object attach(Object ob) {
return attachmentUpdater.getAndSet(this, ob);
}
//返回SelectionKey的附件
public final Object attachment() {
return attachment;
}
interestOps():对应的是该SelectionKey上注册的关注事件,通过register来完成感兴趣事件的集合
readyOps():通道上实际准备就绪的事件,
3.5、复用器的选择过程:
Selector抽象类中提供的方法:
Set<SelectionKey> keys():返回所有的注册的通道实例,一个selector上是可以注册多个通道,每个通道有多个关注的事件,即返回所有注册的通道SelectionKey
Set<SelectionKey> selectedKeys():返回的是所有准备就绪的通道实例
3.5.1、选择过程:
select获取准备就绪的事件,返回就是已经有就绪事件发生。
int select():阻塞到至少有一个通道上注册的事件就绪了。
int select(long timeout):和select()一样,会阻塞等待事件就绪,最长阻塞时间为timeout。
int selectNow():非阻塞,调用会立即返回。
返回结果为int类型表示有多少通道已经就绪,是自上次调用select后已经处于就绪的通道数量。上次未处理的通道是不计入当前数量。
3.5.2、停止选择过程:
wakeup():通过调用wakeup()方法让调用select()处于阻塞的方法立即返回
close():关闭selector实例,使得任意一个处于select阻塞的线程都会被唤醒,会将selector上所有的channel都被注销(cancel)
3.5.3、选择过程:
在程序执行过程中,可以调用key.cancel()方法,该方法不会立即注销channel通道等操作,而是将SelectionKey加入到已取消的集合中,在调用select()等方法时才会进行通道注销等操作。
Java中Selector复用器本质是对操作系统提供的IO复用模型(select()、poll()、epoll())封装
4、BIO和NIO区别:
1、BIO使用过程中为了解决并发量问题必须引入多线程。
2、NIO中只需要使用少量的线程,就可以进行高并发量的处理,NIO和BIO最大的不同,引入了复用器(IO复用模型:select、poll、epoll),就可以同时关注多个用户的连接的问题。