目录
1.libevent
Libevent 是一个用C语言编写的、轻量级的开源高性能事件通知库,主要有以下几个亮点:事件驱动( event-driven),高性能;轻量级,专注于网络,不如 ACE 那么臃肿庞大;源代码相当精炼、易读;跨平台,支持 Windows、 Linux、 *BSD 和 Mac Os;支持多种 I/O 多路复用技术, epoll、 poll、 dev/poll、 select 和 kqueue 等;支持 I/O,定时器和信号等事件;注册事件优先级。(截自百度百科)
libevent官网:libevent
1.1 事件驱动
这里我不做详细介绍,事件驱动大家大致可以理解为:监听很多事件(event),当事件触发(event-active),程序执行相应的方法来针对事件做处理。
2.背景
在公司进行网络开发,接到开发一个高并发的拨号客户端任务,由于当时知识有限,最后开发出PPPoE多播的客户端中是以一次拨号,就创建一个线程来监听、维护套接字,这是由于PPPoE中链接成功后,收到的报文对于网卡来说是全部都接收的,作为客户端需要做到能进行针对用户来转发,而我使用的是一个线程一个参数,以线程当前栈的参数来进行转发逻辑。这样是可以走通的,但是也带了一个问题:当连接数量大的时候程序由于开辟了很多线程,则会造成大量内存占用。这对公司业务环境来说是不允许的。好在我的任务不是很急,所以我有时间研究新的办法..经过几天的思考,我构思了一套事件驱动的模型来替代原来的以维护线程来维护连接的这种结构。
3.示例
这里简单做几个替换的示例(伪代码)供大家学习参考,当然我不可能拿公司的项目来给大家参考(汗!),由于文章是在CSDN现写的,我们也是进行框架学习,不注重运行结果,因此我在这里使用伪代码来供简单的框架学习。
以下是一个多线程来维护的结构:
// 多线程来实现的结构
// 示例:监听一个udp端口,收到一次认证消息,创造一个连接
void *callbackfunc(void *arg)
{
Connection *conn = (Connection *)arg;
//这里进行逻辑处理
int fd_user = socket(..);
while(1)
{
if (recv(fd_user, .., ..) < 0)
{
perror();
exit(-1);
} else {
//这里进行连接维护
}
}
}
while(1)
{
if (recv(fd, buffer, ..) < 0)
{
perror();
exit(-1);
} else {
pthread_create(ph, .., callbackfunc, buffer);
pthread_detach(ph);
}
}
想用事件驱动模型替代这个结构,我们需要构思好,我们在哪进行事件驱动...稍微构思一下,我们可以发现,在回调函数中的while(1)其实是可以用事件驱动来替代的,我们的事件就是套接字是否有可读的消息,我们的驱动就是当事件活跃时我们要执行的方法,这样我们就可以进行改写了!
// 事件驱动来实现的结构
// 示例:监听一个udp端口,收到一次认证消息,进行一次连接
void linkcallbackfunc(int fd, .., void *arg)
{
// 在这里进行连接维护的方法
}
void *callbackfunc(void *arg)
{
MSG *msg = (Connection *)arg;
//这里进行逻辑处理
int fd_user = socket(..);
struct event* ev = event_new(msg.base, linkcallbackfunc, msg.buffer);
event_add(ev);
}
typedef struct MSG_STR{
struct_base *base;
char *arg;
}MSG;
void main(int argc, char **argv)
{
MSG msg;
memset(&msg, 0, sizeof(msg));
struct event_base* base = event_base_new();
while (1)
{
if (recv(fd, buffer, ..) < 0)
{
perror();
exit(-1);
} else {
msg.arg = buffer;
msg.base = base;
pthread_create(ph, .., callbackfunc, msg);
pthread_detach(ph);
}
}
}
简而言之,就是把维护的while(1){recv(fd)}改写为把fd作为事件存放到event_base中,这是最基础的用法!这样做,就没有一直存在的线程了,而是交给epoll来做,大家觉得这样可以吗........当然不行!
这样有一个很严重的问题,当多个线程来操作同一个event_base会引发严重的惊群效应,也就是说虽然我们的框架是OK的,但是这样是大有问题的,会造成资源的严重浪费,加锁也没用!
参考:libevent源码分析9-libevent的多线程编程问题 - 知乎
那有什么办法解决呢?用时三十分钟我想出来了一个非常好的办法,那就是使用定时器和消息队列!我们创建一个每T毫秒的定时器事件,每Tms从消息队列取消息,而消息队列中的消息就是从线程发出的,这样就解决了多线程操作event_base的问题,是不是很帅呢...这就是编程有趣的地方,用“筷子”解决不了的问题,把“筷子”和“勺子”合在一起就可以解决了!
示例如下:
// 事件驱动来实现的结构
// 示例:监听一个udp端口,收到一次认证消息,进行一次连接
void linkcallbackfunc(int fd, .., void *arg)
{
// 在这里进行连接维护的方法
}
void *callbackfunc(void *arg)
{
MSG *msg = (Connection *)arg;
//这里进行逻辑处理
int fd_user = socket(..);
//这里我们需要把buffer是一个消息结构体,根据需求定义
struct event* ev = event_new(msg.base, fd_user,linkcallbackfunc, msg.buffer);
mq_send("/queue", buffer);
}
void timercallback(int fd, .., void *arg)
{
struct event_base *base = (struct event_base *)arg;
mq_recv("/queue");
struct event* ev = event_new(base, buffer.fd_user, linkcallbackfunc, ..);
event_add(ev);
}
typedef struct MSG_STR{
struct_base *base;
char *arg;
}MSG;
void main(int argc, char **argv)
{
MSG msg;
memset(&msg, 0, sizeof(msg));
struct event_base* base = event_base_new();
Time tv;
tv.usec = 20000;
struct event *timer = event_new(base, -1, .., timercallback, base);
event_add(timer, &tv);
mq_unlink("/queue");
mq_create("/queue", .., ..);
while (1)
{
if (recv(fd, buffer, ..) < 0)
{
perror();
exit(-1);
} else {
msg.arg = buffer;
msg.base = base;
pthread_create(ph, .., callbackfunc, msg);
pthread_detach(ph);
}
}
}
这样我们就实现了高并发的libevent!用多线程来操作同一个event_base,在我的项目的环境中,使用第一版的PPPoE客户端,每个连接大概消耗了7MB的内存,这是不可接受的,想象一个学校6000个拨号,就会占用42G的内存占用,这是非常吓人的,使用了新的基于libevent的模型,不管多少个连接,我都只占用了大概5MB的内存,这是非常可观的,由此可见,在未来的网络编程中,或者多线程阻塞IO中我们都可以改写为使用事件驱动来进行优化。答应我,看完这篇文章,以后能别用while(1){recv(fd)},就别用了!