epoll的使用也很简单,我们还是以常用的聊天室为例来讲解epoll的使用。
对于server端来说需要创建bossGroup和workerGroup,在NIO中这两个group是NIOEventLoopGroup,在epoll中则需要使用EpollEventLoopGroup:
接着需要将bossGroup和workerGroup传入到ServerBootstrap中:
注意,这里传入的channel是EpollServerSocketChannel,专门用来处理epoll的请求。其他的部分和普通的NIO服务是一样的。
接下来看下epoll的客户端,对于客户端来说需要创建一个EventLoopGroup,这里使用的是EpollEventLoopGroup:
然后将这个group传入Bootstrap中去:
这里使用的channel是EpollSocketChannel,是和EpollServerSocketChannel对应的客户端的channel。
先看下EpollEventLoopGroup的定义:
和KqueueEventLoopGroup一样,EpollEventLoopGroup也是继承自MultithreadEventLoopGroup,表示它可以开启多个线程。
在使用EpollEventLoopGroup之前,需要确保epoll相关的JNI接口都已经准备完毕:
newChild方法用来生成EpollEventLoopGroup的子EventLoop:
从方法中可以看到,newChild接受一个executor和多个额外的参数,这些参数分别是SelectStrategyFactory,RejectedExecutionHandler,taskQueueFactory和tailTaskQueueFactory,最终将这些参数传入EpollEventLoop中,返回一个新的EpollEventLoop对象。
EpollEventLoop是由EpollEventLoopGroup通过使用new child方法来创建的。
对于EpollEventLoop本身来说,是一个SingleThreadEventLoop:
借助于native epoll IO的强大功能,EpollEventLoop可以在单线程的情况下快速进行业务处理,十分优秀。
和EpollEventLoopGroup一样,EpollEventLoop在初始化的时候需要检测系统是否支持epoll:
在EpollEventLoopGroup调用的EpollEventLoop的构造函数中,初始化了三个FileDescriptor,分别是epollFd,eventFd和timerFd,这三个FileDescriptor都是调用Native方法创建的:
然后调用Native.epollCtlAdd建立FileDescriptor之间的关联关系:
在EpollEventLoop的run方法中,首先会调用 selectStrategy.calculateStrategy 方法,拿到当前的select状态,默认情况下有三个状态,分别是:
这三个状态我们在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继承自AbstractEpollServerChannel并且实现了ServerSocketChannel接口。
EpollServerSocketChannel的构造函数需要传入一个LinuxSocket:
LinuxSocket是一个特殊的socket,用来处理和linux的native socket连接。
EpollServerSocketChannelConfig是构建EpollServerSocketChannel的配置,这里用到了4个配置选项,分别是SO_REUSEPORT,IP_FREEBIND,IP_TRANSPARENT,TCP_DEFER_ACCEPT和TCP_MD5SIG。每个配置项都对应着网络协议的特定含义。
我们再看一下EpollServerSocketChannel的newChildChannel方法:
newChildChannel和KqueueServerSocketChannel方法一样,也是返回一个EpollSocketChannel,并且将传入的fd构造成为LinuxSocket。
EpollSocketChannel是由EpollServerSocketChannel创建返回的,先来看下EpollSocketChannel的定义:
可以看到EpollSocketChannel继承自AbstractEpollStreamChannel,并且实现了SocketChannel接口。
回到之前EpollServerSocketChannel创建EpollSocketChannel时调用的newChildChannel方法,这个方法会调用EpollSocketChannel的构造函数如下所示:
从代码的逻辑可以看到,如果EpollSocketChannel是从EpollServerSocketChannel创建出来的话,那么默认会开启tcpMd5Sig的特性。
什么是tcpMd5Sig呢?
简单点说,tcpMd5Sig就是在TCP的数据报文中添加了MD5 sig,用来进行数据的校验,从而提示数据传输的安全性。
TCP MD5是在RFC 2385中提出的,并且只在linux内核中才能开启,也就是说如果你想使用tcpMd5Sig,那么必须使用EpollServerSocketChannel和EpollSocketChannel。
所以如果是追求性能或者特殊使用场景的朋友,需要接触这种native transport的时候还是很多的,可以仔细研究其中的配置选项。
再看一下EpollSocketChannel中非常重要的doConnect0方法:
在这个方法中会首先判断是否开启了TcpFastOpen选项,如果开启了该选项,那么最终会调用LinuxSocket的write或者sendTo方法,这些方法可以添加初始数据,可以在建立连接的同时传递数据,从而达到Tcp fast open的效果。
如果不是tcp fast open,那么需要调用Socket的connect方法去建立传统的连接。
epoll在netty中的实现和kqueue很类似,他们的不同在于运行的平台和具体的功能参数,如果追求高性能的朋友可以深入研究。
本文的代码,大家可以参考:
learn-netty4
进程级的描述符表的每一条目记录了单个文件描述符的相关信息。
1. 控制文件描述符 *** 作的一组标志。(目前,此类标志仅定义了一个,即close-on-exec标志)
2. 对打开文件句柄的引用
内核对所有打开的文件的文件维护有一个系统级的描述符表格(open file description table)。有时,也称之为打开文件表(open file table),并将表格中各条目称为打开文件句柄(open file handle)。一个打开文件句柄存储了与一个打开文件相关的全部信息,如下所示:
1. 当前文件偏移量(调用read()和write()时更新,或使用lseek()直接修改)
2. 打开文件时所使用的状态标识(即,open()的flags参数)
3. 文件访问模式(如调用open()时所设置的只读模式、只写模式或读写模式)
4. 与信号驱动相关的设置
5. 对该文件i-node对象的引用
6. 文件类型(例如:常规文件、套接字或FIFO)和访问权限
7. 一个指针,指向该文件所持有的锁列表
8. 文件的各种属性,包括文件大小以及与不同类型 *** 作相关的时间戳
下图展示了文件描述符、打开的文件句柄以及i-node之间的关系,图中,两个进程拥有诸多打开的文件描述符。
Epoll - I/O event notification facility
翻译一下,epoll是一种I/O事件通知机制,这句话基本上包含了所有需要理解的要点:
epoll是一种当文件描述符的内核缓冲区非空的时候,发出可读信号进行通知,当写缓冲区不满的时候,发出可写信号通知的机制
epoll的接口非常简单,一共就三个函数:
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
首先通过 create_epoll(int maxfds) 来创建一个epoll的句柄,其中 maxfds 为你epoll所支持的最大句柄数。这个函数会返回一个新的epoll句柄,之后的所有 *** 作将通过这个句柄来进行 *** 作。在用完之后,记得用 close() 来关闭这个创建出来的epoll句柄。
之后在你的网络主循环里面,每一帧的调用 epoll_wait(int epfd, epoll_event events, int max events, int timeout) 来查询所有的网络接口,看哪一个可以读,哪一个可以写了。基本的语法为:
nfds = epoll_wait(kdpfd, events, maxevents, -1)
其中kdpfd为用 epoll_create 创建之后的句柄,events是一个 epoll_event* 的指针,当 epoll_wait 这个函数 *** 作成功之后, epoll_events 里面将储存所有的读写事件。 max_events 是当前需要监听的所有 socket 句柄数。 最后一个 timeout 是 epoll_wait 的超时,为0的时候表示马上返回,为-1的时候表示一直等下去,直到有事件范围,为任意正整数的时候表示等这么长的时间,如果一直没有事件,则范围。 一般如果网络主循环是单独的线程的话,可以用-1来等,这样可以保证一些效率,如果是和主逻辑在同一个线程的话,则可以用0来保证主循环的效率。
epoll_wait范围之后应该是一个循环,遍利所有的事件。
几乎所有的epoll程序都使用下面的框架:
下面给出一个完整的服务器端例子:
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)