一、NIO中的Buffer
Buffer初始化的方式:
ByteBuffer.allocate(int size) //开辟指定大小
ByteBuffer.warp(byte[]) //通过数组初始化
存取元素:
(1)put:添加元素
先判断position是否超过了limit,否则指针position向前移一位,将要添加的字节存入position所指的数组的索引位置。Put()完成之后,position指向当前(存入)元素的下一位。
(2)get:获取元素
与put()类似。get()操作完成之后,position指向当前(取出)元素的下一位。
(3)flip:读写切换
对当前位置设置限制,然后将该位置设置为0。如果已定义了标记,则丢弃该标记。也就是说调用了该方法之后,读写指针指到缓存头部,并且设置了最多能读出之前写入的数据长度(而不是整个缓存的容量的大小)。可以把buffer的当前位置更改为buffer缓冲区的第一个位置。
对于NIO编程来说,我们主要使用的是ByteBuffer。从功能角度而言,ByteBuffer完全可以满足NIO编程的需要,但是由于NIO编程的复杂性,ByteBuffer也有其局限性,它的主要缺点如下。
(1)ByteBuffer长度固定,一旦分配完成,它的容量不能动态扩展和收缩,当需要编码的POJO对象大于ByteBuffer的容量时,会发生索引越界异常。
(2)ByteBuffer只有一个标识位置的指针position,读写的时候需要手工调用flip()和rewind()等,使用者必须小心谨慎地处理这些API,否则很容易导致程序处理失败。
(3)ByteBuffer的API功能有限,一些高级和实用的特性它不支持,需要使用者自己编程实现。
二、Channel
通道和IO直接相连接
通道的类型主要有以下四类:
(1)ServerSocketChannel:主要用来接收连接的通道(针对服务端来接收SocketChanel通道),基于TCP连接的通道
(2)SocketChannel:主要用来连接的通道(针对客户端连接ServerSocketChannel通道),基于TCP连接的通道
(3)DatagramChannel:主要用来UDP连接的通道
(4)FileChannel:主要用来操作文件的通道
通道和IO的区别?
(1)Channel数据是双向的,IO流式单向的
(2)通道是可以设置为非阻塞的,IO流的读写是阻塞的
C/S(客户端/服务端)模型处理流程
服务端:
1、初始化ServerSocketChannel并且绑定端口
2、实例化选择器Selector,将ServerSocketChannel设置为非阻塞并注册到选择器中,并关注ACCEPT事件
3、选择器调用select,监听事件是否发生
4、选择器上事件发生,则进行感兴趣集合遍历,找到ACCEPT事件ACCEPT
5、调用Accept方法,获取新的连接通道SocketChanel,将该通道设置为非阻塞并注册到选择器上,关注READ事件
6、继续调用select,监听读事件是否发生
7、读事件发生、则进行SocketChanel通道读取,读取完成关闭SocketChanel通道
8、关闭选择器Selector、关闭通道ServerSocketChannel
客户端:
1、初始化SocketChanel实例,并设置为非阻塞状态
2、实例化选择器Selector
3、SocketChanel通道尝试连接(Connect)服务端未连接成功,则将通道注册到选择器,并关注CONNECT事件
4、选择器调用select,监听连接事件是否发生
5、连接事件发生、则完成信道连接(SocketChanel.finshConnect),发送消息,并注册READ事件
5、可读事件发生,读取数据
6、数据读取完成,关闭Selector、SocketChanel
三、选择器 Selector
也叫做多路复用器,是检查一个或多个通道是否有时间发生:可读、可写、可连接、可接收的事件。优势是:使用更少的线程管理更多的通道。
使用:
1、选择器的实例化 Selector.open()
2、注册事件 channel.register(selector,SelectionKey.OP_ACCEPT|SelectionKey.OP_READ)
3、监听事件是否发生 selector.select()
Selector类
选择过程设计的方法
(1)select() 方法会被阻塞,直到关注的信道上有事件发生才会返回,返回的int数据表示的是就绪事件的个数
(2)select(int num) 该方法会在一定的时间内返回,返回值有可能为0,说明在timeout时间内没有事件发生
(3)selectNow() 该方法会立即返回
keys():表示的是注册到选择器上的信道的集合,这个集合中key已经失效
selectionKeys():表示的是关注的key(注册到选择器的信道)的集合
select()方法选择过程
select()方法介绍:
在刚初始化的Selector对象中,这三个集合都是空的。 通过Selector的select()方法可以选择已经准备就绪的通道 (这些通道包含你感兴趣的的事件)。比如你对读就绪的通道感兴趣,那么select()方法就会返回读事件已经就绪的那些通道。下面是Selector几个重载的select()方法:
(1) int select():阻塞到至少有一个通道在你注册的事件上就绪了。
(2) int select(long timeout):和select()一样,但最长阻塞时间为timeout毫秒。
(3) int selectNow():非阻塞,只要有通道就绪就立刻返回。
select()方法返回的int值表示有多少通道已经就绪,是自上次调用select()方法后有多少通道变成就绪状态。 之前在select()调用时进入就绪的通道不会在本次调用中被记入,而在前一次select()调用进入就绪但现在已经不在处于就绪的通道也不会被记入。
例如:首次调用select()方法,如果有一个通道变成就绪状态,返回了1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回1。如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道就绪了。一旦调用select()方法,并且返回值不为0时,则可以通过调用Selector的selectedKeys()方法来访问已选择键集合。
selector过程
1、已取消的键的集合将被检查。如果非空,每个被取消的键的集合将从其他两个集合中移除,并且相关通道将被注销,该步骤后,已取消集合将是空的。
2、已注册的键的集合汇总的键的interest集合将被检查,该步骤的检查执行后,对interest集合的改动不会影响剩余的检查过程。一旦就绪事件被确定下来了,底层操作系统将会被查询,来确定底层操作系统真是就绪状态,依赖特定的select()方法调用,如果没有通道准备好,线程将被阻塞在这里,通常会有超时值;直到操作系统调用完成为止,这个过程可能会使调用线程睡眠一段时间,然后当前每个通道的就绪时间将被确定下来,对于那些没有准备好的通道,将不执行任何操作,对于那些系统指示至少已经准备好interest集合中的一种操作的通道,将执行以下两种操作中的一种:
a.如果通道的键还没有处于已选择的键的集合中,那么键的ready集合将被清空,然后表示操作系统发现的当前通道已经准备好的操作系统的比特掩码将被设置。
b.否则,也就是键在已选择的键的集合中,键的ready集合将被表示操作系统发现的当前已经准备好的操作系统的比特掩码更新。所以之前的已经不再是就绪状态的操作不会被清除,事实上,所有的比特位都不会被清除,有操作系统决定的ready集合是与之前的ready集合按位分离的,一旦键被防放置与选择器的已选择的键的集合中,他的ready集合将是积累的,比特位只会被设置,不会被清理。
3、步骤2可能耗费较长,特别激发的线程处于休眠状态时,与该选择器相关的键可能被同时取消,当步骤2结束时,步骤1将重新执行,以完成任意一个在选择进行的过程中,借鉴可能已经被取消的通道注销。
4、select操作的返回值是ready集合在步骤2中被修改的键的数量,而不是已选择的键的集合中的通道的总数,返回值不是已准备好的通道的总数,而是从上一个select()调用之后进入就绪状态的通道的数量,之前的调用中就绪的,并且在本次调用中任然就绪的通道将不会被计入,而那些在前一次的调用中已经就绪但已经不再处于就绪状态的通道也不会被计入,这些通道可能已经在已选择的键的集合中,但不会被计入返回值中,返回值可能是0。
使用内部的已取消的键的集合来延迟注销,是一种防止线程在取消键时阻塞,并防止与正在进行的选择操作冲突的优化,注销通道是一个签字啊的代价很高的操作,这可能需要重新分配资源(记住:键是和通道相关的,并且可能与他们相关的通道之间有复杂的交互),清理已取消的键,并与选择操作之前和之后注销通道,可能消除他们之间正好选择的过程中执行的潜在棘手问题,这是另一种兼顾健壮性的折中方案。
停止选择的方法
选择器执行选择的过程,系统底层会依次询问每个通道是否已经就绪,这个过程可能会造成调用线程进入阻塞状态,那么我们有以下三种方式可以唤醒在select()方法中阻塞的线程。
(1)wakeup()方法 :通过调用Selector对象的wakeup()方法让处在阻塞状态的select()方法立刻返回。该方法使得选择器上的第一个还没有返回的选择操作立即返回。如果当前没有进行中的选择操作, 那么下一次对select()方法的一次调用将立即返回。
(2)close()方法 :通过close()方法关闭Selector,该方法使得任何一个在选择操作中阻塞的线程都被唤醒(类似wakeup()),同时使得注册到该Selector的所有Channel被注销,所有的键将被取消,但是Channel本身并不会关闭。
(3)interrupt()方法:如果睡眠中的线程的interrupt()方法被调用,它的返回状态将被设置。如果被唤醒的线程之后将试图在通道上执行I/O操作,通道将立即关闭,然后线程将捕捉到一个异常。
SelectionKey类
int OP_READ = 1 << 0; 可读事件
int OP_WRITE = 1 << 2;可写事件
int OP_CONNECT = 1 << 3;可连接事件
int OP_ACCEPT = 1 << 4可接受事件
这几个事件通过bit位来做处理
SelectionKey.OP_CONNECT ==> 0100
SelectionKey.OP_READ | SelectionKey.OP_WRITE ==>0011
SelectableChannel channel() 将信道作为SelectionKey
Selector selector() 当前选择器也会存放在SelectionKey