写点什么

netty 系列之:epoll 传输协议详解

作者:程序那些事
  • 2022 年 5 月 23 日
  • 本文字数:4136 字

    阅读完需:约 14 分钟

netty系列之:epoll传输协议详解

简介

在前面的章节中,我们讲解了 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/

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

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

发布于: 2022 年 05 月 23 日阅读数: 29
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020.06.07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
netty系列之:epoll传输协议详解_Java_程序那些事_InfoQ写作社区