原文链接地址: http://www.cnblogs.com/walker-lc/articles/3601100.html
在linux平台上使用c开发网络程序的同志们一般情况下都对鼎鼎大名的libevent非常的熟悉了。但是一些新进入此领域的new new people们对此都是一头雾水。原本的迷茫再加上开源软件一贯的“帮助文件”缺失作风,让我们这些新手们显的非常的无助。幸好有一些热心的朋友们帮忙,才化险为夷啊!
前几天一直在开发一个locker server,虽然公司现有的locker server能很好的运转,但是毕竟是net的,通用性不广,当我们要在linux上开发多集群系统的时候现有的locker server就未免显得有点捉襟见肘了。正是在开发locker server的过程中使用到了libevent。
总体上,libevent是很好用的。一两个函数就能搞定你复杂的网络通讯工作了。当然了,这句话得用在你使用的是“单线程”的情况下。虽然在linux系统中,进程的资源和window系统中进程的资源相比轻量级很多,代价也相当的没有那么昂贵,所以很多的软件都是使用“多进程”方式实现的,比如大名鼎鼎的apache。但是在我们的系统中,我们使用了“单进程多线程”的方式,这样,我们就能在单机上启动多个进程,以达到“伪分布式”的效果来达到测试的目的。
那么这个时候就要注意libevent的使用了,因为对于event_base来说,不是线程安全的。也就是说多线程不能share同一个event_base,就算是加锁操作也不行。那么这个时候就只能采取“单线程单event_base”的策略了。我的做法是做一个task pool(任务对象池),每个任务会被一个thread执行,当然了,thread肯定也是从thread pool拿出来的,而在task pool初始化的时候,我就给每个task中的event_base初始化了对象,这样,万事大吉了。
这个地方注意了以后,就开始说网络通讯了。在使用libevent的时候,触发事件是在接收到网络连接(或者timeout事件超时)的时候。所以你需要在事件处理函数中判断时间源,其次libevent接收网络通讯的字节流时是使用了libevnet中自带的缓冲的,所以当你接收的时候一定要注意累加,并且多次loop或者注册 event_event中的事件。所以在我的task中,会有接收的data。当然了如果你的协议是分为header和body的,通常header比较短,body比较长,而且在client,header和body通常是连续发送的,这样,在使用libevent的时候,header和body是同时被接收到的,这点一定要注意,所以提醒你在接收数据的函数中,需要区分接收header部分还是body部分;当body非常长,超过libevent的缓冲时,是需要多次多次触发接收函数的,这点也要注意,就是让你需要在接收的时候除了区分header和body以外,还要注意一次接收不完全的情况下,对于数据需要累加。
当你在使用libevent时,event_set事件时,只要不是使用EV_PERSIST注册的事件是不需要在接收完一次数据后多次event_add的,只有当你不使用EV_PERSIST时,你的事件才需要多次event_add到event_base中;当然了,使用了EV_PERSIST注册的函数在event_base被task pool回收时是要显式的event_del该注册事件的,没有使用EV_PERSIST注册的事件是不需要显式的使用event_del删除该事件的。
static void read_buffer(int client_socket_fd,short event_type,void *arg) { if(NULL == arg) { log_error("File:"__FILE__",Line:%d.event base arg is NULL.",__LINE__); return; } task_info_t *task_info = (task_info_t *) arg; if(event_type == EV_TIMEOUT) /* 这个地方注意需要判断是否超时 因为我event_add事件的时候没有使用ev_persist 所以当超时时需要再add一次事件到event_base的loop中 */ { if(0 != event_add(&task_info->on_read,&task_info->timeout)) { log_error("File:"__FILE__",Line:%d.repeart add read header event to event_base is error."); close(task_info->on_read.ev_fd); task_pool_push(task_info); } return; } int bytes; /* 这个地方就是开始接收头部 接收头部时,可能分为好几次从缓冲中取得,所以需要一个while累加 */ while(header == task_info->read_type)//recv header { bytes = recv(client_socket_fd,task_info->header_buffer+task_info->offset,REQUEST_LENGTH -task_info->offset,0); if(0 > bytes ) { if (errno == EAGAIN || errno == EWOULDBLOCK) { if(0 != event_add(&task_info->on_read, &task_info->timeout)) { close(task_info->on_read.ev_fd); task_pool_push(task_info); log_error("File: "__FILE__", line: %d, "\ "event_add fail.", __LINE__); return; } } else { log_error("File: "__FILE__", line: %d,recv failed,errno: %d, error info: %s", __LINE__, errno, strerror(errno)); close(task_info->on_read.ev_fd); task_pool_push(task_info); } return; } else if(0 == bytes) { log_warning("File:"__FILE__",Line:%d.recv buffer form network is error.disconnection the network.", __LINE__); close(task_info->on_read.ev_fd); task_pool_push(task_info); return; } if(REQUEST_LENGTH > bytes+task_info->offset) { log_warning("File:"__FILE__",Line:%d.recv header is not over.",__LINE__); task_info->offset += bytes; if(0 != event_add(&task_info->on_read, &task_info->timeout)) { close(task_info->on_read.ev_fd); task_pool_push(task_info); log_error("File: "__FILE__", line: %d, "\ "event_add fail.", __LINE__); return; } } else { task_info->read_type = body; deal_request_header(task_info); task_info->body_buffer = (char *) malloc(task_info->request_info.length); if(NULL == task_info->body_buffer) { log_error("File:"__FILE__",Line:%d.alloc mem to task_info data is error.",__LINE__); close(client_socket_fd); task_pool_push(task_info); return; } memset(task_info->body_buffer,0,task_info->request_info.length); task_info->offset = 0;//set recv body buffer offset to 0 break; } } /* 这个地方就是开始接收body, 和header一样,也要考虑body多次接收累加的情况。 */ while(body == task_info->read_type) { bytes = recv(client_socket_fd,task_info->body_buffer+task_info->offset,task_info->request_info.length-task_info->offset,0); if(0 > bytes ) { if (errno == EAGAIN || errno == EWOULDBLOCK) { if(0 != event_add(&task_info->on_read, &task_info->timeout)) { close(task_info->on_read.ev_fd); task_pool_push(task_info); log_error("File: "__FILE__", line: %d, "\ "event_add fail.", __LINE__); return; } } else { log_error("File: "__FILE__", line: %d,recv failed,errno: %d, error info: %s", __LINE__, errno, strerror(errno)); close(task_info->on_read.ev_fd); task_pool_push(task_info); } return; } else if(0 == bytes) { log_warning("File:"__FILE__",Line:%d.recv buffer form network is error.disconnection the network.", __LINE__); close(task_info->on_read.ev_fd); task_pool_push(task_info); return; } if(task_info->request_info.length-task_info->offset > bytes) { log_warning("File:"__FILE__",Line:%d.recv body is not over.",__LINE__); task_info->offset += bytes; if(0 != event_add(&task_info->on_read, &task_info->timeout)) { close(task_info->on_read.ev_fd); task_pool_push(task_info); log_error("File: "__FILE__", line: %d, "\ "event_add fail.", __LINE__); return; } } else { task_info->read_type = unspecified; break; } } deal_request_body(client_socket_fd,task_info); return; }
void deal_working_thread(void *arg) { log_info("debug to this."); int client_socket_fd = (int) arg; if(0 > client_socket_fd) { log_error("File:"__FILE__",Line:%d.the arg means client socket filedesc is less 0!",__LINE__); return; } /* 设置网络为非阻塞,libevent必须的 */ if(!set_nonblocking(client_socket_fd)) { log_error("File:"__FILE__",Line:%d.set client socket filedesc is error.error info is %s!", __LINE__,strerror(errno)); close(client_socket_fd); return; } task_info_t *task_info; task_info = task_pool_pop(); /* 对event_base注册事件回调函数, 注意没有使用EV_PERSIST */ do { task_info->read_type = header; event_set(&task_info->on_read,client_socket_fd,EV_READ,read_buffer,(void *) task_info); if(0 != event_base_set(task_info->event_base,&task_info->on_read)) { log_error("File:"__FILE__",Line:%d.Associate the read header event to event_base is error.",__LINE__); task_info->read_type = unspecified; close(client_socket_fd); task_pool_push(task_info); break; } event_set(&task_info->on_write,client_socket_fd,EV_WRITE,response_handle,(void *) task_info); if(0 != event_base_set(task_info->event_base,&task_info->on_write)) { log_error("File:"__FILE__",Line:%d.Associate the write hander to event_base is error.",__LINE__); task_info->read_type = unspecified; close(client_socket_fd); task_pool_push(task_info); break; } if(0 != event_add(&task_info->on_read,&task_info->timeout)) { log_error("File:"__FILE__",Line:%d.add the read header event to event_base is error.",__LINE__); task_info->read_type = unspecified; close(client_socket_fd); task_pool_push(task_info); break; } event_base_loop(task_info->event_base,EVLOOP_NONBLOCK); }while(false); return; }
--------------------------------------------------------------------------- ---------------------------------------------------------------------------
在libevent中使用多线程,这里介绍两种方式:管道和消息通知机制
第一种:管道方式:
一 线程的初始化
1线程对象
在进行事件驱动时,每个线程需建立自己的事件根基。由于libevent未提供线程之间通信的方式,我们采用管道来进行线程的通信。同时为方便主线程分配线程,我们还需保留各个线程的id号。因此我们采用如下结构来保留每个线程的有关信息。
typedef struct {
pthread_t thread_id; //线程ID
struct event_base *base; //事件根基
struct event notify_event;
int notify_receive_fd;
int notify_send_fd;
CQ new_conn_queue; //连接队列
} LIBEVENT_THREAD;
2 初始化线程
我们先介绍如何为每个线程创建自己的事件根基。正如前面介绍的我们需自己建立管道来进行线程通信,因此在线程根基初始化后,我们为管道的读添加事件对象。注意:在libevent中,线程的初始化需在事件根基初始化之后(即event_init之后)。
static void setup_thread(LIBEVENT_THREAD *me) {
if (! me->base)
me->base = event_init();
event_set(&me->notify_event, me->notify_receive_fd,
EV_READ | EV_PERSIST, thread_libevent_process, me);
event_base_set(me->base, &me->notify_event);
event_add(&me->notify_event, 0) == -1)
cq_init(&me->new_conn_queue);
}
将主线程存储于结构体的第0个对象中,然后为每个线程创建管道并创建事件根基
threads = malloc(sizeof(LIBEVENT_THREAD) * nthreads);
threads[0].base = main_base;
threads[0].thread_id = pthread_self();
for (i = 0; i < nthreads; i++) {
int fds[2];
pipe(fds)
threads[i].notify_receive_fd = fds[0];
threads[i].notify_send_fd = fds[1];
setup_thread(&threads[i]);
接下来从主线程中创建线程,线程创建成功后激活该线程的事件根基,进入事件循环。
for (i = 1; i < nthreads; i++) {
pthread_t thread;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_create(&thread, &attr, worker_libevent, &threads[i])
}
二 分发连接
现在我们有了多个线程和事件根基,那么我们应该如何将主线程接收到的连接分发给各个线程并将其激活呢?本例子中线程的选择采用最简单的轮询方式。
我们需要对accept_handle函数进行修改。在线程池模型中,我们使用子线程代替主线程来创建缓冲区事件对象,主线程只是激活选中的线程。我们通过向管道写入一个空字节来激活该管道的读请求。
thread += 1;
item->sfd = sfd
cq_push(&threads[thread].new_conn_queue, item);
write(threads[thread].notify_send_fd, "", 1)
三 处理请求
管道的读请求就绪后,回调函数thread_libevent_process被调用,此时就进入到了线程中,后面所有的调度都将在该线程中进行。
首先从管道中读取1个字节,然后创建事件缓冲区对象来实际处理请求。
static void thread_libevent_process(int fd, short which, void *arg) {
char buf[1];
read(fd, buf, 1)
…
}
四结束语
到这里我们的程序就可以使用线程池和libevent事件驱动来协同工作了。在实际的服务中,我们通常要将服务作为守护进程来运行
第二种:消息通知机制
libevent的使用方式是最开始调用event_init初始化一个全局的event_base指针,以后使用其中的API添加新的事件均是对这个指针进行的操作.
试想如下一种典型的场景:主线程使用libevent处理网络IO事件,接收新连接以及接收完客户端的数据之后将该事件交给辅助线程进行处理,辅助线程处理完了,需要往客户端发送回应数据,则再通过libevent提供的API将这个事件添加到可读事件中.但是,由于libevent中这个event_base指针是全局的,如果多线程同时添加可能会造成多线程不安全问题,这个在libevent的代码注释里面作者也做了注解.
在这个地方有人给出了解决的方案:
已经有现成的解决方案:
http://monkeymail.org/archives/libevent-users/2006-October/000257.html
原作者给的地址好像已经失效了。iunknown在 spserver 中就是使用了他的代码,可以从这里获得
http://code.google.com/p/spserver/source/browse/trunk/spserver/event_msgqueue.h
(对应的C文件可以通过修改上面文件的后缀名所得).
简单的说一说这个event_msgqueue的原理及使用方式:
它的结构体为:
struct event_msgqueue {
int push_fd;
int pop_fd;
int unlock_between_callbacks;
struct event queue_ev;
sp_thread_mutex_t lock;
void (*callback)(void *, void *);
void *cbarg;
struct circqueue *queue;
};
其中的queue是保存事件的队列,callback是处理事件的回调函数, lock是线程锁.
首先,它创建了一对socketpair(也就是结构体中的push_fd和pop_fd),将其中的一个通过libevent的event_add接口添加到关注的事件中,它的事件类型的是READ|PERSIST, 也就是说可读同时永不删除,而这个事件的回调函数是msgqueue_pop,这个函数的功能是将当前queue中的数据一一取出并且调用callback函数进行回调处理.
其次,外部的多线程需要往libevent添加事件时使用这个文件提供的msgqueue_push函数进行添加,此时, 往socketpair中发送一个字节的数据, 这么做的目的是为了让第一步已经添加的socketpair得到响应,此时, 由第一步,必然会触发之前注册的回调函数msgqueue_pop,将这个队列中的事件取出来进行处理.
这里使用socketpair起到了一个通知的作用,当队列中有数据时,通过向socketpair发送数据以调用msgqueue_pop,将数据取出来进行处理.
可见,这个event_msgqueue起了一个中间层的作用,辅助线程要往libevent添加事件就通过这个队列,当队列中有数据时再触发libevent的事件处理机制进行处理.设计的非常巧妙.