// 办事情必须要实事求是,所以我写了如下程序,并有如下结论:
// 事件处理模型是事件和事件处理的一一对应关系。
// 事件被登记并挂载在事件树上, 然后由dispatch 函数进行查询。
// 对于满足条件的事件,处理其事件。
// 通过此例测试可知,dispatch 是个单线程模型,不能够并发工作,当一个事件被处理时,
// 下一个事件不会被同时处理。 当一个事件被长时间处理时,后面的事件会被堆积。
// 总之,事件被顺序的一个一个执行。有一个好处,对于事件共享的数据,不必加锁保护。
// 我无意认真研读libevent 源码,而很多研读源码的文章也未说清上述观点,所以才有此测试程序
观点都在代码里,就直接上代码来。 头文件:libevent_srv.h
ifndef __LIBEVENT_TST
#define __LIBEVENT_TST
#define PORT 8800
#define BACKLOG 5
#define MAX_RTSP_BUFFER 2048
struct sock_ev {
struct event* read_ev;
struct event* write_ev;
struct event* timer_ev;
char* buffer;
// record client info
char ipDocDecimal[INET_ADDRSTRLEN];
unsigned short port;
};
int GetSessionID();
void on_accept(int sock, short event, void* arg);
void on_write(int sock, short event, void* arg);
void on_read(int sock, short event, void* arg);
void Timer_CB(int fd, short event , void *arg);
void release_sock_event(struct sock_ev* ev);
#endif
代码体: libevent_srv.c
#include <stdio.h> // printf 等声明
#include <stdlib.h> // malloc, free 声明
#include <string.h> // memset, strlen 等声明
#include <sys/socket.h> // socket 等声明
#include <netinet/in.h> // struct sockeadd 等声明
#include <event.h> // struct event, event_base 等
#include "libevent_srv.h"
//gBase 是基本事件管理员。 对所有连接仅此一个.
//因在多个函数中出现,故声明为全局变量
struct event_base* gBase;
int main(int argc, char* argv[])
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
// 设置了该选项后,在父子进程模型中,当子进程为客户服务的时候如果父进程退出,可以重新启动程序完成服务的无缝升级,
// 否则在所有父子进程完全退出前再启动程序会在该端口上绑定失败,也即不能完成无缝升级.
// SO_REUSEADDR, 地址可以重用,不必time_wait 2个MSL 时间 (maximum segment lifetime)
int yes = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int));
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
serv_addr.sin_addr.s_addr = INADDR_ANY;
bind(sock, (struct sockaddr*)&serv_addr, sizeof(struct sockaddr));
printf("socket listen now\n");
listen(sock, BACKLOG);
//创建一个事件处理的全局变量,可以理解为这是一个负责集中处理各种出入IO事件的总管家,
//它负责接收和派发所有输入输出IO事件的信息,这里调用的是函数event_base_new(), 很多程序里这里用的是event_init(),
//但event_init 是一个过时的接口,官方建议采用event_base_new()
gBase = event_base_new();
struct event listen_ev;
//listen_en这个事件监听sock这个描述字的读操作,当读消息到达时调用on_accept函数,
//EV_PERSIST参数告诉系统持续的监听sock上的读事件,如果不加该参数,每次要监听该事件时就要重复的调用event_add函数,
//sock这个描述符是bind到本地的socket端口上,因此其对应的可读事件自然就是来自客户端的连接到达
event_set(&listen_ev, sock, EV_READ|EV_PERSIST, on_accept, NULL);
//告诉处理IO的管家请留意我们的listen_ev上的事件
event_base_set(gBase, &listen_ev);
//当有关心的事件到达时,调用on_accept函数
event_add(&listen_ev, NULL);
//正式启动libevent的事件处理机制,使系统运行起来,
//event_base_dispatch是一个无限循环 , 跟windows 编程的事件处理机制还真是相似啊。
//通过此例测试可知,dispatch 是个单线程模型,不能够并发工作,当一个事件被处理时,下一个事件不会被同时处理。
//当一个事件被长时间处理时,后面的事件会被堆积。总之,事件被顺序的一个一个执行。
event_base_dispatch(gBase);
return 0;
}
void on_accept(int sock, short event, void* arg)
{
int sid = GetSessionID();
printf("session id:%d\n",sid);
// socket的描述字可以封装一个结构体sock_ev 来保护读、写的事件以及数据缓冲区
// 多个客户端因而对应多个newfd, 并对应多个sock_ev 变量
struct sock_ev* ev = (struct sock_ev*)malloc(sizeof(struct sock_ev));
ev->read_ev = (struct event*)malloc(sizeof(struct event));
ev->write_ev = (struct event*)malloc(sizeof(struct event));
ev->timer_ev = (struct event*)malloc(sizeof(struct event));
ev->buffer = (char*)malloc(MAX_RTSP_BUFFER);
int sin_size = sizeof(struct sockaddr_in);
struct sockaddr_in cli_addr;
int newfd = accept(sock, (struct sockaddr*)&cli_addr, &sin_size);
char IPDotDecimal[INET_ADDRSTRLEN];
struct in_addr addr=cli_addr.sin_addr;
inet_ntop(AF_INET,&addr,IPDotDecimal,sizeof(IPDotDecimal));
strcpy(ev->ipDocDecimal, IPDotDecimal);
ev->port = cli_addr.sin_port;
printf("ip:%s, port:%d\n",IPDotDecimal, cli_addr.sin_port);
//在客户描述字newfd上监听可读事件,当有数据到达是调用on_read函数。read_ev作为参数传递给了on_read函数。
//注意:read_ev需要从堆里malloc出来,如果是在栈上分配,那么当函数返回时变量占用的内存会被释放,
//此时事件主循环event_base_dispatch会访问无效的内存而导致进程崩溃(即crash);
event_set(ev->read_ev, newfd, EV_READ|EV_PERSIST, on_read, ev);
event_base_set(gBase, ev->read_ev);
event_add(ev->read_ev, NULL);
// 增加一个定时器:
evtimer_set(ev->timer_ev, Timer_CB, ev);
event_base_set(gBase, ev->timer_ev);
struct timeval timer_v;
timer_v.tv_sec = 2;
timer_v.tv_usec = 0;
evtimer_add(ev->timer_ev, &timer_v);
}
void on_write(int sock, short event, void* arg)
{
char* buffer = (char*)arg;
send(sock, buffer, strlen(buffer), 0);
}
void on_read(int sock, short event, void* arg)
{
struct event* write_ev;
int size;
struct sock_ev* ev = (struct sock_ev*)arg;
bzero(ev->buffer, MAX_RTSP_BUFFER);
size = recv(sock, ev->buffer, MAX_RTSP_BUFFER, 0);
printf("---receive data:---, size:%d\n", size);
printf("%s\n", ev->buffer);
if (size == 0) {
//已经关闭了连接, 释放资源
release_sock_event(ev);
close(sock);
return;
}
// 在on_read函数中从socket读取数据后程序就可以直接调用write/send接口向客户回写数据了,
// 直接调用write/send函数向客户端写数据可能导致程序较长时间阻塞在IO操作上,
// 比如socket的输出缓冲区已满,则write/send操作阻塞到有可用的缓冲区之后才能进行实际的写操作,
// 而通过注册on_write函数,那么libevent会在合适的时间调用我们的callback函数
//在sock 注册 on_write 函数
event_set(ev->write_ev, sock, EV_WRITE, on_write, ev->buffer);
event_base_set(gBase, ev->write_ev);
event_add(ev->write_ev, NULL);
}
void Timer_CB(int fd, short event , void *arg)
{
struct sock_ev * ev = (struct sock_ev *) arg;
printf("Timer_CB called sleep 2s,emulate a long process\n");
sleep(2);
struct timeval timer_v;
timer_v.tv_sec = 2;
timer_v.tv_usec = 0;
evtimer_add(ev->timer_ev, &timer_v);
}
void release_sock_event(struct sock_ev* ev)
{
printf("socket resource released:%s %d\n",ev->ipDocDecimal,ev->port);
event_del(ev->read_ev);
free(ev->read_ev);
free(ev->write_ev);
free(ev->timer_ev);
free(ev->buffer);
free(ev);
}
int GetSessionID()
{
static int sessionID = 1;
// 对于成千上万的连接请求,如果有并发存在,需要枷锁保护sessionID 的数据更新
// 但libevent 是单线程模型,所以不用加锁。
return sessionID++;
}
为了代码的完整性,也贴上socket 客户端程序 client_test.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAXLINE 256
#define SERV_PORT 8800
void do_loop(FILE *fp, int sockfd)
{
int n;
char sendline[MAXLINE], recvline[MAXLINE + 1];
while(fgets(sendline, MAXLINE, fp) != NULL)
{
/* read a line and send to server */
write(sockfd, sendline, strlen(sendline));
/* receive data from server */
n = read(sockfd, recvline, MAXLINE);
if(n == -1)
{
perror("read error");
exit(1);
}
recvline[n] = 0; /* terminate string */
printf("\nreceiv:%s\n",recvline);
}
}
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;
/* check args */
if(argc != 2)
{
printf("usage: udpclient <IPaddress>\n");
exit(1);
}
/* init servaddr */
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
if(inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
{
printf("[%s] is not a valid IPaddress\n", argv[1]);
exit(1);
}
// sockfd = socket(AF_INET, SOCK_DGRAM, 0);
sockfd = socket(AF_INET, SOCK_STREAM, 0);
/* connect to server */
if(connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
{
perror("connect error");
exit(1);
}
do_loop(stdin, sockfd);
return 0;
}
[hjj@hjj ~/work/test/libevent_srv]$ cat Makefile
all:
gcc -g -o libevent_srv libevent_srv.c -levent
gcc -g -o client_test client_test.c
clean:
rm *~ libevent_srv client_test
结果分析:
server 端可接受多个连接, 每个连接是一个收什么回什么的程序,并有定时器
client 是一个从键盘输入即发送,然后等待响应的程序。
服务端可以接受客户端数据,并穿插长时间处理的timer 操作。
[hjj@hjj ~/work/test/libevent_srv]$ ./libevent_srv
socket listen now
session id:1
ip:127.0.0.1, port:58065
Timer_CB called sleep 2s,emulate a long process
---receive data:---, size:4
123
Timer_CB called sleep 2s,emulate a long process
Timer_CB called sleep 2s,emulate a long process
---receive data:---, size:4
abc
Timer_CB called sleep 2s,emulate a long process
Timer_CB called sleep 2s,emulate a long process
---receive data:---, size:5
asdf
123 将会立即被发送,受server sleep 的影响,在receiv 未返回之前,
受阻于recv 函数。
但此时控制台仍然可以接受用户输入,存到键盘缓冲区。就是abc.
程序接受来响应,此时我们又往键盘缓冲区送人了asdf.
程序从键盘缓存区读到了abc, 发走了,又收到了。(fget, write,read)
程序从键盘缓存区读到了asdf, 发走了,又收到了。(fgets, write,read)
可见fgets 一次从键盘缓冲区读取一行,按步就班的进行。
[hjj@hjj ~/work/test/libevent_srv]$ ./client_test 127.0.0.1
123
abc
receiv:123
asdf
receiv:abc
receiv:asdf