目录
一、安装libevent库
二、libevent
三、基于 libevent 实现信号处理与定时任务
四、基于 libevent 的事件驱动 TCP 服务端代码
一、安装libevent库
sudo su
apt install libevent-dev
二、libevent
libevent 是一个轻量级网络i/o库,i/o框架库,封装了select,poll,epoll ,跨平台支持,支持linux,window,unix。统一事件源,libevent对i/o事件,信号和定时事件提供统一的处理。线程安全,使用libevent_pthreads库来提供线程安全支持。基于reactor模式实现多种 I/O 模型和事件处理提供统一接口,助力开发者编写高效、可扩展的网络服务器和客户端程序,尤其适用于大量并发连接场景
提供了一个libevent库,定义事件提供一个文件描述符关注什么事件读或者写还要一个回调函数fun,添加事件,添加到libevent库中。事件循环libevent库通过io检测调用select,poll,epoll。自己决定用什么方法(其实是调用libevent中的库函数,就等于完成了事件循环添加事件的启动) ,然后将就绪的描述符放到类似于就绪队列中,然后调用就绪描述符的回调函数
三、基于 libevent
实现信号处理与定时任务
这段代码利用 libevent
库实现了信号处理和定时任务功能。libevent
是一个跨平台的事件驱动库,能简化处理 I/O 事件、信号和定时事件的过程。代码主要实现了捕获 SIGINT
信号(通常是用户按下 Ctrl + C
时产生)并输出信号编号,同时设置了一个 5 秒的定时器,定时器到期时输出 time out
信息。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include <event.h>// 信号处理回调函数
// 当捕获到指定信号时,此函数会被调用
// fd: 信号编号
// event: 事件类型,用于判断是否为信号事件
// arg: 用户传递的参数,这里未使用
void sig_cb(int fd, short event, void* arg)
{// 检查事件类型是否为信号事件if( event & EV_SIGNAL){// 若是信号事件,打印信号编号printf("sig=%d\n",fd);}}// 定时器回调函数
// 当定时器到期时,此函数会被调用
// fd: 未使用
// event: 事件类型,用于判断是否为定时器事件
// arg: 用户传递的参数,这里未使用
void timeout_cb(int fd, short event, void* arg)
{// 定时器到期,打印提示信息printf("time out\n");
}int main()
{// 创建libevent实例,event_init函数会初始化一个事件循环基础对象// base指向这个对象,后续的事件操作都基于此对象struct event_base * base = event_init();// 检查libevent实例是否创建成功if( NULL == base ){// 若创建失败,以状态码1终止程序exit(1);}// 添加事件// 处理Ctrl + C产生的SIGINT信号// evsignal_new函数用于创建一个信号事件对象// base: 之前创建的libevent实例// SIGINT: 要监听的信号,即Ctrl + C产生的中断信号// sig_cb: 信号触发时调用的回调函数// NULL: 用户传递的参数,这里不使用struct event * sig_ev = evsignal_new(base,SIGINT,sig_cb,NULL);// 检查信号事件对象是否创建成功if( sig_ev != NULL){// 若创建成功,将信号事件注册到libevent实例中// NULL表示使用默认的时间设置event_add(sig_ev,NULL);}// 定时事件// evtimer_new函数用于创建一个定时器事件对象// base: 之前创建的libevent实例// timeout_cb: 定时器到期时调用的回调函数// NULL: 用户传递的参数,这里不使用struct event* time_ev = evtimer_new(base,timeout_cb,NULL);// 定义一个timeval结构体,设置定时器的超时时间// {5, 0} 表示5秒,0微秒struct timeval tv = {5,0};// 检查定时器事件对象是否创建成功if( time_ev != NULL ){// 若创建成功,将定时器事件注册到libevent实例中// &tv: 指向timeval结构体的指针,指定定时器的超时时间event_add(time_ev,&tv);}// 启动事件循环// event_base_dispatch函数会进入一个循环,不断监听注册的事件// 当有事件发生时,调用相应的回调函数event_base_dispatch(base);// 释放定时器事件对象的内存event_free(time_ev);// 释放信号事件对象的内存event_free(sig_ev);// 释放libevent实例的内存event_base_free(base);// 程序正常退出,返回状态码0exit(0);
}
timeout只能执行一次,但是按下ctrl+c后信号可以一直执行。ctrl+/结束。因为信号是永久性事件
如果换成下面的代码:
四、基于 libevent 的事件驱动 TCP 服务端代码
该程序通过 libevent
库实现了一个简单的 TCP 服务器,使用事件驱动的方式处理客户端连接和数据接收。当有新的客户端连接时,接受连接并为其创建一个事件对象,监听可读事件;当客户端发送数据时,接收数据并发送响应;当客户端关闭连接时,释放相关资源。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <event.h>int socket_init();
void recv_data(int c, short ev, void* arg)
{struct event* c_ev = NULL;struct event**p = (struct event**)arg;if( p != NULL){c_ev = *p; }if( ev & EV_READ){char buff[128] = {0};int n = recv(c,buff,127,0);if( n <= 0 ){event_free(c_ev);//先从libevent中移除事件,然后free释放内存空间close(c);free(p);printf("client close(%d)\n",c);return;}printf("recv(%d)=%s\n",c,buff);send(c,"ok",2,0);}}
void accept_client(int sockfd, short ev, void* arg)
{struct event_base* base = (struct event_base*)arg;if( ev & EV_READ){int c = accept(sockfd,NULL,NULL);if( c < 0){return;}printf("accept c =%d\n",c);struct event** p = (struct event**)malloc(sizeof(struct event*));if( p != NULL ){*p = event_new(base,c,EV_READ|EV_PERSIST,recv_data,p);event_add(*p,NULL);}}
}
int main()
{int sockfd = socket_init();if( -1 == sockfd ){exit(1);}struct event_base * base = event_init();if( base == NULL ){exit(1);}struct event* sock_ev = event_new(base,sockfd,EV_READ|EV_PERSIST,accept_client,base);if( sock_ev != NULL){event_add(sock_ev,NULL);}event_base_dispatch(base);event_free(sock_ev);event_base_free(base);close(sockfd);exit(0);
}
int socket_init()
{int sockfd = socket(AF_INET,SOCK_STREAM,0);if( -1 == sockfd ){return -1;}struct sockaddr_in saddr;memset(&saddr,0,sizeof(saddr));saddr.sin_family = AF_INET;saddr.sin_port = htons(6000);saddr.sin_addr.s_addr = inet_addr("127.0.0.1");int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));if( -1 == res ){printf("bind err\n");return -1;}res = listen(sockfd,5);if( -1 == res){return -1;}return sockfd;
}
1. 头文件包含
stdio.h
:提供标准输入输出函数,如printf
。stdlib.h
:包含内存分配、进程终止等函数,如malloc
、exit
。unistd.h
:提供 UNIX 标准的系统调用,如close
。string.h
:包含字符串处理函数,如memset
。sys/socket.h
:提供套接字编程相关的函数和结构体。arpa/inet.h
:提供 IP 地址转换函数,如inet_addr
。netinet/in.h
:包含网络地址结构体定义,如sockaddr_in
。sys/epoll.h
:用于 Linux 下的 I/O 多路复用,libevent
底层可能会使用。event.h
:libevent
库的头文件,用于事件驱动编程。2.
socket_init
函数
int socket_init() {// 创建一个 TCP 套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == sockfd){return -1;}// 初始化服务器地址结构体struct sockaddr_in saddr;memset(&saddr, 0, sizeof(saddr));saddr.sin_family = AF_INET;saddr.sin_port = htons(6000);saddr.sin_addr.s_addr = inet_addr("127.0.0.1");// 将套接字绑定到指定的地址和端口int res = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));if (-1 == res){printf("bind err\n");return -1;}// 开始监听客户端连接,最大允许 5 个连接请求排队res = listen(sockfd, 5);if (-1 == res){return -1;}return sockfd; }
socket(AF_INET, SOCK_STREAM, 0)
:创建一个 IPv4 的 TCP 套接字。bind
:将套接字绑定到本地地址127.0.0.1
和端口6000
。listen
:将套接字设置为监听状态,允许最多 5 个客户端连接请求排队等待处理。3.
recv_data
函数
void recv_data(int c, short ev, void* arg) {struct event* c_ev = NULL;struct event** p = (struct event**)arg;if (p != NULL){c_ev = *p;}if (ev & EV_READ){// 定义一个缓冲区用于接收数据char buff[128] = {0};// 从客户端套接字接收数据int n = recv(c, buff, 127, 0);if (n <= 0){// 若接收数据失败或客户端关闭连接event_free(c_ev); // 释放事件对象close(c); // 关闭客户端套接字free(p); // 释放内存printf("client close(%d)\n", c);return;}// 打印接收到的数据printf("recv(%d)=%s\n", c, buff);// 向客户端发送响应 "ok"send(c, "ok", 2, 0);} }
recv
:从客户端套接字接收数据。- 如果接收数据长度小于等于 0,表示客户端关闭连接或发生错误,释放事件对象、关闭套接字并释放内存。
- 若接收成功,打印接收到的数据,并向客户端发送响应 "ok"。
4.
accept_client
函数
void accept_client(int sockfd, short ev, void* arg) {struct event_base* base = (struct event_base*)arg;if (ev & EV_READ){// 接受客户端连接int c = accept(sockfd, NULL, NULL);if (c < 0){return;}printf("accept c =%d\n", c);// 为客户端套接字分配内存struct event** p = (struct event**)malloc(sizeof(struct event*));if (p != NULL){// 创建一个新的事件对象,监听客户端套接字的可读事件*p = event_new(base, c, EV_READ | EV_PERSIST, recv_data, p);// 将事件对象添加到事件循环中event_add(*p, NULL);}} }
accept
:接受客户端的连接请求,返回一个新的客户端套接字。event_new
:创建一个新的事件对象,监听客户端套接字的可读事件,并指定回调函数为recv_data
。event_add
:将事件对象添加到libevent
的事件循环中。5.
main
函数
int main() {// 初始化服务器套接字int sockfd = socket_init();if (-1 == sockfd){exit(1);}// 初始化 libevent 事件循环struct event_base * base = event_init();if (base == NULL){exit(1);}// 创建一个事件对象,监听服务器套接字的可读事件struct event* sock_ev = event_new(base, sockfd, EV_READ | EV_PERSIST, accept_client, base);if (sock_ev != NULL){// 将事件对象添加到事件循环中event_add(sock_ev, NULL);}// 启动事件循环event_base_dispatch(base);// 释放事件对象event_free(sock_ev);// 释放事件循环对象event_base_free(base);// 关闭服务器套接字close(sockfd);// 程序正常退出exit(0); }
socket_init
:初始化服务器套接字。event_init
:初始化libevent
的事件循环。event_new
:创建一个事件对象,监听服务器套接字的可读事件,当有新的客户端连接时,调用accept_client
函数。event_base_dispatch
:启动事件循环,开始监听事件。- 最后释放资源,关闭套接字并退出程序。
和用select,poll,epoll写服务器区别:
libevent
库提供了更高级、更简洁、更可移植的方式来编写 TCP 服务器,适合快速开发和维护复杂的网络应用
- libevent:
libevent
是一个高级的事件驱动库,它对底层的 I/O 多路复用机制(如select
、poll
、epoll
)进行了封装,提供了统一的接口。使用libevent
编写 TCP 服务器时,开发者无需关心底层具体使用的是哪种 I/O 多路复用机制,代码更加简洁、易读和易维护。例如,在libevent
中创建和管理事件只需要调用event_new
、event_add
等函数,而不需要手动处理文件描述符集合的设置和轮询等操作。- 示例代码中,只需要关注业务逻辑(如处理客户端连接和数据接收),而事件循环和事件管理等复杂操作都由
libevent
库完成。
- select、poll、epoll:
- 直接使用
select
、poll
、epoll
编写 TCP 服务器时,需要手动处理很多底层细节。例如,使用select
时需要手动设置文件描述符集合,并且在每次调用select
后需要遍历文件描述符集合来判断哪些文件描述符有事件发生;使用epoll
时需要手动管理epoll
实例,包括创建、添加和删除文件描述符等操作。代码复杂度较高,尤其是在处理大量文件描述符时,容易出错。
- 直接使用