简介

在后面的章节中,咱们解说了kqueue的应用和原理,接下来咱们再看一下epoll的应用。两者都是更加高级的IO形式,都须要借助native的办法实现,不同的是Kqueue用在mac零碎中,而epoll用在liunx零碎中。

epoll的具体应用

epoll的应用也很简略,咱们还是以罕用的聊天室为例来解说epoll的应用。

对于server端来说须要创立bossGroup和workerGroup,在NIO中这两个group是NIOEventLoopGroup,在epoll中则须要应用EpollEventLoopGroup:

        EventLoopGroup bossGroup = new EpollEventLoopGroup(1);        EventLoopGroup workerGroup = new EpollEventLoopGroup();

接着须要将bossGroup和workerGroup传入到ServerBootstrap中:

ServerBootstrap b = new ServerBootstrap();            b.group(bossGroup, workerGroup)             .channel(EpollServerSocketChannel.class)             .handler(new LoggingHandler(LogLevel.INFO))             .childHandler(new NativeChatServerInitializer());

留神,这里传入的channel是EpollServerSocketChannel,专门用来解决epoll的申请。其余的局部和一般的NIO服务是一样的。

接下来看下epoll的客户端,对于客户端来说须要创立一个EventLoopGroup,这里应用的是EpollEventLoopGroup:

EventLoopGroup group = new EpollEventLoopGroup();

而后将这个group传入Bootstrap中去:

Bootstrap b = new Bootstrap();            b.group(group)             .channel(EpollSocketChannel.class)             .handler(new NativeChatClientInitializer());

这里应用的channel是EpollSocketChannel,是和EpollServerSocketChannel对应的客户端的channel。

EpollEventLoopGroup

先看下EpollEventLoopGroup的定义:

public final class EpollEventLoopGroup extends MultithreadEventLoopGroup 

和KqueueEventLoopGroup一样,EpollEventLoopGroup也是继承自MultithreadEventLoopGroup,示意它能够开启多个线程。

在应用EpollEventLoopGroup之前,须要确保epoll相干的JNI接口都曾经筹备结束:

Epoll.ensureAvailability();

newChild办法用来生成EpollEventLoopGroup的子EventLoop:

    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 EpollEventLoop(this, executor, maxEvents,                selectStrategyFactory.newSelectStrategy(),                rejectedExecutionHandler, taskQueueFactory, tailTaskQueueFactory);    }

从办法中能够看到,newChild承受一个executor和多个额定的参数,这些参数别离是SelectStrategyFactory,RejectedExecutionHandler,taskQueueFactory和tailTaskQueueFactory,最终将这些参数传入EpollEventLoop中,返回一个新的EpollEventLoop对象。

EpollEventLoop

EpollEventLoop是由EpollEventLoopGroup通过应用new child办法来创立的。

对于EpollEventLoop自身来说,是一个SingleThreadEventLoop:

class EpollEventLoop extends SingleThreadEventLoop 

借助于native epoll IO的弱小性能,EpollEventLoop能够在单线程的状况下疾速进行业务解决,非常优良。

和EpollEventLoopGroup一样,EpollEventLoop在初始化的时候须要检测零碎是否反对epoll:

    static {        Epoll.ensureAvailability();    }

在EpollEventLoopGroup调用的EpollEventLoop的构造函数中,初始化了三个FileDescriptor,别离是epollFd,eventFd和timerFd,这三个FileDescriptor都是调用Native办法创立的:

this.epollFd = epollFd = Native.newEpollCreate();this.eventFd = eventFd = Native.newEventFd();this.timerFd = timerFd = Native.newTimerFd();

而后调用Native.epollCtlAdd建设FileDescriptor之间的关联关系:

Native.epollCtlAdd(epollFd.intValue(), eventFd.intValue(), Native.EPOLLIN | Native.EPOLLET);Native.epollCtlAdd(epollFd.intValue(), timerFd.intValue(), Native.EPOLLIN | Native.EPOLLET);

在EpollEventLoop的run办法中,首先会调用selectStrategy.calculateStrategy办法,拿到以后的select状态,默认状况下有三个状态,别离是:

    int SELECT = -1;    int CONTINUE = -2;    int BUSY_WAIT = -3;

这三个状态咱们在kqueue中曾经介绍过了,不同的是epoll反对BUSY_WAIT状态,在BUSY_WAIT状态下,会去调用Native.epollBusyWait(epollFd, events)办法返回busy wait的event个数。

如果是在select状态下,则会去调用Native.epollWait(epollFd, events, 1000)办法返回wait状态下的event个数。

接下来会别离调用processReady(events, strategy)runAllTasks办法,进行event的ready状态回调解决和最终的工作执行。

EpollServerSocketChannel

先看下EpollServerSocketChannel的定义:

public final class EpollServerSocketChannel extends AbstractEpollServerChannel implements ServerSocketChannel

EpollServerSocketChannel继承自AbstractEpollServerChannel并且实现了ServerSocketChannel接口。

EpollServerSocketChannel的构造函数须要传入一个LinuxSocket:

    EpollServerSocketChannel(LinuxSocket fd) {        super(fd);        config = new EpollServerSocketChannelConfig(this);    }

LinuxSocket是一个非凡的socket,用来解决和linux的native socket连贯。

EpollServerSocketChannelConfig是构建EpollServerSocketChannel的配置,这里用到了4个配置选项,别离是SO_REUSEPORT,IP_FREEBIND,IP_TRANSPARENT,TCP_DEFER_ACCEPT和TCP_MD5SIG。每个配置项都对应着网络协议的特定含意。

咱们再看一下EpollServerSocketChannel的newChildChannel办法:

    protected Channel newChildChannel(int fd, byte[] address, int offset, int len) throws Exception {        return new EpollSocketChannel(this, new LinuxSocket(fd), address(address, offset, len));    }

newChildChannel和KqueueServerSocketChannel办法一样,也是返回一个EpollSocketChannel,并且将传入的fd结构成为LinuxSocket。

EpollSocketChannel

EpollSocketChannel是由EpollServerSocketChannel创立返回的,先来看下EpollSocketChannel的定义:

public final class EpollSocketChannel extends AbstractEpollStreamChannel implements SocketChannel {

能够看到EpollSocketChannel继承自AbstractEpollStreamChannel,并且实现了SocketChannel接口。

回到之前EpollServerSocketChannel创立EpollSocketChannel时调用的newChildChannel办法,这个办法会调用EpollSocketChannel的构造函数如下所示:

    EpollSocketChannel(Channel parent, LinuxSocket fd, InetSocketAddress remoteAddress) {        super(parent, fd, remoteAddress);        config = new EpollSocketChannelConfig(this);        if (parent instanceof EpollServerSocketChannel) {            tcpMd5SigAddresses = ((EpollServerSocketChannel) parent).tcpMd5SigAddresses();        }    }

从代码的逻辑能够看到,如果EpollSocketChannel是从EpollServerSocketChannel创立进去的话,那么默认会开启tcpMd5Sig的个性。

什么是tcpMd5Sig呢?

简略点说,tcpMd5Sig就是在TCP的数据报文中增加了MD5 sig,用来进行数据的校验,从而提醒数据传输的安全性。

TCP MD5是在RFC 2385中提出的,并且只在linux内核中能力开启,也就是说如果你想应用tcpMd5Sig,那么必须应用EpollServerSocketChannel和EpollSocketChannel。

所以如果是谋求性能或者非凡应用场景的敌人,须要接触这种native transport的时候还是很多的,能够认真钻研其中的配置选项。

再看一下EpollSocketChannel中十分重要的doConnect0办法:

    boolean doConnect0(SocketAddress remote) throws Exception {        if (IS_SUPPORTING_TCP_FASTOPEN_CLIENT && config.isTcpFastOpenConnect()) {            ChannelOutboundBuffer outbound = unsafe().outboundBuffer();            outbound.addFlush();            Object curr;            if ((curr = outbound.current()) instanceof ByteBuf) {                ByteBuf initialData = (ByteBuf) curr;                long localFlushedAmount = doWriteOrSendBytes(                        initialData, (InetSocketAddress) remote, true);                if (localFlushedAmount > 0) {                    outbound.removeBytes(localFlushedAmount);                    return true;                }            }        }        return super.doConnect0(remote);    }

在这个办法中会首先判断是否开启了TcpFastOpen选项,如果开启了该选项,那么最终会调用LinuxSocket的write或者sendTo办法,这些办法能够增加初始数据,能够在建设连贯的同时传递数据,从而达到Tcp fast open的成果。

如果不是tcp fast open,那么须要调用Socket的connect办法去建设传统的连贯。

总结

epoll在netty中的实现和kqueue很相似,他们的不同在于运行的平台和具体的性能参数,如果谋求高性能的敌人能够深入研究。

本文的代码,大家能够参考:

learn-netty4

更多内容请参考 http://www.flydean.com/53-2-netty-epoll-transport/

最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!

欢送关注我的公众号:「程序那些事」,懂技术,更懂你!