JDK IO多路复用基础


本文所有源码均基于JDK1.8版本

通过学习JDK提供的IO多路复用相关源码,为后续的Netty源码学习打下基础

IO多路复用底层实现

JDK nio包多路复用基于底层操作系统,linux2.6版本使用epoll实现,低于2.6版本则使用select或poll实现多路复用


JDK NIO

Channel

该接口用于操作(读,写)数据源(文件,网络socket等),并提供多种实现如FileChannel, ServerSocketChannel等,替换基于流的形式操作数据源

  • 通道是全双工的,它可以比流更好地映射底层操作系统的API。特别是在UNIX网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作

  • Channel用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据

SelectorProvider

一个抽象类,用于selector和可选择的channel上,包括打开一个selector,打开一个 server-socket channel,或socket channel,同时该类持有自身类的一个属性provider

该类提供了一个静态方法provider(),用于获取当前操作系统范围下默认的支持的selector provider

public abstract class SelectorProvider {

    private static final Object lock = new Object();

    // 持有自身实例的引用
    private static SelectorProvider provider = null;

    // 获取可用的selectorProvider
    public static SelectorProvider provider() {
        synchronized (lock) {
            // 如果已存在,直接返回
            if (provider != null)
                return provider;
            return AccessController.doPrivileged(
                new PrivilegedAction<SelectorProvider>() {
                    public SelectorProvider run() {
                            // 1. 从属性配置中利用反射实例化selectorProvider
                            if (loadProviderFromProperty())
                                return provider;
                            // 2. 利用Java SPI加载(META-INF/services/)并反射实例化selectorProvider
                            if (loadProviderAsService())
                                return provider;
                            // 3. 以上都没有的话,就采用默认jdk环境下默认的selectorProvider,windows下为WindowsSelectorProvider
                            // Linux内核2.6及以上版本,采用EPollSelectorProvider,
                            //低版本内核使用PollSelectorProvider
                            provider = sun.nio.ch.DefaultSelectorProvider.create();
                            return provider;
                        }
                    });
        }
    }

    // 从属性配置中利用反射实例化selectorProvider
    private static boolean loadProviderFromProperty() {
        String cn = System.getProperty("java.nio.channels.spi.SelectorProvider");
        if (cn == null)
            return false;
        try {
            Class<?> c = Class.forName(cn, true,
                                    ClassLoader.getSystemClassLoader());
            provider = (SelectorProvider)c.newInstance();
            return true;
        } catch (ClassNotFoundException x) {
            throw new ServiceConfigurationError(null, x);
        } catch (IllegalAccessException x) {
            throw new ServiceConfigurationError(null, x);
        } catch (InstantiationException x) {
            throw new ServiceConfigurationError(null, x);
        } catch (SecurityException x) {
            throw new ServiceConfigurationError(null, x);
        }
    }

    // 利用ServiceLoader加载META-INF/services下全限定接口文件实例化文件内的实现类
    private static boolean loadProviderAsService() {

        ServiceLoader<SelectorProvider> sl =
            ServiceLoader.load(SelectorProvider.class,
                            ClassLoader.getSystemClassLoader());
        Iterator<SelectorProvider> i = sl.iterator();
        for (;;) {
            try {
                if (!i.hasNext())
                    return false;
                provider = i.next();
                return true;
            } catch (ServiceConfigurationError sce) {
                if (sce.getCause() instanceof SecurityException) {
                    // Ignore the security exception, try the next provider
                    continue;
                }
                throw sce;
            }
        }
    }

    // 打开一个selector(也是一个文件)
    public abstract AbstractSelector openSelector() throws IOException;

    // 打开一个 ServerSocketChannel
    public abstract ServerSocketChannel openServerSocketChannel() throws IOException;

    // 打开一个 SocketChannel
    public abstract SocketChannel openSocketChannel() throws IOException;

    // ...
}
WindowsSelectorProvider UML 类图
WindowsSelectorProvider

抽象类 Selector

jdk.nio中的selector对应操作系统底层的select结构(select/poll/epoll),在linux下也是一个文件

windows下的具体的Selector实现类是WindowsSelectorImpl

WindowsSelectorImpl UML类图
WindowsSelectorImpl
Selector抽象类

看下Selector中定义了哪些方法

public abstract class Selector implements Closeable {


    // 1. 通过open方法,内部还是调用了SelectorProvider的provider方法得到选择器供应商,再调用其openSelector方法获取selector
    public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
    }

    // 2. 返回是否该selector是打开状态
    public abstract boolean isOpen();

    // 3. 返回创建了该selector的selectorProvider
    public abstract SelectorProvider provider();

    // 返回该selector选择器的注册上来的key 集合 
    public abstract Set<SelectionKey> keys();

    // 返回该selector选择器的注册上来并且被选择的key 集合 
    public abstract Set<SelectionKey> selectedKeys();

    // 选择准备好IO操作的相应通道的keys集合,即发起系统调用,该方法会被阻塞,直到至少一个通道被选择
    // 或该selector的wakeup方法被调用,或当前线程被打断,或给定的事件timeout超时
    // 参数timeout若为正,则阻塞指定毫秒后返回0
    // 若为0,则无限阻塞
    // 不可为负数
    // 返回值代表了准备好IO操作的keys数量,可能返回0
    public abstract int select(long timeout)
        throws IOException;


    public abstract int select() throws IOException;

    // 调用该方法,会导致还未阻塞返回的selector立即返回
    public abstract Selector wakeup();

}

主要包括如下:

  • 获取当前selector上注册的所有keys

  • 获取当前selector上所有已选择的keys

  • 阻塞方法select()

  • 唤醒方法wakeup()

  • 其子类AbstractSelector定义的cancelledKeys(),register(),deregister()方法等

记住一点就是selector会维护三种key的集合

  • key set
    代表当前通道注册到该selector上的集合

  • selected-key set
    代表注册的通道发生了注册感兴趣的事件(ACCEPT/读/写),其selectionKey就会被加入到selected-key set中

  • canceled-key set
    当调用了cancel方法,相应的selectionKey会被加入cancelledKeys集合中

AbstractSelector 类

该类继承了Selector类,主要定义了抽象方法register方法以及一个
cancelledKeys的集合和provider属性持有

重点看register方法

protected abstract SelectionKey register(AbstractSelectableChannel ch,
                                         int ops, Object att);

该方法有三个参数

  1. 可被选择的通道ch

  2. 感兴趣的事件ops

  3. 附加属性

返回值SelectionKey对象,代表了一个注册到当前selector上的给定通道,并指定了感兴趣的事件

注册方法的具体实现由SelectorImpl类实现

SelectorImpl

protected final SelectionKey register(AbstractSelectableChannel var1, int var2, Object var3) {
    if (!(var1 instanceof SelChImpl)) {
        throw new IllegalSelectorException();
    } else {
        SelectionKeyImpl var4 = new SelectionKeyImpl((SelChImpl)var1, this);
        var4.attach(var3);
        synchronized(this.publicKeys) {
            this.implRegister(var4);
        }

        var4.interestOps(var2);
        return var4;
    }
}

可以看到这里就是new了一个SelectionKeyImpl,并将其注册到selector上(即加入到keys set集合中)同时设置感兴趣的事件到该SelectionKey实例的interestOps属性中

注册操作:

  1. 添加SelectionKey至Java Selector对象的keys集合

  2. 记录Java Selector对象的fdMap属性,key为该SelectionKey注册的channel的fd值,value为该SelectionKey

  3. 调用操作系统底层epoll_ctl系统调用中段注册epollfd需要监听的channel指定的事件

SelectionKeyImpl 选择键

一个SelectionKey即代表了一个channel和其注册的selector之间的关系,即通过这个SelectionKey就可以知道是哪个channel注册到了哪个selector上,并且其希望Selector监听该Channel的哪些事件

UML类图
image.png

该类有几个主要属性

SelChImpl channel

注册的通道

SelectorImpl selector

通道注册到哪个选择器

 int interestOps

注册时感兴趣的操作事件

readyOps

已准备好的操作事件

该类同时定义了一些列的操作常量如:OP_READ, OP_WRITE, OP_CONNECT, OP_ACCEPT

以及一些判断是否该key的channel准备好了相应的事件如:isReadable(),isWritable(), isConnectable(), isAcceptable(),其判断逻辑就是用key的内部属性readyOps中是否含有指定的事件

select方法详解

Java Selector有两个重载的select方法以及一个selectNow()方法

public abstract int select() throws IOException;

public abstract int select(long timeout) throws IOException;

public abstract int selectNow() throws IOException;
  • selectNow()

该方法是非阻塞的,如果没有channels变为可选择的,会立即返回0

  • select()

该方法时阻塞的,返回的前提是至少有一个channel是可选择的或该selector的wakeup方法被调用,或者运行select方法的当前线程被打断

  • select(long timeout)

该方法基本上同select(),只是另外提供了timeout参数用于当没有channel可选择时,阻塞等待的毫秒数,注意该参数为0即代表一直阻塞,需要大于0才生效

以上三个方法最终的调用都会转到doSelect方法

这里以windows环境为例,WindowsSelectorImpl

定义了最大可选择的fd数量为1024

private static final int MAX_SELECTABLE_FDS = 1024;

doSelect伪代码

protected int doSelect(long var1) throws IOException {

    // 1. 处理取消注册channel
    this.processDeregisterQueue();

    // 2. 调用内部SubSelector的poll方法,SubSelector类为WindowsSelectorImpl的内部类,封装了系统调用poll的相关native方法
    this.subSelector.poll();

    // 3. 再次处理取消注册的channel
    this.processDeregisterQueue();

    // 4. 更新可选择channel的keys到selector的selectedKeys属性中
    int var3 = this.updateSelectedKeys();

    // 5. 返回可选择key的数量
    return var3;
}

其中1. 中主要处理已取消注册通道,即若cancelledKeys集合不为空,则遍历依次取消该Key

主要包括

  1. 调用内核取消该fd的指定事件的监听

  2. fdMap属性的清除

  3. keys属性清除

  4. selectedKeys属性清除

  5. 失效该SelectionKey,设置valid属性为false

而poll()的调用即是发起内核系统调用,阻塞获取可选择的fd

最后一个的updateSelectedKeys()方法就是根据内核返回的可选择的fd去fdMap中找到对应的SelectionKey,再将SelectionKey加入到该selector的selectedKeys集合中,这样我们应用程序的select方法返回后就可以从selectedKeys集合中获取可选择的key,进而进行我们自己的处理

总结

最后以一张图表示整个IO多路复用流程

image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,273评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,349评论 3 398
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,709评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,520评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,515评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,158评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,755评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,660评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,203评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,287评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,427评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,122评论 5 349
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,801评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,272评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,393评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,808评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,440评论 2 359

推荐阅读更多精彩内容