netty程序员技术干货

关于 NIO 你不得不知道的一些“地雷”

2017-08-22  本文已影响1177人  tomas家的小拨浪鼓

本文是笔者在学习NIO过程中发现的一些比较容易让人忽略的知识的一个总结,而这些让人忽略的小细节恰恰是NIO网络编程中必不可少。虽然现在我们不会直接编写NIO来完成我们的网络层通讯,而是使用成熟的基于NIO的网络框架来实现我们的网络层。如,netty、mina。但对NIO网络编程过程的了解,非常有助于我们更深入的理解netty、mina等网络框架,以至于能更好的使用它们。
因此,本文并不对NIO的一些基层知识做过多的介绍,主要侧重于NIO编程中细节的讲解。

NIO VS IO

Buffer

Java NIO中的Buffer用于和NIO通道进行交互。数据总是从通道读取到缓冲区,或者从缓冲区写入到通道中。
Buffer是一个特定的原生类型数据容器。
Buffer是一种特定的原生类型的线程的、有限的元素序列。除了它的内容之外,一个Buffer一个重要的本质属性是它的capacity、limit、和position;

数据操作:

Buffer的每一个子类都定义了两类get和put操作。

不变性:

0 <= mark <= position <= limit <= capacity

线程安全性:

buffer在多线程并发下并不是安全的。如果一个buffer会在多个线程使用,那么需要使用恰当的同步操作来访问buffer。也就是buffer本身并不是线程安全的。

Java NIO 内存分配
方法

Selector

为什么使用Selector?

仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道。因为对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源。因此,使用的线程越少越好。

selector的非阻塞模式

与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以。

方法

关于selector的详细实现可见浅谈 Linux 中 Selector 的实现原理

SocketChannel

Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。

方法
示例: 无论如何在connect后finishConnect()sorry 方法都是需要被调用的。调用finishConnect()的三种返回:

① 如果你在connect()后直接调用了finishConnect()( 并非在CONNECT事件中调用 ),则若finishConnect()返回了true,则表示channel连接已经建立,而且CONNECT事件也不会被触发了。
② 如果finishConnect()方法返回false,则表示连接还未建立好。那么就可以通过CONNECT事件来监听连接的完成。当然也可以像上面的写法,无论如何都会给SocketChannel注册CONNECT事件,finishConnect()方法的调用放到CONNECT事件处理中调用。
③ 如果finishConnect()方法抛出了一个IOException异常,则表示连接操作失败。

支持的事件:SelectionKey.OP_CONNECT | SelectionKey.OP_READ | SelectionKey.OP_WRITE

ServerSocketChannel

Java NIO中的 ServerSocketChannel 是一个可以监听新进来的TCP连接的通道, 就像标准IO中的ServerSocket一样。

支持的事件:SelectionKey.OP_ACCEPT

ServerSocketChannel & SocketChannel

关于selectedKey集合的处理

对于已经处理的SelectionKey需要充selectedKey集合中移除,如果不将已经处理的SelectionKey从selectedKey集合中移除,那么下次有新事件到来时,在遍历selectedKey集合时又会遍历到这个SelectionKey,这个时候就很可能出错了。比如,如果没有在处理完OP_ACCEPT事件后将对应SelectionKey从selectedKey集合移除,那么下次遍历selectedKey集合时,处理到到该SelectionKey,相应的ServerSocketChannel.accept()将返回一个空(null)的SocketChannel。

关于OP_WRITE事件:

OP_WRITE事件的就绪条件并不是发生在调用channel的write方法之后,而是在当底层缓冲区有空闲空间的情况下。因为写缓冲区在绝大部分时候都是有空闲空间的,所以如果你注册了写事件,这会使得写事件一直处于就就绪,选择处理现场就会一直占用着CPU资源。所以,只有当你确实有数据要写时再注册写操作,并在写完以后马上取消注册。
其实,在大部分情况下,我们直接调用channel的write方法写数据就好了,没必要都用OP_WRITE事件。那么OP_WRITE事件主要是在什么情况下使用的了?
其实OP_WRITE事件主要是在发送缓冲区空间满的情况下使用的。如:

while (buffer.hasRemaining()) {
     int len = socketChannel.write(buffer);   
     if (len == 0) {
          selectionKey.interestOps(selectionKey.interestOps() | SelectionKey.OP_WRITE);
          selector.wakeup();
          break;
     }
}

当buffer还有数据,但缓冲区已经满的情况下,socketChannel.write(buffer)会返回已经写出去的字节数,此时为0。那么这个时候我们就需要注册OP_WRITE事件,这样当缓冲区又有空闲空间的时候就会触发OP_WRITE事件,这是我们就可以继续将没写完的数据继续写出了。
而且在写完后,一定要记得将OP_WRITE事件注销:
selectionKey.interestOps(sk.interestOps() & ~SelectionKey.OP_WRITE);
注意,这里在修改了interest之后调用了wakeup();方法是为了唤醒被堵塞的selector方法,这样当while中判断selector返回的是0时,会再次调用selector.select()。而selectionKey的interest是在每次selector.select()操作的时候注册到系统进行监听的,所以在selector.select()调用之后修改的interest需要在下一次selector.select()调用才会生效。

关于远端关闭事件

SelectionKey并没有提供关闭事件,其实通过OP_READ是可以监听到远端的关闭操作的。
当OP_READ事件触发使,int readByteNum = channel.read(buffer)会返回从channel读取到的字节数。
① 当readByteNum > 0 时,表示从channel读取到了readByteNum个字节到buffer中。
② 当readByteNum == 0 时,表示channel中已经没有数据可以读取了,这个时候buffer的position==limit。
当 readByteNum == -1 时,表示远端channel正常关闭了。这个时候我们就需要进行该通道的关闭和注销操作了。
netty源码中OP_READ事件也会根据读取到的字节数为-1时,进行channel的关闭操作。

这里closeOnRead(pipeline)方法最终会调用channel.close()方法来完成tcp套接字的关闭(这点下面会详细说明)
如何正确的关闭一个已经注册的SelectableChannel了?

需要调用channel.close()
最终调用的会使AbstractInterruptibleChannel的close方法

总归来说,调用channel.close()方法:
① 能够调动channel对应的SelectionKey的cancel()方法使该SelectionKey加到Selector的cancel selectionKey set集合中,这样在下一次selector的时候,就会将其从selector中相关的selectionKey集合中移除,并且不会监听该selectionKey所感兴趣的事件了。
② 会关闭底层的套接字连接。
这里注意:如果只是通过调用SelectionKey.cancel()来注销一个远端已经关闭了的channel,是一个不对的方法。因为selector.select()在处理cancel selectionKey set(注销的SelectionKey集合)的时候,会判断若该SelectionKey对应的channel已经没有注册到其他的selector,并且该channel open表示为false的情况下,才会去调用底层套接字的关闭操作。所以如果之调用SelectionKey.cancel()来注销一个远端已经关闭了的channel,会导致本段的TCP连接处于“CLOSE_WAIT”状态,一直在等待程序调用套接字的关闭。
补充:channel的open标志,只有在下面两种情况下才会将open置为false。
a) 调用了channel.close()方法;

b) 或者操作channel读/写的当前线程发生中断时。

参考

圣思园netty课程

上一篇下一篇

猜你喜欢

热点阅读