利用epoll写一个"迷你"的网络事件库
epoll是linux下高性能的IO复用技术,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
为什么会出现IO复用技术呢,比如在Web应用中,大量的请求连接事件,如果采用多进程方式处理,也就是一个连接对应一个fork来处理,这样开销太大了,毕竟创建进程还是很耗资源的;如果采用多线程方式处理,也就是一个连接对应一个线程来处理,当请求并发量上去的话,系统中就会充斥着很多处理线程,毕竟一个系统创建线程是有一定上限的。这时,就需要我们的IO复用技术了。常见的网络模型中,有多进程+IO复用编程模型,也有多线程+IO复用编程模型,比如大名鼎鼎的nginx默认采用的就是多进程+IO复用技术来处理网络请求的;开源网络库libevent也是基于IO复用技术来完成网络数据处理的。
epoll系列函数
epoll是Linux特有的IO复用函数,它在实现和使用上与select和poll有很大差异,首先,epoll使用一组函数来完成操作,而不是单个函数。其次,epoll把用户关心的文件描述符上的事件放在内核上的一个事件表中,从而无须像select和poll那样每次调用都要重复传入文件描述符集合事件表。但epoll需要使用一个额外的文件描述符,来唯一标识内核中这个事件表,这个文件描述符使用如下epoll_create函数创建:
#include <sys/epoll.h>
int epoll_create(int size); // 返回:成功返回创建的内核事件表对应的描述符,出错-1
size参数现在并不起作用,只是给内核一个提示,告诉它内核表需要多大,该函数返回的文件描述符将用作其他所有epoll函数的第一个参数,以指定要访问的内核事件表。用epoll_ctl函数操作内核事件表
#include <sys/epoll.h>
int epoll_ctl(int opfd, int op, int fd, struct epoll_event *event); // 返回:成功返回创建的内核事件表对应的描述符,出错-1
fd参数是要操作的文件描述符,op指定操作类型,操作类型有3种
- EPOLL_CTL_ADD:往事件表中注册fd上的事件
- EPOLL_CTL_MOD:修改fd上的注册事件
- EPOLL_CTL_DEL:删除fd上的注册事件
event指定事件类型,它是epoll_event结构指针类型:
struct epoll_event
{
__uint32_t events; /* epoll事件 */
epoll_data_t data; /* 用户数据 */
};
其中events描述事件类型,epoll支持的事件类型和poll基本相同,表示epoll事件类型的宏是在poll对应的宏加上”E”,比如epoll的数据可读事件是EPOLLIN,但epoll有两个额外的事件类型-EPOLLET和EPOLLONESHOT,它们对于高效运作非常关键,data用于存储用户数据,其类型epoll_data_t定义如下:
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
epoll_data_t是一个联合体,其4个成员最多使用的是fd,它指定事件所从属的目标文件描述符,ptr成员可用来指定fd相关的用户数据,但由于opoll_data_t是一个联合体,我们不能同时使用fd和ptr,如果要将文件描述符嗯哼用户数据关联起来,以实现快速的数据访问,则只能使用其他手段,比如放弃使用fd成员,而在ptr指针指向的用户数据中包含fd。
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); // 返回:成功返回就绪的文件描述符个数,出错-1
timeout参数的含义与poll接口的timeout参数相同,maxevents参数指定最多监听多少个事件,它必须大于0。
epoll_wait如果检测到事件,就将所有就绪的事件从内核事件表(由epfd指定)中复制到events指定的数组中,这个数组只用来输epoll_wait检测到的就绪事件,而不像select和poll的参数数组既传递用于用户注册的事件,有用于输出内核检测到就绪事件,这样极大提高了应用程序索引就绪文件描述符的效率。

epoll原理与实现
epoll是怎么实现的呢?其实很简单,从这3个方法就可以看出,它比select聪明的避免了每次频繁调用“哪些连接已经处在消息准备好阶段”的 epoll_wait时,是不需要把所有待监控连接传入的。这意味着,它在内核态维护了一个数据结构保存着所有待监控的连接。这个数据结构就是一棵红黑树,它的结点的增加、减少是通过epoll_ctrl来完成的。

图片来源于CSDN:高性能网络编程5--IO复用与并发编程
图中左下方的红黑树由所有待监控的连接构成。左上方的链表,同是目前所有活跃的连接。于是,epoll_wait执行时只是检查左上方的链表,并返回左上方链表中的连接给用户。这样,epoll_wait的执行效率能不高吗?
基于epoll的"迷你"网络事件库
网络事件库封装了底层IO复用函数,同时提供给外部使用的接口,提供的接口可以多种多样,但是一般有添加事件、删除事件、开始事件循环等接口。为了展示下网络事件库的是如何封装IO复用函数,同时学习epoll的使用,"迷你"网络事件库-tomevent今天诞生了 :) (ps:tomevent采用C++语言实现)。
既然是网络事件库,那首先需要定义一个事件的结构,LZ这里就使用Event结构体了,事件结构体中包含监听的文件描述符、事件类型、回调函数、传递给回调函数的参数,当然,这只是一个简单的事件结构,如果还需要其他信息可另外添加。
/**
* event struct.
*/
struct Event {
int fd; /* the fd want to monitor */
short event; /* the event you want to monitor */
void *(*callback)(int fd, void *arg); /* the callback function */
void *arg; /* the parameter of callback function */
};
定义一个事件处理接口IEvent,该接口定义了3个基本的事件操作函数,也就是添加事件、删除事件、开始事件循环。定义IEvent接口,与具体的底层IO技术解耦,使用具体的IO复用类来实现该接口,比如对应select的SelectEvent,或者是对应poll的PollEvent,当然,这里就用epoll对应的EpollEvent来实现IEvent接口(ps:c++中接口貌似应该称为抽象类,不过这里称为接口更合适一点)。
/**
* the interface of event.
*/
class IEvent {
public:
virtual int addEvent(const Event &event) = ;
virtual int delEvent(const Event &event) = ;
virtual int dispatcher() = ; virtual ~IEvent() { }
};
IEvent的实现类EpollEvent,其中封装了epoll相关的函数。EpollEvent有3个成员,分别是pollCreateSize、epollFd、events,pollCreateSize表示调用epoll_create时传递的参数值,epollFd表示epoll_create的返回值,events是记录事件的map,events中记录了监听事件的信息,当事件来临时被用到。
class EpollEvent : public IEvent {
public:
EpollEvent() : EpollEvent() {
}
EpollEvent(int createSize) {
if (createSize < ) {
createSize = ;
}
epollCreateSize = createSize;
initEvent();
}
virtual int addEvent(const Event &event);
virtual int delEvent(const Event &event);
virtual int dispatcher();
private:
int initEvent() {
int epollFd = epoll_create(this->epollCreateSize);
if (epollFd <= ) {
perror("create_create error:");
return epollFd; /* here epollFd is -1 */
}
this->epollFd = epollFd;return ;
}
int epollCreateSize;
int epollFd;
//Event event;
map<int, Event> events;
};
int EpollEvent::addEvent(const Event &event) {
struct epoll_event epollEvent;
epollEvent.data.fd = event.fd;
epollEvent.events = event.event;
int retCode = epoll_ctl(this->epollFd, EPOLL_CTL_ADD, event.fd, &epollEvent);
if (retCode < ) {
perror("epoll_ctl error:");
return retCode;
}
/* add event to this->events */
this->events[event.fd] = event;return ;
}
int EpollEvent::delEvent(const Event &event) {
struct epoll_event epollEvent;
epollEvent.data.fd = event.fd;
epollEvent.events = event.event;
int retCode = epoll_ctl(this->epollFd, EPOLL_CTL_DEL, event.fd, &epollEvent);
if (retCode < ) {
perror("epoll_ctl error:");
return retCode;
}
this->events.erase(event.fd);
return ;
}
int EpollEvent::dispatcher() {
struct epoll_event epollEvents[];
//cout << "epoll_wait before" << endl;
int nEvents = epoll_wait(epollFd, epollEvents, , -);
if (nEvents <= ) {
perror("epoll_wait error:");
return -;
}
//cout << "epoll_wait after nEvent" << endl;
for (int i = ; i < nEvents; i++) {
int fd = epollEvents[i].data.fd;
Event event = this->events[fd];
if (event.callback) {
event.callback(fd, event.arg);
}
}
return ;
}
到这里整个tomevent的框架代码就结束了,那么该如何使用呢,以下是一个测试用例。使用tomevent来同时监听2个文件描述符,一个是标准输入(fd为0),另一个是提供UDP服务的一个文件描述符。
void *test(int fd, void *arg) {
cout << "****************test(): fd=" << fd << endl;
char buff[];
int len = recvfrom(fd, buff, sizeof(buff), , NULL, NULL);
if (len > ) {
buff[len] = '\0';
cout << buff << endl;
}
else {
perror("recvfrom error:");
}
cout << "****************test()**********" << endl;
}
void *inTest(int fd, void *arg) {
cout << "****************inTest(): fd=" << fd << endl;
char buff[];
int len = read(fd, buff, sizeof(buff));
if (len > ) {
buff[len] = '\0';
cout << buff << endl;
}
else {
perror("read stdin error:");
}
cout << "****************inTest()**********" << endl;
}
int main(int argc, char **argv) {
int listenFd = -;
int connFd = -;
struct sockaddr_in servAddr;
listenFd = socket(AF_INET, SOCK_DGRAM, );
memset(&servAddr, , sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons();
servAddr.sin_addr.s_addr = INADDR_ANY;
bind(listenFd, (struct sockaddr *)&servAddr, sizeof(servAddr));
listen(listenFd, );
Event event, inEvent;
EpollEvent eventBase;
event.fd = listenFd;
event.event = EPOLLIN;
event.arg = NULL;
event.callback = test;
inEvent.fd = ;
inEvent.event = EPOLLIN;
inEvent.arg = NULL;
inEvent.callback = inTest;
eventBase.addEvent(event);
eventBase.addEvent(inEvent);
for (; ;) {
eventBase.dispatcher();
}
return ;
}
以下是测试结果 ,同时提供UDP服务和响应键盘输入。

参考
利用epoll写一个"迷你"的网络事件库的更多相关文章
- 学了C语言,如何利用cURL写一个程序验证某个网址的有效性?
在<C程序设计伴侣>以及这几篇关于cURL的文章中,我们介绍了如何利用cURL写一个下载程序,从网络下载文件.可是当我们在用这个程序下载文件时,又遇到了新问题:如果这个网址是无效的,那么我 ...
- 学了C语言,如何利用CURL写一个下载程序?—用nmake编译CURL并安装
在这一系列的前一篇文章学了C语言,如何为下载狂人写一个磁盘剩余容量监控程序?中,我们为下载狂人写了一个程序来监视磁盘的剩余容量,防止下载的东西撑爆了硬盘.可是,这两天,他又抱怨他的下载程序不好用,让我 ...
- 写一个迷你版Smarty模板引擎,对认识模板引擎原理非常好(附代码)
前些时间在看创智博客韩顺平的Smarty模板引擎教程,再结合自己跟李炎恢第二季开发中CMS系统写的tpl模板引擎.今天就写一个迷你版的Smarty引擎,虽然说我并没有深入分析过Smarty的源码,但是 ...
- 如何写一个跨浏览器的事件处理程序 js
如何 写一个合格的事件处理程序,看如下代码: EventUtil可以直接拿去用 不谢 <!DOCTYPE html> <html> <head> <title ...
- 利用Python写一个抽奖程序,解密游戏内抽奖的秘密
前言 本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者: 极客挖掘机 PS:如有需要Python学习资料的小伙伴可以加点击下 ...
- 利用 Python 写一个颜值测试小工具
我们知道现在有一些利用照片来测试颜值的网站或软件,其实使用 Python 就可以实现这一功能,本文我们使用 Python 来写一个颜值测试小工具. 很多人学习python,不知道从何学起.很多人学习p ...
- 利用反射--调用一个按钮的Click事件
最基本的调用方法 (1)button1.PerformClick();(2)button1_Click(null,null);(3)button_Click(null,new EventArgs()) ...
- python利用socket写一个文件上传
1.先将一张图片拖入‘文件上传’的目录下,利用socket把这张图片写到叫‘yuan’的文件中 2.代码: #模拟服务端 import subprocess import os import sock ...
- 利用jmSlip写一个移动端顶部日历选择组件
可滚动选日期,并限制哪些日期可选和不可选. 主要用来根据后台返回生成一个日期选择器. 具体实现可关注jmslip: https://github.com/jiamao/jmSlip 示例:http:/ ...
随机推荐
- 纯css3手机页面图标样式代码
全部图标:http://hovertree.com/texiao/css/19/ 先看效果: 或者点这里:http://hovertree.com/texiao/css/19/hoverkico.ht ...
- SQL Server 中 EXEC 与 SP_EXECUTESQL 的区别
SQL Server 中 EXEC 与 SP_EXECUTESQL 的区别 MSSQL为我们提供了两种动态执行SQL语句的命令,分别是 EXEC 和 SP_EXECUTESQL ,我们先来看一下两种方 ...
- PetaPoco4.0的事务为什么不会回滚
using (var srop=DbHelper.CurrentDb.GetTransaction()) { ID = bp.AddModel(model).ToStr(); #region 参与楼盘 ...
- 微信扫码支付~官方DEMO的坑~参数不能自定义
返回目录 由于微信在校验参数时采用了“微信服务端”校验,它的参数是前期定义好的,所以用户不能自己添加自定义的参数,你可以把参数写在Attach字段时,作为它的附加参数. 参数和返回值定义如下: pub ...
- php实现设计模式之 简单工厂模式
作为对象的创建模式,用工厂方法代替new操作. 简单工厂模式是属于创建型模式,又叫做静态工厂方法模式,但不属于23种GOF设计模式之一.简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例. 工厂 ...
- (原)3.1 Zookeeper应用 - Master选举
本文为原创文章,转载请注明出处,谢谢 Master 选举 1.原理 服务器争抢创建标志为Master的临时节点 服务器监听标志为Master的临时节点,当监测到节点删除事件后展开新的一轮争抢 某个服务 ...
- MVC的增删改查
基本都要使用C控制器中的两个action来完成操作,一个用于从主界面跳转到新页面.同时将所需操作的数据传到新界面,另一个则对应新界面的按钮,用于完成操作.将数据传回主界面以及跳转回主界面.根据不同情况 ...
- TouchPoint.js – 可视化展示 HTML 原型点击效果
TouchPoint.js 是一个用于 HTML 原型展示的 JavaScript 库(作为UX过程的一部分),通过视觉表现用户在屏幕上的点击.TouchPoint 是高度可定制,非常适合屏幕录制,用 ...
- 低调奢华 CSS3 transform-style 3D旋转
点击这里查看效果:http://keleyi.com/a/bjad/s89uo4t1.htm 效果图: CSS3 transform-style 属性 以下是代码: <!DOCTYPE html ...
- MSSQL 分页
使用数据库分页返回用户数据有如下好处:1.减少服务器磁盘系统地读取压力2.减少网络流量,减轻网络压力3.减轻客户端显示数据的压力4.提高处理效率. 一般而言分页处理分为两种:应用程序中的分页(查询出所 ...