netty补充NIO的SelectableChannel和Sel
SelectableChannel作为nio选择器和通道的关键
先看官方描述
/**
* A channel that can be multiplexed via a {@link Selector}.
* <p> In order to be used with a selector, an instance of this class must
* first be <i>registered</i> via the {@link #register(Selector,int,Object)
* register} method. This method returns a new {@link SelectionKey} object
* that represents the channel's registration with the selector.
*其他略。。。
*/
简单总结:
它通道的注册使用大致过程如下:
-
新建通道 open方法
SelectionKey key = channel.register(selector, SelectionKey.OP_READ); -
将通道已经通道感兴趣的事件注册到选择器Selector上
-
通过SelectKey获得需要处理的通道,然后对通道进行处理
关闭一个已经注册的SelectableChannel需要两个步骤:
*上面channel的父类就是SelectableChannel了
它通道的取消大致过程如下: -
取消注册的key,这个可以通过SelectionKey.cancel方法,也可以通过SelectableChannel.close方法,或者中断阻塞在该channel上的IO操作的线程来做到。
-
后续的Selector.selectXXX方法的调用才真正地关闭 本地Socket。
因而,如果,如果在取消SelectionKey后没有调用到selector的select方法(因为Client一般在取消key后, 我们都会终止调用select的循环,当然,server关闭一个注册的channel我们是不会终止select循环的),那么本地socket将进入CLOSE-WAIT 状态(等待本地Socket关闭)。简单的解决办法是在 SelectableChannel.close方法之后调用Selector.selectNow方法,类似:
Selector sel;
SocketChannel sch;
// …
sch.close();
sel.selectNow();
实现类有关UDP协议的:DatagramChannel
有关SCTP协议的:SctpChannel、SctpMultiChannel、SctpServerChannel
[有关TCP协议的:ServerSocketChannel、SocketChannel
有关管道的:SinkChannel、SourceChannel这两个抽象类定义在java.nio.channels.Pipe类中
仅以SocketChannel和ServerSocketChannel分析提供如下类图:
SocketChannel和ServerSocketChannel,两者的父类是SelectableChannel,类图结构如下:
ServerSocketChannel的类图
图片.png
SocketChannel的类图
图片.png
SelectorProvider作为选择器的核心部分
- 顾名思义作为选择器的生产类,那么他是如何做到呢
- 通常我们使用Selector 创建一个新的选择器
Selector selector = Selector.open();
跟踪selector的源码如下:
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
继续往下跟踪就是我们要介绍的关键了SelectorProvider.provider()
public static SelectorProvider provider() {
synchronized (lock) {
if (provider != null)
return provider;
//AccessController.doPrivileged属于特权操作,下面详说
return AccessController.doPrivileged(
new PrivilegedAction<SelectorProvider>() {
public SelectorProvider run() {
//loadProviderFromProperty方法是通过JDK的参数//-Djava.nio.channels.spi.SelectorProvider=class设置的class来反射构造SelectorProvider
if (loadProviderFromProperty())
return provider;
//loadProviderAsService从jar中的目录META-INF/services配置文件中找参数//java.nio.channels.spi.SelectorProvider=class设置的第一个class来反射构造SelectorProvider
if (loadProviderAsService())
return provider;
//最后都没有则调用不同操作系统版本的JDK里自带的sun.nio.ch.DefaultSelectorProvider来创建SelectorProvider
provider = sun.nio.ch.DefaultSelectorProvider.create();
return provider;
}
});
}
}
先解释下AccessController访问控制类的三个功能
1、根据当前有效的安全策略,决定是允许还是拒绝对关键系统资源的访问,
2、将代码标记为“特权”,从而影响后续访问确定
3、获取当前调用上下文的“快照”,因此可以针对保存的上下文进行来自不同上下文的访问控制决策。
上面的特权操作主要使用了功能2,部分javadoc中伪代码如下:
//类注释
/**
* somemethod() {
* ...normal code here...
* String user = AccessController.doPrivileged(
* new PrivilegedAction<String>() {
* public String run() {
* return System.getProperty("user.name");
* }
* });
* ...normal code here...
* }}
* 其他略。。
**/
public final class AccessController {
//方法注释
/**
* Performs the specified {@code PrivilegedAction} with privileges
* enabled. The action is performed with <i>all</i> of the permissions
* possessed by the caller's protection domain.
* 其他略。。
*/
//简言之就是让调用者有权执行方法代码中的action
@CallerSensitive
public static native <T> T doPrivileged(PrivilegedAction<T> action);
//其他略。。
}
总结SelectorProvider的创建分三步进行:
(1)由JDK的参数-Djava.nio.channels.spi.SelectorProvider=class设置的class来反射构造SelectorProvider,找不到就跳转到步骤(2)
(2)从jar中的目录META-INF/services配置文件中找参数java.nio.channels.spi.SelectorProvider=class设置的第一个class来反射构造SelectorProvider,找不到就跳转到步骤(3)
(3)调用不同操作系统版本的JDK里自带的sun.nio.ch.DefaultSelectorProvider来创建SelectorProvider
一般都会走到最后的步骤(3),而这个步骤里创建的SelectorProvider在各个操作系统对应的JDK里各不相同。sun.nio.ch.DefaultSelectorProvider这个类最终编译后放置在JDK的安装根目录下的jre/lib/rt.jar里。
- 该方法返回系统范围默认选择器提供程序,那么系统默认的选择器和系统有关吗,答案是肯定的
不通系统的DefaultSelectorProvider实现
- Windows
public class DefaultSelectorProvider {
//
public static SelectorProvider create() {
return new WindowsSelectorProvider();
}
}
- MAC
public class DefaultSelectorProvider {
public static SelectorProvider create()
{
return new KQueueSelectorProvider();
}
}
- Linux
public class DefaultSelectorProvider {
public static SelectorProvider create()
{
String str1 = (String)AccessController.doPrivileged(new GetPropertyAction("os.name"));
if ("SunOS".equals(str1)) {
return new DevPollSelectorProvider();
}
if ("Linux".equals(str1)) {
String str2 = (String)AccessController.doPrivileged(new GetPropertyAction("os.version"));
String[] arrayOfString = str2.split("\\.", 0);
if (arrayOfString.length >= 2) {
try {
int i = Integer.parseInt(arrayOfString[0]);
int j = Integer.parseInt(arrayOfString[1]);
if ((i > 2) || ((i == 2) && (j >= 6))) {
return new EPollSelectorProvider();
}
}
catch (NumberFormatException localNumberFormatException)
{
}
}
}
return new PollSelectorProvider();
}
}
那么为什么会出现几种不同的实现方式呢?原因在于
不同操作系统的I/O多路复用选择器各自内核实现不同,目前有select、poll、epoll、kqueue四种实现
操作系统下JDK | JDK里SelectorProvider实现 |
---|---|
windows JDK | sun.nio.cn.WindowsSelectorProvider |
MAC JDK | sun.nio.ch.KQueueSelectorProvider |
Linux JDK | 1)os.name=SunOS时,sun.nio.ch.DevPollSelectorProvider 2)os.name=Linux并且os.version版本号中第一个版本大于2或者第一个版本号等于2且第二个版本大于6时, sun.nio.chEPollSelectorProvider Linux kernels 2.6内核版本及以后使用epoll进行支持; Linux kernels 2.6内核版本之前使用poll进行支持; 比如Linux version 3.10.0-327.el7.x86_64就是用的 |
- 注 操作系统的I/O多路复用可参考https://www.jianshu.com/p/db5da880154a