- 获取Selector
Selector selector = Selector.open();
Selector.open()执行SelectorProvider.provider().openSelector();会先通过单例模式获取SelectorProvider。
如果provider不为空,则直接返回,如果为空,则通过java提供的访问安全控制获取SelectorProvider。
- 查看系统配置是否有java.nio.channels.spi.SelectorProvider定义的配置,如果有,则通过反射实例话类并返回
- 通过SPI查看是否有扩展的SelectorProvider。如果有返回
- 通过DefaultSelectorProvider创建默认的SelectorProvider。
上图为linux的DefaultSelectorProvider,可以看到在linux内核在2.6以上的,使用的是EPollSelectorProvider。
在不同的系统DefaultSelectorProvider实现不一样。Windows返回WindowsSelectorProvider,linux返回的是EPollSelectorProvider。Mac返回的是KQueueSelectorProvider。
- 然后调用相应SelectorProvider的openSelector()
以mac为例,实例化了一个KQueueSelectorImpl。KQueueSelectorImpl的继承关系如图
//构造函数
KQueueSelectorImpl(SelectorProvider var1) {
//调用父类的构造函数,传入的是一个SelectorProvider,在Mac上是KQueueSelectorProvider
super(var1);
long var2 = IOUtil.makePipe(false);//native方法
this.fd0 = (int)(var2 >>> 32);//高32位存放的是Pipe管道的读端的文件描述符
this.fd1 = (int)var2;//低32位存放的是Pipe管道的写端的文件描述符
this.kqueueWrapper = new KQueueArrayWrapper();
this.kqueueWrapper.initInterrupt(this.fd0, this.fd1);
this.fdMap = new HashMap();
this.totalChannels = 1;
}
IOUtil.makePipe(false); 是一个static native方法,所以我们没办法查看源码。但是我们可以知道该函数返回了一个非堵塞的管道(pipe),底层是通过Linux的pipe系统调用实现的;创建了一个管道pipe并返回了一个64为的long型整数,该数的高32位存放了该管道读端的文件描述符,低32位存放了该pipe的写端的文件描述符。@link http://www.importnew.com/26258.htmlhttps://blog.youkuaiyun.com/u010853261/article/details/53464475
2.ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
ServerSocketChannel.open()执行了SelectorProvider.provider().openServerSocketChannel(),SelectorProvider.provider()和第一步一样,由于第一步已经获取了KQueueSelectorProvider,在直接执行KQueueSelectorProvider.openServerSocketChannel()。由于openServerSocketChannel()属于KQueueSelectorProvider的父类SelectorProviderImpl,返回的是ServerSocketChannelImpl。在各个系统一致。
ServerSocketChannelImpl(SelectorProvider var1) throws IOException {
super(var1);
this.fd = Net.serverSocket(true);////获取ServerSocket的文件描述符
this.fdVal = IOUtil.fdVal(this.fd);// //获取文件描述的id
this.state = 0;
}
从上面来看,ServerSocketChannelImpl的初始化主要是初始化ServerSocket通道线程thread,地址绑定,接受连接同步锁,默认创建ServerSocketChannelImpl的状态为未初始化,文件描述和文件描述id,如果使用本地地址,则获取本地地址。
3.serverSocketChannel.configureBlocking(false);
此方法调用的是native方法,将通道设置为非阻塞模式
4. serverSocketChannel.socket().bind(new InetSocketAddress(2000));
调用socket()方法,返回ServerSocketAdaptor实例,调用ServerSocketAdaptor的bind(),然后调用ServerSocketChannelImpl的bind().进行端口绑定和监听。
- serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);调用AbstractSelectableChannel.register()
首先判断该通道是否打开,以及SelectionKey是否合法是否为非阻塞模式。此方法也可以在通道上附带对象,方便后续使用。然后调用SelectorImpl.register()。调用KQueueSelectorImpl. implRegister()。将通道和selector进行绑定。
- 循环调用KQueueSelectorImpl.select()查看是否有第五步注册的SelectionKey.OP_ACCEPT事件。Selector有三个方法进行select。一个是select()阻塞,直到有事件产生。select(timeout)超时阻塞,如果达到超时时间时没有事件则返回。selectNow()无论有没有事件立即返回。这三个方法都调用的SelectorImpl.lockAndDoSelect(),根据传入的超时事件进行区别。lockAndDoSelect是加锁的,线程安全的。如果有进行处理实际的C代码可以参考https://juejin.im/entry/5b51546df265da0f70070b93
byteBuffer
java提供了对通道的高速缓存byteBuffer。有三个关键字mark,position,limit,capacity
mark用于对当前position的标记
position表示当前可读写的指针,如果是向ByteBuffer对象中写入一个字节,那么就会向position所指向的地址写入这个字节,如果是从ByteBuffer读出一个字节,那么就会读出position所指向的地址读出这个字节,读写完成后,position加1
limit是可以读写的边界,当position到达limit时,就表示将ByteBuffer中的内容读完,或者将ByteBuffer写满了。
capacity是这个ByteBuffer的容量,上面的程序中调用ByteBuffer.allocate(128)就表示创建了一个容量为capacity字节的ByteBuffer对象。
每次读完后需要调用filp()进行反转后才能写,同样的每次写之前也需要调用filp()进行反转才能读。
总结
通过上面我们对源码的分析,javanio也是通过调用各个不同的系统实现不同的方式,然后绑定channel和selector,通过循环调用查询响应的事件,然后进行处理。Java nio在各个不同的系统使用的I/O模式都不相同,实际以系统为准,通过第一节对linux的I/O模型进行分析,linux下使用I/O复用模型,能够达到最好的效果。简述java nio的使用,第一步创建一个通道、一个selector,设置为非阻塞并且绑定到响应的端口,然后将通道注册到selector。通过selector的select()进行循环,查询出感兴趣的事件。