概述
NIO是Java1.4推出的一种全新的IO模型,全称是java non-blocking IO,提供ByteBuffer等缓存的容器,达到非阻塞式的高伸缩性网络。
IO模型
IO模型是机器进行IO具体操作方法的一种抽象,每种IO模型都有各自的优缺点,需要注意的是要完成各模型的实际开发需要操作系统的支持,在没有poll、epoll出来之前,java进行非阻塞式的读写操作很复杂,而当上述功能出现之后,java才在基于该功能上添加了nio模块,包名是java.nio,现在在类Linux是基于epoll实现的,类Unix(包含Mac)是基于kqueue 实现的。
Buffer
是一种缓冲数据的容器,可以存储各种基本类型的数据。线程不安全,数据结构如下图所示:
类似于一个数组,其中capacity为缓冲数组的长度,为固定的值;
postion表示下一个需要操作的位置;
limit为下一个不可操作的位置;
各种数据的大小关系是0<=position<=limit<=capacity
- put 写入数据,每次写入数据的地方都是postion,就会使得postion的值变大,当直到填充的数据长度超过了数组的长度,会抛出BufferOverflowException异常;
- get 读取数据 每次也会进行postion+1操作,这里需要注意到每次读取数据之前必须进行clear操作要不然会出现数据错误的问题,如下使用例子:
错误示例
正确示例
- clear() 清空缓冲区的数据,实际上是假清除,只是将postion置位0,limit置位capacity;
源码如下:
1 | public final Buffer clear() { |
- allocate(int n) 申请缓冲区,大小由参数决定;
- wrap(byte[] byets) 同样是申请缓冲区,传入的参数却是byte[],相当于设置的缓冲区大小是byte数组的长度,然后初始化设置了该缓冲容器的值;
- flip() 切换到读模式,修改limit为postion,position为0;
- hasRemaining() 查看是否还有数据可读 return position < limit;
一般的使用套路都是:
1 | ByteBuffer byteBuffer = ByteBuffer.allocate(10); |
Channel
Channel通道,和pipeline一个意思,类似于IO的Stream,只是stream是单向,要么是Input要么是Output,而Channel是双向的,也就意味着可以通过一个channel进行读写操作了。不过需要注意可以读写操作和能不能读写操作这是两回事。
Nio的channel具体实现主要为FileChannel
、DatagramChannel
、SocketChannel
、ServerSocketChannel
四种,分别对应的是文件、UDP和TCP的客户端和服务端。重点介绍SocketChannle和ServerSocketChannel。
SocketChannel
Socketchannel 是客户端,连接一个创建好的TCP网络套接字的通道,可有两种创建方式
- 新建一个socketchannel,并连接到服务器上;
1 | SocketChannel socketChannel = SocketChannel.open(); |
- 服务器接收到来自客户端的请求接收到的
1 | // 服务端接收到客户端发送的信息,通过accpet即可获取对应的socketChannel |
由上图所示,需要从channel读数据,以及向外发送数据都需要使用buffer作为缓冲容器
1 | // 读数据 |
由于其为异步模式,在其调用connect()方法的时候是立即返回结果,连接成功返回true,连接不成功返回false,并继续进行连接(服务自主操作),存在还未建立连接就返回了,所以在使用服务端数据的时候再调用finishConnect()确保链接的建立:
1 | SocketChannel socketChannel = SocketChannel.open(); |
可以通过方法isConnectionPending()的返回值确认是否处于连接中。
ServerSocketChannel
ServerSocketChannel 是应用在服务端的,和Socketchannel相对应,主要是用来监听新来的TCP请求的一个通道。绑定的本地端口,IP是本机IP。创建方式如下:
1 | ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); |
Selector
selector 是nio中能够管理多个channel通道并感知各个通道的读写状态的一个组件,可以使用单线程管理多个channel的从而同时处理多个网络请求。selector和channel是通过selectorkey绑定的
1 | // 创建一个selector |
注意register()方法的第二个参数。这是一个“interest集合”,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件:
- Selectionkey.Connect 可以连接 (客户端接收到可连接到请求);
- Selectionkey.Accept 可以接受 (服务端接到客户端连接的请求);
- Selectionkey.Read 可以读取数据;
- Selectionkey.Write 可以写入数据;
SelectionKey
绑定channel和selector的对象,还包含有read集合和interest集合(感兴趣,在register设置的值),还可以通过attachment()方法绑定一些其他数据。配套的还有判断其状态的方法
1 | public class SelectionKeyImpl extends AbstractSelectionKey { |
选择通道
selector从已经注册好的channel中获取已经准备就绪的通道进行操作,以下三种是获取通道的方法:
- int select() // 阻塞模式,至少有一个准备就绪的通道才返回
- int select(long timeout) // 加入超时设置
- int selectNow() // 会立即返回,返回当前就绪的通道个数
- selectedKeys()获取当前就绪的通道集合
- close() 关闭当前的selector,使得绑定的key全部不可用,但是通道本身还是可以正常使用的
1 | int count = selector.select(); |
DatagramChannel
收发UTP包的通道,适用于UTP协议,发送和读取的是用户数据报
1 | DatagramChannel channel = DatagramChannel.open(); |