netty系列之:kequeue传输协议详解
简介
在前面的章节中,我们介绍了在netty中可以使用kequeue或者epoll来实现更为高效的native传输方式。那么kequeue和epoll和NIO传输协议有什么不同呢?
本章将会以kequeue为例进行深入探讨。
在上面我们介绍的native的例子中,关于kqueue的类有这样几个,分别是KQueueEventLoopGroup,KQueueServerSocketChannel和KQueueSocketChannel,通过简单的替换和添加对应的依赖包,我们可以轻松的将普通的NIO netty服务替换成为native的Kqueue服务。
是时候揭开Kqueue的秘密了。
KQueueEventLoopGroup
eventLoop和eventLoopGroup是用来接受event和事件处理的。先来看下KQueueEventLoopGroup的定义:
public final class KQueueEventLoopGroup extends MultithreadEventLoopGroup
作为一个MultithreadEventLoopGroup,必须实现一个newChild方法,用来创建child EventLoop。在KQueueEventLoopGroup中,除了构造函数之外,额外需要实现的方法就是newChild:
    protected EventLoop newChild(Executor executor, Object... args) throws Exception {
        Integer maxEvents = (Integer) args[0];
        SelectStrategyFactory selectStrategyFactory = (SelectStrategyFactory) args[1];
        RejectedExecutionHandler rejectedExecutionHandler = (RejectedExecutionHandler) args[2];
        EventLoopTaskQueueFactory taskQueueFactory = null;
        EventLoopTaskQueueFactory tailTaskQueueFactory = null;
        int argsLength = args.length;
        if (argsLength > 3) {
            taskQueueFactory = (EventLoopTaskQueueFactory) args[3];
        }
        if (argsLength > 4) {
            tailTaskQueueFactory = (EventLoopTaskQueueFactory) args[4];
        }
        return new KQueueEventLoop(this, executor, maxEvents,
                selectStrategyFactory.newSelectStrategy(),
                rejectedExecutionHandler, taskQueueFactory, tailTaskQueueFactory);
    }
newChild中的所有参数都是从KQueueEventLoopGroup的构造函数中传入的。除了maxEvents,selectStrategyFactory和rejectedExecutionHandler之外,还可以接收taskQueueFactory和tailTaskQueueFactory两个参数,最后把这些参数都传到KQueueEventLoop的构造函数中去,最终返回一个KQueueEventLoop对象。
另外在使用KQueueEventLoopGroup之前我们还需要确保Kqueue在系统中是可用的,这个判断是通过调用KQueue.ensureAvailability();来实现的。
KQueue.ensureAvailability首先判断是否定义了系统属性io.netty.transport.noNative,如果定了,说明native transport被禁用了,后续也就没有必要再进行判断了。
如果io.netty.transport.noNative没有被定义,那么会调用Native.newKQueue()来尝试从native中获取一个kqueue的FileDescriptor,如果上述的获取过程中没有任何异常,则说明kqueue在native方法中存在,我们可以继续使用了。
以下是判断kqueue是否可用的代码:
    static {
        Throwable cause = null;
        if (SystemPropertyUtil.getBoolean("io.netty.transport.noNative", false)) {
            cause = new UnsupportedOperationException(
                    "Native transport was explicit disabled with -Dio.netty.transport.noNative=true");
        } else {
            FileDescriptor kqueueFd = null;
            try {
                kqueueFd = Native.newKQueue();
            } catch (Throwable t) {
                cause = t;
            } finally {
                if (kqueueFd != null) {
                    try {
                        kqueueFd.close();
                    } catch (Exception ignore) {
                        // ignore
                    }
                }
            }
        }
        UNAVAILABILITY_CAUSE = cause;
    }
KQueueEventLoop
KQueueEventLoop是从KQueueEventLoopGroup中创建出来的,用来执行具体的IO任务。
先来看一下KQueueEventLoop的定义:
final class KQueueEventLoop extends SingleThreadEventLoop
不管是NIO还是KQueue或者是Epoll,因为使用了更加高级的IO技术,所以他们使用的EventLoop都是SingleThreadEventLoop,也就是说使用单线程就够了。
和KQueueEventLoopGroup一样,KQueueEventLoop也需要判断当前的系统环境是否支持kqueue:
    static {
        KQueue.ensureAvailability();
    }
上一节讲到了,KQueueEventLoopGroup会调用KQueueEventLoop的构造函数来返回一个eventLoop对象, 我们先来看下KQueueEventLoop的构造函数:
    KQueueEventLoop(EventLoopGroup parent, Executor executor, int maxEvents,
                    SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler,
                    EventLoopTaskQueueFactory taskQueueFactory, EventLoopTaskQueueFactory tailTaskQueueFactory) {
        super(parent, executor, false, newTaskQueue(taskQueueFactory), newTaskQueue(tailTaskQueueFactory),
                rejectedExecutionHandler);
        this.selectStrategy = ObjectUtil.checkNotNull(strategy, "strategy");
        this.kqueueFd = Native.newKQueue();
        if (maxEvents == 0) {
            allowGrowing = true;
            maxEvents = 4096;
        } else {
            allowGrowing = false;
        }
        this.changeList = new KQueueEventArray(maxEvents);
        this.eventList = new KQueueEventArray(maxEvents);
        int result = Native.keventAddUserEvent(kqueueFd.intValue(), KQUEUE_WAKE_UP_IDENT);
        if (result < 0) {
            cleanup();
            throw new IllegalStateException("kevent failed to add user event with errno: " + (-result));
        }
    }
传入的maxEvents表示的是这个KQueueEventLoop能够接受的最大的event个数。如果maxEvents=0,则表示KQueueEventLoop的event容量可以动态扩展,并且最大值是4096。否则的话,KQueueEventLoop的event容量不能扩展。
maxEvents是作为数组的大小用来构建changeList和eventList。
KQueueEventLoop中还定义了一个map叫做channels,用来保存注册的channels:
private final IntObjectMap<AbstractKQueueChannel> channels = new IntObjectHashMap<AbstractKQueueChannel>(4096);
来看一下channel的add和remote方法:
    void add(AbstractKQueueChannel ch) {
        assert inEventLoop();
        AbstractKQueueChannel old = channels.put(ch.fd().intValue(), ch);
        assert old == null || !old.isOpen();
    }
    void remove(AbstractKQueueChannel ch) throws Exception {
        assert inEventLoop();
        int fd = ch.fd().intValue();
        AbstractKQueueChannel old = channels.remove(fd);
        if (old != null && old != ch) {
            channels.put(fd, old);
            assert !ch.isOpen();
        } else if (ch.isOpen()) {
            ch.unregisterFilters();
        }
    }
可以看到添加和删除的都是AbstractKQueueChannel,后面的章节中我们会详细讲解KQueueChannel,这里我们只需要知道channel map中的key是kequeue中特有的FileDescriptor的int值。
再来看一下EventLoop中最重要的run方法:
   protected void run() {
        for (;;) {
            try {
                int strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
                switch (strategy) {
                    case SelectStrategy.CONTINUE:
                        continue;
                    case SelectStrategy.BUSY_WAIT:
                    case SelectStrategy.SELECT:
                        strategy = kqueueWait(WAKEN_UP_UPDATER.getAndSet(this, 0) == 1);
                        if (wakenUp == 1) {
                            wakeup();
                        }
                    default:
                }
                final int ioRatio = this.ioRatio;
                if (ioRatio == 100) {
                    try {
                        if (strategy > 0) {
                            processReady(strategy);
                        }
                    } finally {
                        runAllTasks();
                    }
                } else {
                    final long ioStartTime = System.nanoTime();
                    try {
                        if (strategy > 0) {
                            processReady(strategy);
                        }
                    } finally {
                        final long ioTime = System.nanoTime() - ioStartTime;
                        runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                    }
它的逻辑是先使用selectStrategy.calculateStrategy获取当前的select strategy,然后根据strategy的值来判断是否需要执行processReady方法,最后执行runAllTasks,从task queue中拿到要执行的任务去执行。
selectStrategy.calculateStrategy用来判断当前的select状态,默认情况下有三个状态,分别是:SELECT,CONTINUE,BUSY_WAIT。 这三个状态都是负数:
    int SELECT = -1;
    int CONTINUE = -2;
    int BUSY_WAIT = -3;
分别表示当前的IO在slect的block状态,或者跳过当前IO的状态,和正在IO loop pull的状态。BUSY_WAIT是一个非阻塞的IO PULL,kqueue并不支持,所以会fallback到SELECT。
除了这三个状态之外,calculateStrategy还会返回一个正值,表示当前要执行的任务的个数。
在run方法中,如果strategy的结果是SELECT,那么最终会调用Native.keventWait方法返回当前ready的events个数,并且将ready的event放到KQueueEventArray的eventList中去。
如果ready的event个数大于零,则会调用processReady方法对这些event进行状态回调处理。
怎么处理的呢?下面是处理的核心逻辑:
            AbstractKQueueChannel channel = channels.get(fd);
            AbstractKQueueUnsafe unsafe = (AbstractKQueueUnsafe) channel.unsafe();
            if (filter == Native.EVFILT_WRITE) {
                unsafe.writeReady();
            } else if (filter == Native.EVFILT_READ) {
                unsafe.readReady(eventList.data(i));
            } else if (filter == Native.EVFILT_SOCK && (eventList.fflags(i) & Native.NOTE_RDHUP) != 0) {
                unsafe.readEOF();
            }
这里的fd是从eventList中读取到的:
final int fd = eventList.fd(i);
根据eventList的fd,我们可以从channels中拿到对应的KQueueChannel,然后根据event的filter状态来决定KQueueChannel的具体操作,是writeReady,readReady或者readEOF。
最后就是执行runAllTasks方法了,runAllTasks的逻辑很简单,就是从taskQueue中读取任务然后执行。
KQueueServerSocketChannel和KQueueSocketChannel
KQueueServerSocketChannel是用在server端的channel:
public final class KQueueServerSocketChannel extends AbstractKQueueServerChannel implements ServerSocketChannel {
KQueueServerSocketChannel继承自AbstractKQueueServerChannel,除了构造函数之外,最重要的一个方法就是newChildChannel:
    @Override
    protected Channel newChildChannel(int fd, byte[] address, int offset, int len) throws Exception {
        return new KQueueSocketChannel(this, new BsdSocket(fd), address(address, offset, len));
    }
这个方法用来创建一个新的child channel。从上面的代码中,我们可以看到生成的child channel是一个KQueueSocketChannel的实例。
它的构造函数接受三个参数,分别是parent channel,BsdSocket和InetSocketAddress。
    KQueueSocketChannel(Channel parent, BsdSocket fd, InetSocketAddress remoteAddress) {
        super(parent, fd, remoteAddress);
        config = new KQueueSocketChannelConfig(this);
    }
这里的fd是socket accept acceptedAddress的结果:
int acceptFd = socket.accept(acceptedAddress);
下面是KQueueSocketChannel的定义:
public final class KQueueSocketChannel extends AbstractKQueueStreamChannel implements SocketChannel {
KQueueSocketChannel和KQueueServerSocketChannel的关系是父子的关系,在KQueueSocketChannel中有一个parent方法,用来返回ServerSocketChannel对象,这也是前面提到的newChildChannel方法中传入KQueueSocketChannel构造函数中的serverChannel:
public ServerSocketChannel parent() {
        return (ServerSocketChannel) super.parent();
    }
KQueueSocketChannel还有一个特性就是支持tcp fastopen,它的本质是调用BsdSocket的connectx方法,在建立连接的同时传递数据:
int bytesSent = socket.connectx(
                                (InetSocketAddress) localAddress, (InetSocketAddress) remoteAddress, iov, true);
总结
以上就是KqueueEventLoop和KqueueSocketChannel的详细介绍,基本上和NIO没有太大的区别,只不过性能根据优秀。
更多内容请参考 http://www.flydean.com/53-1-netty-kqueue-transport/
最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!
欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!
netty系列之:kequeue传输协议详解的更多相关文章
- 快来体验快速通道,netty中epoll传输协议详解
		目录 简介 epoll的详细使用 EpollEventLoopGroup EpollEventLoop EpollServerSocketChannel EpollSocketChannel 总结 简 ... 
- 流媒体传输协议详解之---RTSP认证
		from:http://blog.csdn.net/machh/article/details/52121648 Rtsp认证主要分为两种: 基本认证(basic authentication)和 ... 
- RTMP协议详解(转)
		转自<RTMP协议详解(一) (二) (三) > Real Time Messaging Protocol(实时消息传送协议协议)是Adobe Systems公司为Flash播放器和服务器 ... 
- TCP/IP协议详解概述
		TCP/IP协议详解卷1--第一章概述--读书笔记 作者:vpoet 日期:2015/06/25 注:本系列的文章只是作者对TCP/IP协议的理解,难免会出现纰漏或者不完整,当然也有可能很肤浅,希望大 ... 
- HTTP 协议详解(转载)
		原文: http://kb.cnblogs.com/page/130970/#httpmeessagestructe HTTP协议详解 当今web程序的开发技术真是百家争鸣,ASP.NET, PHP, ... 
- Telnet 协议详解
		Telnet 协议详解 一.概述 ============================================================ Telnet 协议是 TCP/IP 协议族中 ... 
- 第十六章 IIC协议详解+UART串口读写EEPROM
		十六.IIC协议详解+Uart串口读写EEPROM 本文由杭电网友曾凯峰根据小梅哥FPGA IIC协议基本概念公开课内容整理并最终编写Verilog代码实现使用串口读写EEPROM的功能. 以下为原文 ... 
- (转)写的非常好的一篇HTTP协议详解
		原文地址:http://www.cnblogs.com/TankXiao/archive/2012/02/13/2342672.html#3931004(推荐看原文,因为原文文章末有更详细链接) 当今 ... 
- TCP /IP协议详解【转】
		转自:https://www.jianshu.com/p/0cf648510bce?utm_campaign=maleskine&utm_content=note&utm_medium ... 
随机推荐
- Python 查找算法_众里寻他千百度,蓦然回首那人却在灯火阑珊处(线性、二分,分块、插值查找算法)
			查找算法是用来检索序列数据(群体)中是否存在给定的数据(关键字),常用查找算法有: 线性查找: 线性查找也称为顺序查找,用于在无序数列中查找. 二分查找: 二分查找也称为折半查找,其算法用于有序数列. ... 
- 【笔记】排查CPU占用过高
			本文是该教程视频的笔记 https://www.bilibili.com/video/BV15T4y1y7eH 1. 问题演示 将演示项目打包放到服务器运行 执行 curl http://localh ... 
- windows下的操作
			1.java -jar启动war包 将打好的war包丢到tomcat的webapps目录,然后进入tomcat的bin目录双击运行startup.bat会自动解压war包,在浏览器直接可访问web项目 
- 在jupyterlab中实现实时协同功能
			1 简介 当你在使用jupyter时,有没有想象过如果我们可以把正在编写代码的jupyter界面共享给其他人,使得别人可以在其他地方实时看到与你同步的jupyter界面,这样一来无论是与他人沟通代码逻 ... 
- 数据交换格式 JSON
			1. 什么是 JSON 概念 : JSON 的英文全称是 JavaScript ObjEct Notation, 即 "JavaScript 对象表示法" . 简单来讲 : JSO ... 
- 《计算机组成原理/CSAPP》网课总结(一)
			现在是2022年4月17日晚10点,本月计划的网课<csapp讲解>视频课看到了第八章"异常"第三讲,视频讲的很好但更新很慢,暂时没有最新的讲解,所以先做一个简单总结. ... 
- jQuery.extend()[$.extend()]
			jQuery.extend(target, object1[,objectN]);jQuery.extend([deep], target, object1[,objectN]);jQuery.ext ... 
- victoriaMetrics无法获取抓取target的问题
			victoriaMetrics无法获取抓取target的问题 问题描述 最近在新环境中部署了一个服务,其暴露的指标路径为:10299/metrics,配置文件如下(名称字段有修改): apiVersi ... 
- opencv学习之边缘检测
			边缘检测 是图像处理 过程中经常会涉及到的一个环节.而在计算机视觉 和 机器学习领域,边缘检测 用于 特征提取 和 特征检测 效果也是特别明显.而 openCV 中进行边缘检测的 算法 真是五花八门, ... 
- unity---UI管理模块
			UI管理器 任务: 1.所有面板的父类,2.UIMgr 所有UI控件都继承UIBehaviour 面板基类 找到相应空间 简化后 也存在问题:一个物体可以同时挂载两个组件 导致键相同,而值不同, 将值 ... 
