前言
实习完了回家歇了一个多星期,也没闲着,在计划着做做自己的项目,由于自己主要学的是c++,所以更倾向于做网络以及服务器开发方面的项目,因此先研究了一番epoll,学习了一下libevent这个高性能事件驱动库,不过这些库学起来还是相对比较复杂的,所以打算先从epoll这个最基础的组件入手,自己慢慢写,然后看看这些库到底解决了哪些痛点
epoll介绍
这里其实介绍太多也没啥意思,简单介绍一下:epoll是Linux内核为处理大批量句柄而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著减少程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。epoll的优点有以下几个:
- 支持一个进程打开大数目的socket描述符。
- IO效率不随FD数目增加而线性下降。
- 内核支持,因此效率相对更高
epoll底层是一个红黑树和一个就绪文件描述符的链表,这里就不再细说,感兴趣的同学可以自行去了解一下,本文的目的不是介绍这个,而且要介绍底层的话又得带上源码,可能占用大量篇幅,这里就不再细说了。
epoll的接口函数
不同于之前的select和poll,epoll的接口是一组函数,下面我来简单介绍下epoll的接口函数
epoll_create
函数原型为int epoll_create(int size);
这里在较新的内核之后,size已经不再起作用,但是为了兼容性,还是要求size这个参数必须是正数,而不能是负数或者0,这个函数的作用也就是告诉内核创建一个epoll句柄,值得一提的是这个函数返回的也是一个文件描述符,因此需要注意的是在使用完之后,需要调用close来关闭这个描述符,否则可能导致文件描述符被耗尽。
返回值:返回一个正数表示创建成功,正数代表的也就是epoll对应的文件描述符,作为创建好的句柄。如果返回-1,代表创建失败,errno会相应被置为错误码。
epoll_ctl
函数原型为int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
这个函数就是主要进行操作的函数了,其中epfd就是我们在上一步中使用epoll_create获得的文件描述符,op代表操作选项,有以下三个可选项:
- EPOLL_CTL_ADD:注册新的fd到epfd中;
- EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
- EPOLL_CTL_DEL:从epfd中删除一个fd;
fd是指需要进行操作的文件描述符,epoll_event的结构如下:
1 | typedef union epoll_data { |
events可以是以下几个宏的集合:
- EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
- EPOLLOUT:表示对应的文件描述符可以写;
- EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
- EPOLLERR:表示对应的文件描述符发生错误;
- EPOLLHUP:表示对应的文件描述符被挂断;
- EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
- EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
值得说一下的是epoll_data的类型是一个union,所以也就是说实际上一个时刻,只有一种类型的数据会储存在epoll_data里面,不过这里面有个void *的指针,这个就够了,这就是很多库让用户提供回调函数,然后在用户指定的事件发生的时候,调用这个回调函数的原理,使用一个结构体储存一些必要信息,然后获得指向这个结构体的指针,强制类型转换为void *之后,传给epoll_data,再事件发生的时候,epoll会讲我们传递的这个指针再返回给我们,我们再强制类型转换回去,就能获得我们之前的数据
epoll_wait
函数原型为:int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
参数epfd是epoll的句柄,events则是一个指向数组首元素的指针,这个数组里面就是内核返回给我们的已经发生的事件集合,maxevents很简单,就是事件最大有多长,timeout的单位是毫秒,代表我们愿意等待的超时时间。
返回值:成功时,返回需要处理的事件数目。调用失败时,返回0,表示等待超时。
这里我也参考了一些教程,写了一个简单的例子,这个例子相当简陋,但是能够粗略看出如果使用epoll的接口函数,以及单纯只使用这些接口函数有什么弊端,如果要开发一个完成的库需要做哪些改进等等。代码如下,自己也只做了简单的测试,也就是冒烟通过,并没有做完整测试,所以如果有问题的话,希望看客们不吝指出,感谢!
1 |
|
代码解释
这里我使用了google的开源日志库glog,这里其实有点小题大做了,但是个人觉得c++自带的输出信息实在太少,自己又不想麻烦去写一个日志库,索性就直接用了google家的日志库,这个库本身也是非常好用的,引入头文件之后在main函数最前面简单初始化一下就ok了。
另外一个值得一提的是,我在收到来自己客户端的消息时,并没有去向epoll中添加一个监听套接字是否可写的事件,这里的原因很简单,因为实际上只要操作系统中,这个套接字的写缓冲里面还有空间,就是一直可写的,但是空间是否够大,这个就是不确定的了,所以我们需要做的其实很简单,直接向套接字调用send发送数据,如果send返回成功,我们就已经成功将数据写入了,只有当send返回失败同时errno为EAGAIN时,我们才需要向epoll添加一个监听套接字是否可写的事件,等待可写再进行写操作,说来惭愧,我这里没有做这一步的操作,因为这里只是单纯的熟悉一些epoll的一些接口,但是如果在开发一个库的时候是绝对不能这样做的。
epoll的事件触发模式有两种,默认是LT也就是水平触发,ET是可选的,我这里压根没提ET,虽然面试的时候可能经常会问这两种模式的区别,但是我个人认为实际开发中是不应该使用了ET模式的,ET模式下数据丢失带来的后果比一点点性能提高带来的好处是更大的,所以从编程方便以及实用的角度,用默认的LT模式就好,没有必要使用ET模式,因此我这里也没做深入介绍
总结
这份代码我在我的电脑上(ubuntu16.04默认环境下)成功编译通过,需要注意的是电脑上需要安装glog,编译的时候也需要加上选项-lglog,才能够编译,否则在链接动态库的那一步是无法通过的哦,通过这份代码,我们可以熟悉一下epoll接口的基本用法,了解一下单纯只使用epoll的接口的弊端,需要作出哪些改进,这个我后面会再提到。暂时了解的不是非常深入无法很好的总结提炼出来,这里就不嫌丑了。