下篇针对一些特定问题进行分析,主要分析epoll_event结构体,并给出了两个实例。

1. epoll_event结构体分析

上篇我们讲到了epoll_wait函数的功能。

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

这个函数只能获取是否有注册事件发生,至于说事件的细节,我们并不清楚。好比一个人在山洞中,只能听到声响,至于这个声响从何发出并不清楚。而这些关键信息就存储在epoll_event结构中

结构体如下所示:

typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;

struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};

它分为eventsdata两个部分

  • events是epoll注册的事件,比如EPOLLINEPOLLOUT等等,这个参数在epoll_ctl注册事件时,可以明确告知注册事件的类型。
  • data是一个联合体,用于传递参数。

2. epoll_event使用实例

2.1 实例1:服务器侦听客户端连接

这个例子很棒的展示了epoll_data中的int fd该怎么用。先看下面一段代码:

//创建socket
nSocketListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
...
//绑定地址
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_ANY);//0.0.0.0所有地址都合法
local.sin_port = htons(TCP_PORT);
bind(nSocketListen, (struct sockaddr*) & local, sizeof(local))

//创建epoll
nListenEpoll = epoll_create(MAX_LISTEN_EVENTS);
//注册事件
struct epoll_event Ev;
memset(&Ev, 0, sizeof(epoll_event));
Ev.events= EPOLLIN | EPOLLET 
Ev.data.fd = nSocketListen;

epoll_ctl(nListenEpoll, EPOLL_CTL_ADD, nSocketListen, &Ev);
//侦听
int nFdNumber = epoll_wait(nListenEpoll, lpListenEvents, MAX_LISTEN_EVENTS, -1);
//处理侦听结果
for (int i = 0; i < nFdNumber; i++)
{
    if (lpListenEvents[i].data.fd != nSocketListen) continue;
    ...
}

这段代码在网上很常见,作用是建立一个服务器,侦听所有客户端的连接。具体过程是先建立了一个socket,地址设为设为0.0.0.0(所有人都可以连接),然后将这个socket的句柄nSocketListen附加在注册事件Ev.data.fd上。在wait等到结果后做一个判断,看看接收到和预设的是否一致。上篇的demo也用到了类似的思路。

if (lpListenEvents[i].data.fd != nSocketListen) 
    continue;

虽然这段代码用到了fd,但他并没有体现出fd的作用!整个程序仅仅设置并注册了一个socket来连接所有IP地址htonl(INADDR_ANY);wait收到的消息必然来自于这个唯一的socket,所以这句判断根本是多此一举

正确的用法是:创建多个socket来管理不同的字段,比如:

Socket句柄 管理的IP范围
101 100-120
102 121-191
103 192-255

将这三个socket都注册进epoll里面,当wait到来时,我们就可以根据Ev.data.fd传进来的socket句柄来进行处理。

比如上午8点到10点这个时间段,服务器只允许100-120范围的IP连接进来,就可以做一个判断if (lpListenEvents[i].data.fd == 101),如果是再接受连接。

这个例子中,fd传递了socket的句柄,帮助我们管理不同的网络连接。

2.2 实例2:进程间通信

epoll常常用于线程间的协同工作。

//线程A代码
struct epoll_event Ev;
memset(&Ev, 0, sizeof(Ev));
Ev.events= EPOLLOUT | EPOLLET | EPOLLERR | EPOLLHUP
Ev.data.ptr = lpCatList;

epoll_ctl(iClientEpoll, EPOLL_CTL_ADD, lpCatList->nClientSocket, &Ev);
//线程B代码
int nFdNumber = epoll_wait(iClientEpoll, lpEvent, MAX_CLIENT_EVENTS, -1);

IOPACKHEAD_LIST* RelpCatList = (IOPACKHEAD_LIST*)lpEvent[i].data.ptr;

线程AB都是服务器上的线程。

线程A功能相当于接线员,跟前面展示的服务器功能相同:监听客户的连接,accept客户的请求,建立客户与服务器间的socket连接通道(此处的建立的socket句柄为nClientSocket)。然后将这些客户连接注册到iClientEpoll

这些通道建立后,客户一般不会时刻收发数据,也就是说客户可能不定时的使用为他们建立的socket连接通道,线程B的iClientEpoll就是用来监听有没有已经建立连接的客户需要收发数据的。

如果像上一个例子一样,只用Ev.data.fd传一个客户socket的句柄,这样线程B能得到的信息太少了。所以我们需要使用结构体lpCatList来传参。

lpCatList相当于一个令牌,他是一个指针,指向的地址存储了客户的信息(Socket句柄,IP地址,MAC地址,请求时间等等),A线程在接收客户连接后,将他们写到这个令牌中,一并注册到iClientEpoll。B线程就可以利用Ev.data.ptr包含的重要的地址信息。

这样ptr就相当于一个小纸条,A线程通过iClientEpoll将这个小纸条交到B线程手中,B线程就能了解A线程的信息,实现了线程间的通信。

下面我们打印一下线程A的lpCatlist

(gdb) p lpCatList
$18 = (IOPACKHEAD_LIST *) 0x7ffff0001120

再打印一下线程B的ptr,可以发现他们指向同一个地址0x7ffff0001120,说明参数成功传递

(gdb) p lpEvent[0]
$14 = {events = 4, data = {ptr = 0x7ffff0001120, fd = -268431072, u32 = 4026536224, 
    u64 = 140737219924256}}