下篇针对一些特定问题进行分析,主要分析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 */
};
它分为events
和data
两个部分
events
是epoll注册的事件,比如EPOLLIN
、EPOLLOUT
等等,这个参数在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}}