关于netty:快来体验快速通道netty中epoll传输协议详解

46次阅读

共计 6177 个字符,预计需要花费 16 分钟才能阅读完成。

简介

在后面的章节中,咱们解说了 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/

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

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

正文完
 0