Linux网络编程 第六天
目录
学习目标
libevent介绍
libevent的安装
libevent库的使用
libevent的使用
libevent的地基-event_base
等待事件产生-循环等待event_loop
使用libevent库的步骤:
事件驱动-event
编写一个基于event实现的tcp服务器:
自带buffer的事件-bufferevent
链接监听器-evconnlistener
总结
学习目标
- 描述什么是libevent并掌握如何安装
- 掌握event_base的作用和使用方法
- 熟练掌握libevent库中的事件循环
- 掌握event事件的使用方法
- 掌握bufferevent的工作方式
- 掌握使用libevent实现tcp服务器端流程
- 掌握使用Libevent实现tcp客户端流程
libevent介绍
1 事件驱动, 高性能, 轻量级, 专注于网络
2 源代码精炼, 易读
3 跨平台
4 支持多种I/O多路复用技术, 如epoll select poll等
5 支持I/O和信号等事件
libevent的安装
登录官方网站: http://libevent.org, 查看相关信息
libevent源码下载主要分2个大版本:
- 1.4.x 系列, 较为早期版本, 适合源码学习
- 2.x系列, 较新的版本, 代码量比1.4版本多很多, 功能也更完善。
libevent的核心实现:
在linux上, 其实质就是epoll反应堆.
libevent是事件驱动, epoll反应堆也是事件驱动, 当要监测的事件发生的时候, 就会调用事件对应的回调函数, 执行相应操作. 特别提醒: 事件回调函数是由用户开发的, 但是不是由用户显示去调用的, 而是由libevent去调用的.
从官网http://libevent.org上下载安装文件之后, 将安装文件上传到linux系统上;源码包的安装,以2.0.22版本为例,在官网可以下载到源码包libevent-2.0.22-stable.tar.gz, 安装步骤与第三方库源码包安装方式基本一致。
第一步: 解压libevent-2.0.22-stable.tar.gz
- 解压: tar -zxvf libevent-2.0.22-stable.tar.gz
- cd到libevent-2.0.22-stable目录下, 查看README文件, 该文件里描述了安装的详细步骤, 可参照这个文件进行安装.
第二步: 进入源码目录:
- 执行配置./configure, 检测安装环境, 生成makefile.
- 执行./configure的时候也可以指定路径, ./configure --prefix=/usr/xxxxx, 这样就可以安装到指定的目录下, 但是这样在进行源代码编译的时候需要指定用-I头文件的路径和用-L库文件的路径. 若默认安装不指定--prefix, 则会安装到系统默认的路径下, 编译的时候可以不指定头文件和库文件所在的路径.
- 执行make命令编译整个项目文件.
- 通过执行make命令, 会生成一些库文件(动态库和静态库)和可执行文件.
- 执行sudo make install进行安装
- 安装需要root用户权限, 这一步需要输入当前用户的密码
- 执行这一步, 可以将刚刚编译成的库文件和可执行文件以及一些头文件拷贝到/usr/local目录下:
----头文件拷贝到了/usr/local/include目录下;
----库文件拷贝到了/usr/local/lib目录下.
libevent库的使用
进入到libevent-2.0.22-stable/sample下, 可以查看一些示例源代码文件.
使用libevent库编写代码在编译程序的时候需要指定库名:-levent;
安装文件的libevent库文件所在路径:libevent-2.0.22-stable/.libs;
编写代码的时候用到event.h头文件, 或者直接参考sample目录下的源代码文件也可以.
#include <event2/event.h>
编译源代码文件(以hello-world.c文件为例)
gcc hello-world.c -levent
由于安装的时候已经将头文件和库文件拷贝到了系统头文件所在路径/usr/local/include和系统库文件所在路径/usr/local/lib, 所以这里编译的时候可以不用指定-I和-L.
编译示例代码hello-world.c程序:
gcc -o hello-world hello-world.c -levent
测试: 在另一个终端窗口进行测试, 输入: nc 127.1 9995, 然后回车立刻显示Hello, World!字符串.
libevent的使用
libevent的地基-event_base
使用libevent 函数之前需要分配一个或者多个 event_base 结构体, 每个event_base结构体持有一个事件集合, 可以检测以确定哪个事件是激活的, event_base结构相当于epoll红黑树的树根节点, 每个event_base都有一种用于检测某种事件已经就绪的 “方法”(回调函数)
通常情况下可以通过event_base_new函数获得event_base结构。
下面介绍一些常用函数:
相关函数说明:
struct event_base *event_base_new(void); //event.h的L:337
函数说明: 获得event_base结构参数说明: 无返回值:
成功返回event_base结构体指针;
失败返回NULL;
void event_base_free(struct event_base *); //event.h的L:561函数说明: 释放event_base指针
3 int event_reinit(struct event_base *base); //event.h的L:349函数说明: 如果有子进程, 且子进程也要使用base, 则子进程需要对event_base重新初始化, 此时需要调用event_reinit函数.
函数参数: 由event_base_new返回的执行event_base结构的指针
返回值: 成功返回0, 失败返回-1
对于不同系统而言, event_base就是调用不同的多路IO接口去判断事件是否已经被激活, 对于linux系统而言, 核心调用的就是epoll, 同时支持poll和select.
查看libevent支持的后端的方法有哪些:
const char **event_get_supported_methods(void);函数说明: 获得当前系统(或者称为平台)支持的方法有哪些参数: 无返回值: 返回二维数组, 类似与main函数的第二个参数**argv.
const char * event_base_get_method(const struct event_base *base);
函数说明: 获得当前base节点使用的多路io方法
函数参数: event_base结构的base指针.
返回值: 获得当前base节点使用的多路io方法的指针
获取evnet内部使用的方法
编写代码获得当前系统支持的多路IO方法和当前所使用的方法:
相关的代码片段如下:
等待事件产生-循环等待event_loop
libevent在地基打好之后, 需要等待事件的产生, 也就是等待事件被激活, 所以程序不能退出, 对于epoll来说, 我们需要自己控制循环, 而在libevent中也给我们提供了API接口, 类似where(1)的功能.
函数如下:
int event_base_loop(struct event_base *base, int flags); //event.h的L:660
函数说明: 进入循环等待事件
参数说明:
base: 由event_base_new函数返回的指向event_base结构的指针
flags的取值:
#define EVLOOP_ONCE 0x01只触发一次, 如果事件没有被触发, 阻塞等待
#define EVLOOP_NONBLOCK 0x02非阻塞方式检测事件是否被触发, 不管事件触发与否, 都会立即返回.
这个函数一般不用, 而大多数都调用libevent给我们提供的另外一个API:
int event_base_dispatch(struct event_base *base); //event.h的L:364
函数说明: 进入循环等待事件
参数说明:由event_base_new函数返回的指向event_base结构的指针
调用该函数, 相当于没有设置标志位的event_base_loop。程序将会一直运行, 直到没有需要检测的事件了, 或者被结束循环的API终止。
int event_base_loopexit(struct event_base *base, const struct timeval *tv);
int event_base_loopbreak(struct event_base *base);
struct timeval
{long tv_sec; long tv_usec;
};
两个函数的区别是如果正在执行激活事件的回调函数, 那么event_base_loopexit将在事件回调执行结束后终止循环(如果tv时间非NULL, 那么将等待tv设置的时间后立即结束循环), 而event_base_loopbreak会立即终止循环。
两个函数的区别是如果正在执行激活事件的回调函数, 那么event_base_loopexit将在事件回调执行结束后终止循环(如果tv时间非NULL, 那么将等待tv设置的时间后立即结束循环), 而event_base_loopbreak会立即终止循环。
使用libevent库的步骤:
1 创建根节点--event_base_new
2 设置监听事件和数据可读可写的事件的回调函数
设置了事件对应的回调函数以后, 当事件产生的时候会自动调用回调函数
3 事件循环--event_base_dispatch
相当于while(1), 在循环内部等待事件的发生, 若有事件发生则会触发事件对应的回调函数。
4 释放根节点--event_base_free
释放由event_base_new和event_new创建的资源, 分别调用event_base_free
和event_free函数.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <event2/event.h>int main()
{int i = 0;// 获取当前系统支持的方法const char **p = event_get_supported_methods();while (p[i] != NULL){printf("%s \t", p[i++]);}// 获取地基节点struct event_base *base = event_base_new();if (base == NULL){printf("create event error!\n");return -1;}// 获取当前系统使用的方法const char *pp = event_base_get_method(base);printf("Current method: %s\n", pp);// 释放地基event_base_free(base);return 0;
}
事件驱动-event
事件驱动实际上是libevent的核心思想, 本小节主要介绍基本的事件event。
主要的状态转化:
主要几个状态:
无效的指针: 此时仅仅是定义了 struct event *ptr;
非未决:相当于创建了事件, 但是事件还没有处于被监听状态, 类似于我们使用epoll的时候定义了struct epoll_event ev并且对ev的两个字段进行了赋值, 但是此时尚未调用epoll_ctl对事件上树.
未决:就是对事件开始监听, 暂时未有事件产生。相当于调用epoll_ctl对要监听的事件上树, 但是没有事件产生.
激活:代表监听的事件已经产生, 这时需要处理, 相当于调用epoll_wait函数有返回, 当事件被激活以后, libevent会调用该事件对应的回调函数.
libevent的事件驱动对应的结构体为struct event, 对应的函数在图上也比较清晰, 下面介绍一下主要的函数:
typedef void (*event_callback_fn)(evutil_socket_t fd, short events, void *arg);
struct event *event_new(struct event_base *base, evutil_socket_t fd, short events, event_callback_fn cb, void *arg);
函数说明: event_new负责创建event结构指针, 同时指定对应的地基base, 还有对应的文件描述符, 事件, 以及回调函数和回调函数的参数。
参数说明:
base: 对应的根节点--地基
fd: 要监听的文件描述符
events:要监听的事件#define EV_TIMEOUT 0x01 //超时事件
#define EV_READ 0x02 //读事件
#define EV_WRITE 0x04 //写事件
#define EV_SIGNAL 0x08 //信号事件
#define EV_PERSIST 0x10 //周期性触发
#define EV_ET 0x20 //边缘触发, 如果底层模型支持设置 则有效, 若不支持则无效.
若要想设置持续的读事件则: EV_READ | EV_PERSIST
用 | 代表+ 也就是监听所有或的事件
cb 回调函数, 原型如下:
typedef void (*event_callback_fn)(evutil_socket_t fd, short events, void *arg);
注意: 回调函数的参数就对应于event_new函数的fd, event和arg#define evsignal_new(b, x, cb, arg) \ event_new((b), (x), EV_SIGNAL|EV_PERSIST, (cb), (arg))
int event_add(struct event *ev, const struct timeval *timeout);
函数说明: 将非未决态事件转为未决态, 相当于调用epoll_ctl函数(EPOLL_CTL_ADD), 开始监听事件是否产生, 相当于epoll的上树操作.
参数说明:ev: 调用event_new创建的事件
timeout: 限时等待事件的产生, 也可以设置为NULL, 没有限时。
int event_del(struct event *ev);
函数说明: 将事件从未决态变为非未决态, 相当于epoll的下树(epoll_ctl调用 EPOLL_CTL_DEL操作)操作。
参数说明: ev指的是由event_new创建的事件.
void event_free(struct event *ev);
函数说明: 释放由event_new申请的event节点。
编写一个基于event实现的tcp服务器:
总体步骤:
1 搭建服务器的固定三步:
- --创建socket
- --绑定bind
- --监听listen
2 调用event_base_new函数创建event_base节点
3 创建要监听的事件event, 主要就是监听事件和读数据的事件
--设置好监听事件的回调函数,然后event_add上树---->有新的连接, 则 调用accept接受新的连接---->将这个新的连接设置好回调函数(一般 是设置读事件), 然后继续event_add上树, 若有客户端关闭连接则 从树上摘除该事件节点.
4 调用event_base_dispatch进入循环等待事件的发生
5 释放资源
调用event_base_free释放根节点和调用event_free释放事件节点
测试代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <event2/event.h>// 使用了libevent库时,需要在后面加-levent
// bufferevent_data_cb为自定义
// typedef void (*bufferevent_data_cb)(struct bufferevent *bev, void *ctx);/*
编写一个基于event实现的tcp服务器
1 创建socket---socket()
2 设置端口复用---setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int))
3 绑定--bind()
4 设置监听--listen()
5 创建地基struct event_base *base = event_base_new()
6 创建lfd对应的事件struct event *ev = event_new(base, lfd, EV_READ|EV_PERSIST, conncb, base);
7 上event_base地基event_add(ev, NULL);
8 进入事件循环event_base_dispatch(base);9 释放资源event_base_free(base);event_free(ev);
*/// 函数原型
// typedef void (*event_callback_fn)(evutil_socket_t fd, short events, void *arg);// 函数执行步骤
// event_base_dispatch 会将函数设置为分离持续运行状态
// 当有事件发生的时候(连接事件) 会调用的一个ev事件,ev事件注册了连接函数conncb
// 再有事件发生时(发送数据) 会调用conncb里面创建的新事件 readcb事件struct ConEvnt
{int cfd;struct event *event;
};struct ConEvnt Events[1024];void readcb(evutil_socket_t fd, short events, void *arg);
void conncb(evutil_socket_t fd, short events, void *arg);// 写成全局以便关闭时可以调用
struct event *conev;void readcb(evutil_socket_t fd, short events, void *arg)
{int n;char buf[1024];memset(buf, 0x00, sizeof(buf));// 这里不写while(1)是因为read函数会阻塞 如果写会影响其他函数的监听n = read(fd, buf, sizeof(buf));if (n <= 0){printf("client close or no data!");// 关闭通信描述符close(fd);// 将通信文件描述符对应的事件从base地基上删除event_del(Events[fd].event);}else{printf("%s", buf);write(fd, buf, n);}
}// 连接函数
void conncb(evutil_socket_t fd, short events, void *arg)
{struct event_base *base = (struct event_base *)arg;// 接收新连接int cfd = accept(fd, NULL, NULL);if (cfd > 0){// EV_READ|EV_PERSIST读事件和持续监听// 创建通信文件描述符对应的事件并设置回调函数为readcbconev = event_new(base, cfd, EV_READ | EV_PERSIST, readcb, NULL);if (conev == NULL){exit(0);}// 将通信描述符保存到数组中if (cfd < 1024){Events[cfd].cfd = cfd;Events[cfd].event = conev;}// 将通信文件描述符对应的事件上event_base地基event_add(conev, NULL);}
}// 监听回调函数 有新链接来时
struct event *event_new(struct event_base *base, evutil_socket_t fd, short events, event_callback_fn cb, void *arg);int main()
{// 创建socketint lfd = socket(AF_INET, SOCK_STREAM, 0);if (lfd < 0){perror("create socket error!\n");}// 设置端口复用 防止服务器断开时不能马上启动 两种写法// 端口复用允许在一个应用程序可以把 n 个套接字绑在一个端口上而不出错int opt = 1;setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));// setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(int));// 初始化结构体数组for (int i = 0; i < 1024; i++){Events[i].cfd = -1;}// 绑定数据struct sockaddr_in serv;bzero(&serv, sizeof(serv));serv.sin_family = AF_INET;serv.sin_port = htons(8888); // host代表主机 s短整型 l长整型 只能绑定未使用的端口 否则 报错bind error: Permission deniedserv.sin_addr.s_addr = htonl(INADDR_ANY); // 表示使用本地任意可用IPif ((bind(lfd, (struct sockaddr *)&serv, sizeof(serv))) < 0){return -1;}// 监听数据listen(lfd, 128);// 创建地基struct event_base *base = event_base_new();// 创建event事件监听描述符对应的事件struct event *ev = event_new(base, lfd, EV_READ | EV_PERSIST, conncb, base);// 将新的事件节点上base地基event_add(ev, NULL);// 设置事件循环 相当于while(1), 在循环内部等待事件的发生, 若有事件发生则会触发事件对应的回调函数event_base_dispatch(base);// 释放根节点event_base_free(base);event_free(ev);close(lfd);return 0;
}
程序名称: 02_server.c
测试过程:
编译源代码: gcc -o 02_server 02_server.c -levent
启动程序: ./02_server
在其他终端窗口上使用nc命令进行测试: nc 127.1 8888, 多开几个终端窗口使用nc命令进行测试.
发现问题:
当使用多个客户端(nc命令模拟客户端程序)进行测试的时候, 特别是当关闭所有客户端程序的时候, 若再次开启nc命令, 会发现异常.
分析原因:
在02_server.c代码中,L:11处 struct event *readev = NULL; 是一个全局变量, 当有多个客户端请求服务的时候, 如2个客户端请求服务的时候, 第二次readev的值会将第一次readev的值覆盖掉: 代码L:55处readev = event_new(base, cfd, EV_READ | EV_PERSIST, readcb, base);可以进行如下的测试重现异常情况: 先后在终端A和B上执行nc 127.1 8888命令, 然后ctrl+c结束掉终端A上的nc命令, 再次进如到终端B上, 则会出现异常情况.(原因是由于readev是一个全局变量, 所以readev只能保留最后一次所赋的值, 当客户端退出后, 服务端会调用event_del(readev);从根节点上摘除该事件, 此时其实从base节点上摘掉的是最后一个event事件节点, 所以最后一个客户端会出现异常, 其实只要是开启了多个客户端, 而且关闭客户端的时候只要不是关闭最后一个客户端, 都会出现这种异常情况)
若先结束终端B上的nc命令, 不会出现异常情况.
解决办法: 可以将对应事件的文件描述符和事件做一个映射, 说的通俗一点就是可以将fd和event定义在一个结构体当中, 然后定义一个结构体数组, 这样可以使fd和event形成一个一对一的映射关系, 通过fd就可以找到event.
struct event_fd
{
evutil_socket_t fd;
struct event *ev;
}event[MAX];
自带buffer的事件-bufferevent
bufferevent实际上也是一个event, 只不过比普通的event高级一些, 它的内部有两个缓冲区, 以及一个文件描述符(网络套接字)。一个网络套接字有读和写两个缓冲区, bufferevent同样也带有两个缓冲区, 还有就是libevent事件驱动的核心回调函数, 那么四个缓冲区以及触发回调的关系如下:
从图中可以得知, 一个bufferevent对应两个缓冲区, 三个回调函数, 分别是写回调, 读回调和事件回调.
bufferevent有三个回调函数:
读回调 – 当bufferevent将底层读缓冲区的数据读到自身的读缓冲区时触发读事件回调.需要注意的是:数据由内核到bufferevent的过程不是用户调用,而是bufferevent内部进行操作的。
写回调 – 当bufferevent将自身写缓冲的数据写到底层写缓冲区的时候触发写事件回调, 由于数据最终是写入了内核的写缓冲区中, 应用程序已经无法控制, 这个事件对于应用程序来说基本没什么用, 只是通知功能.
事件回调 – 当bufferevent绑定的socket连接, 断开或者异常的时候触发事件回调.
主要使用的函数如下:
struct bufferevent *bufferevent_socket_new(struct event_base *base, evutil_socket_t fd, int options);
函数说明: bufferevent_socket_new 对已经存在socket创建bufferevent事件, 可用于后面讲到的连接监听器的回调函数中.
参数说明:base :对应根节点fd :文件描述符options : bufferevent的选项BEV_OPT_CLOSE_ON_FREE -- 释放bufferevent自动关闭底层接口(当bufferevent被释放以后, 文件描述符也随之被close) BEV_OPT_THREADSAFE -- 使bufferevent能够在多线程下是安全的
int bufferevent_socket_connect(struct bufferevent *bev, struct sockaddr *serv, int socklen);
函数说明: 该函数封装了底层的socket与connect接口, 通过调用此函数, 可以将bufferevent事件与通信的socket进行绑定, 参数如下:bev – 需要提前初始化的bufferevent事件serv – 对端(一般指服务端)的ip地址, 端口, 协议的结构指针socklen – 描述serv的长度
说明: 调用此函数以后, 通信的socket与bufferevent缓冲区做了绑定, 后面调用了bufferevent_setcb函数以后, 会对bufferevent缓冲区的读写操作的事件设置回调函数, 当往缓冲区中写数据的时候会触发写回调函数, 当数据从socket的内核缓冲区读到bufferevent读缓冲区中的时候会触发读回调函数.
void bufferevent_free(struct bufferevent *bufev);
函数说明: 释放bufferevent
void bufferevent_setcb(struct bufferevent *bufev,bufferevent_data_cb readcb, bufferevent_data_cb writecb,
bufferevent_event_cb eventcb, void *cbarg);
函数说明: bufferevent_setcb用于设置bufferevent的回调函数, readcb, writecb, eventcb分别对应了读回调, 写回调, 事件回调, cbarg代表回调函数的 参数。
回调函数的原型:
typedef void (*bufferevent_data_cb)(struct bufferevent *bev, void *ctx);
typedef void (*bufferevent_event_cb)(struct bufferevent *bev, short what, void *ctx);
What 代表 对应的事件
BEV_EVENT_EOF--遇到文件结束指示
BEV_EVENT_ERROR--发生错误
BEV_EVENT_TIMEOUT--发生超时
BEV_EVENT_CONNECTED--请求的过程中连接已经完成
int bufferevent_write(struct bufferevent *bufev, const void *data, size_t size);bufferevent_write是将data的数据写到bufferevent的写缓冲区
int bufferevent_write_buffer(struct bufferevent *bufev, struct evbuffer *buf);bufferevent_write_buffer 是将数据写到写缓冲区另外一个写法, 实际上bufferevent的内部的两个缓冲区结构就是struct evbuffer。
size_t bufferevent_read(struct bufferevent *bufev, void *data, size_t size);bufferevent_read 是将bufferevent的读缓冲区数据读到data中, 同时将读到的数据从bufferevent的读缓冲清除。
int bufferevent_read_buffer(struct bufferevent *bufev, struct evbuffer *buf);bufferevent_read_buffer 将bufferevent读缓冲数据读到buf中, 接口的另外一种。
int bufferevent_enable(struct bufferevent *bufev, short event);
int bufferevent_disable(struct bufferevent *bufev, short event);bufferevent_enable与bufferevent_disable是设置事件是否生效, 如果设置为disable, 事件回调将不会被触发。
链接监听器-evconnlistener
链接监听器封装了底层的socket通信相关函数, 比如socket, bind, listen, accept这几个函数。链接监听器创建后实际上相当于调用了socket, bind, listen, 此时等待新的客户端连接到来, 如果有新的客户端连接, 那么内部先进行调用accept处理, 然后调用用户指定的回调函数。可以先看看函数原型, 了解一下它是怎么运作的:
函数声明所在的头文件: event2/listener.h
struct evconnlistener *evconnlistener_new_bind(struct event_base *base,evconnlistener_cb cb, void *ptr, unsigned flags, int backlog,
const struct sockaddr *sa, int socklen);
函数说明:
是在当前没有套接字的情况下对链接监听器进行初始化, 看最后2个参数实际上就是bind使用的关键参数, backlog是listen函数的关键参数(略有不同的是, 如果backlog是-1, 那么监听器会自动选择一个合适的值, 如果填0, 那么监听器会认为listen函数已经被调用过了), ptr是回调函数的参数, cb是有新连接之后的回调函数, 但是注意这个回调函数触发的时候, 链接器已经处理好新连接了, 并将与新连接通信的描述符交给回调函数。flags 需要参考几个值:LEV_OPT_LEAVE_SOCKETS_BLOCKING 文件描述符为阻塞的LEV_OPT_CLOSE_ON_FREE 关闭时自动释放LEV_OPT_REUSEABLE 端口复用LEV_OPT_THREADSAFE 分配锁, 线程安全
struct evconnlistener *evconnlistener_new(struct event_base *base,evconnlistener_cb cb, void *ptr, unsigned flags, int backlog,
evutil_socket_t fd);
evconnlistener_new函数与前一个函数不同的地方在与后2个参数, 使用本函数时, 认为socket已经初始化好, 并且bind完成, 甚至也可以做完listen, 所以大多数时候, 我们都可以使用第一个函数。
两个函数的回调函数
typedef void (*evconnlistener_cb)(struct evconnlistener *evl, evutil_socket_t fd, struct sockaddr *cliaddr, int socklen, void *ptr);
evconnlistener内部会自动接收新的信号,监听描述符在内部进行处理,因此返回出来的文件描述符不是监听描述符,回调函数fd参数是与客户端通信的描述符, 并非是等待连接的监听的那个描述符, 所以cliaddr对应的也是新连接的对端地址信息, 已经是accept处理好的。*ptr是回调函数参数,由evconnlistener_new_bind里面的*ptr传过来的。
void evconnlistener_free(struct evconnlistener *lev);
函数说明: 释放链接监听器int evconnlistener_enable(struct evconnlistener *lev);
函数说明: 使链接监听器生效int evconnlistener_disable(struct evconnlistener *lev);
函数说明: 使链接监听器失效
如果上述函数都较为了解了, 可以尝试去看懂hello-world.c的代码, 在安装包的sample目录下, 其中有涉及到信号的函数, 看看自己能否找到函数的原型在哪?实际上就是一个宏定义, 也是我们之前介绍的event_new函数, 只是对应一个信号事件而已, 处理机制略有不同。
#define evsignal_new(b, x, cb, arg) \event_new((b), (x), EV_SIGNAL|EV_PERSIST, (cb), (arg))
代码测试:
/*This exmple program provides a trivial server program that listens for TCPconnections on port 9995. When they arrive, it writes a short message toeach client connection, and closes each connection once it is flushed.Where possible, it exits cleanly in response to a SIGINT (ctrl-c).
*/#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <signal.h>
#ifndef WIN32
#include <netinet/in.h>
# ifdef _XOPEN_SOURCE_EXTENDED
# include <arpa/inet.h>
# endif
#include <sys/socket.h>
#endif#include <event2/bufferevent.h>
#include <event2/buffer.h>
#include <event2/listener.h>
#include <event2/util.h>
#include <event2/event.h>
#include <ctype.h>static const char MESSAGE[] = "Hello, World!\n";static const int PORT = 9995;static void listener_cb(struct evconnlistener *, evutil_socket_t,struct sockaddr *, int socklen, void *);
static void conn_writecb(struct bufferevent *, void *);
static void conn_readcb(struct bufferevent *, void *);
static void conn_eventcb(struct bufferevent *, short, void *);
static void signal_cb(evutil_socket_t, short, void *);int
main(int argc, char **argv)
{struct event_base *base; //地基struct evconnlistener *listener; //链接监听器struct event *signal_event; //信号事件struct sockaddr_in sin;
#ifdef WIN32WSADATA wsa_data;WSAStartup(0x0201, &wsa_data);
#endif//创建地基---相当于epoll的树根(epoll_create)base = event_base_new();if (!base) {fprintf(stderr, "Could not initialize libevent!\n");return 1;}//绑定端口地址memset(&sin, 0, sizeof(sin));sin.sin_family = AF_INET;sin.sin_port = htons(PORT);//创建链接监听器--socket-bind-listen-accept//listener_cb: 回调函数//LEV_OPT_REUSEABLE|LEV_OPT_CLOSE_ON_FREE: 设置端口复用, 当链接监听器释放的时候关闭套接字(监听文件描述符)listener = evconnlistener_new_bind(base, listener_cb, (void *)base,LEV_OPT_REUSEABLE|LEV_OPT_CLOSE_ON_FREE, -1,(struct sockaddr*)&sin,sizeof(sin));if (!listener) {fprintf(stderr, "Could not create a listener!\n");return 1;}//设置SIGINT信号的事件回调//系统就会监听这个函数 只会触发一次 注册之后触发改信号就会调用事件回调函数signal_event = evsignal_new(base, SIGINT, signal_cb, (void *)base);if (!signal_event || event_add(signal_event, NULL)<0) {fprintf(stderr, "Could not create/add a signal event!\n");return 1;}//进入等待事件循环---相当于while(1)event_base_dispatch(base);//释放资源evconnlistener_free(listener);event_free(signal_event);event_base_free(base);printf("done\n");return 0;
}//listener: 链接监听器
//fd: 通信文件描述符
//sa和socklen: 客户端IP地址信息
//user_data: 参数
static void
listener_cb(struct evconnlistener *listener, evutil_socket_t fd,struct sockaddr *sa, int socklen, void *user_data)
{struct event_base *base = user_data;struct bufferevent *bev;//创建bufferevent缓冲区//BEV_OPT_CLOSE_ON_FREE: bufferevent释放的时候自动关闭通信文件描述符bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);if (!bev) {fprintf(stderr, "Error constructing bufferevent!");event_base_loopbreak(base);//退出循环,程序结束return;}//设置回调函数: 读回调, 写回调和事件回调bufferevent_setcb(bev, conn_readcb, conn_writecb, conn_eventcb, NULL);//使bufferevent设置生效bufferevent_enable(bev, EV_WRITE);bufferevent_enable(bev, EV_READ);//使bufferevent设置无效//bufferevent_disable(bev, EV_READ);//bufferevent_write(bev, MESSAGE, strlen(MESSAGE));
}//读取数据
//读取之后可以通过写到bufferevent_write写到bufevnet缓冲区
//当数据发送过来是会调用改函数
static void
conn_readcb(struct bufferevent *bev, void *user_data)
{//size_t bufferevent_read(struct bufferevent *bufev, void *data, size_t size);char buf[1024];memset(buf, 0x00, sizeof(buf));//从bufferevent读数据int n = bufferevent_read(bev, buf, sizeof(buf));printf("n=[%d],buf==[%s]\n", n, buf);int i=0;for(i=0; i<n; i++){buf[i] = toupper(buf[i]);}//往bufferevent的写缓冲区写数据//int bufferevent_write(struct bufferevent *bufev, const void *data, size_t size);bufferevent_write(bev, buf, n);//写bufferevent缓冲区会触发写事件回调
}//写数据
//相当于按下enter键事件
static void
conn_writecb(struct bufferevent *bev, void *user_data)
{struct evbuffer *output = bufferevent_get_output(bev);if (evbuffer_get_length(output) == 0) {printf("flushed answer\n");//bufferevent_free(bev);}
}static void
conn_eventcb(struct bufferevent *bev, short events, void *user_data)
{if (events & BEV_EVENT_EOF) {printf("Connection closed.\n");} else if (events & BEV_EVENT_ERROR) {printf("Got an error on the connection: %s\n",strerror(errno));/*XXX win32*/}/* None of the other events can happen here, since we haven't enabled* timeouts */bufferevent_free(bev);
}//事件回调函数
//按下crtl + c会调用该函数
static void
signal_cb(evutil_socket_t sig, short events, void *user_data)
{struct event_base *base = user_data;struct timeval delay = { 2, 0 };printf("Caught an interrupt signal; exiting cleanly in two seconds.\n");event_base_loopexit(base, &delay);
}
思路hello-world.c代码中当使用nc客户端测试的时候为什么nc收到hello world之后就立刻关闭了.
答案: 当服务器收到新的连接请求的时候, 会自动触发listener_cb回调函数, 该函数中有往bufferevent缓冲区中写入的操作(调用bufferevent_write), 接着又会触发写回调函数conn_writecb的执行, 这个回调函数中调用了bufferevent_free, 该函数能够释放bufferevent, 同时会关闭socket连接.
若是按下crtl+c会将程序终止, 此时会触发异常事件的退出函数(conn_eventcb)
hello-world.c代码没有读事件触发, 可以将代码进行修改, 将bufferevent的读事件添加上.
思考: 如何修改hello-world.c添加读回调.
libevent客户端代码阅读和分析.
//bufferevent建立客户端的过程
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <event.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h>
#include <event2/util.h>int tcp_connect_server(const char* server_ip, int port);
void cmd_msg_cb(int fd, short events, void* arg);
void server_msg_cb(struct bufferevent* bev, void* arg);
void event_cb(struct bufferevent *bev, short event, void *arg);int main(int argc, char** argv)
{if( argc < 3 ){//两个参数依次是服务器端的IP地址、端口号printf("please input 2 parameter\n");return -1;}//创建根节点struct event_base *base = event_base_new();//创建并且初始化buffer缓冲区struct bufferevent* bev = bufferevent_socket_new(base, -1,BEV_OPT_CLOSE_ON_FREE);//监听终端输入事件 设置标准输入的监控,设置回调是 cmd_msg_cb STDIN_FILENO为标准输入账号struct event* ev_cmd = event_new(base, STDIN_FILENO,EV_READ | EV_PERSIST,cmd_msg_cb, (void*)bev);//上树 开始监听标准输入的读事件event_add(ev_cmd, NULL);struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr) );server_addr.sin_family = AF_INET;server_addr.sin_port = htons(atoi(argv[2]));//将ip地址转换为网络字节序inet_aton(argv[1], &server_addr.sin_addr);//连接到 服务器ip地址和端口 初始化了 socket文件描述符 socket+connect bufferevent_socket_connect(bev, (struct sockaddr *)&server_addr,sizeof(server_addr));//设置buffer的回调函数 主要设置了读回调 server_msg_cb ,传入参数是标准输入的读事件bufferevent_setcb(bev, server_msg_cb, NULL, event_cb, (void*)ev_cmd);bufferevent_enable(bev, EV_READ | EV_PERSIST);event_base_dispatch(base);//循环等待event_free(ev_cmd);bufferevent_free(bev);event_base_free(base);printf("finished \n");return 0;
}
//终端输入回调
void cmd_msg_cb(int fd, short events, void* arg)
{char msg[1024];int ret = read(fd, msg, sizeof(msg));if( ret < 0 ){perror("read fail ");exit(1);}//得到bufferevent指针,目的是为了写到bufferevent的写缓冲区struct bufferevent* bev = (struct bufferevent*)arg;//把终端的消息发送给服务器端bufferevent_write(bev, msg, ret);
}void server_msg_cb(struct bufferevent* bev, void* arg)
{char msg[1024];size_t len = bufferevent_read(bev, msg, sizeof(msg));msg[len] = '\0';printf("recv %s from server\n", msg);
}void event_cb(struct bufferevent *bev, short event, void *arg)
{if (event & BEV_EVENT_EOF)printf("connection closed\n");else if (event & BEV_EVENT_ERROR)printf("some other error\n");else if( event & BEV_EVENT_CONNECTED){printf("the client has connected to server\n");return ;}//这将自动close套接字和free读写缓冲区bufferevent_free(bev);//释放event事件 监控读终端struct event *ev = (struct event*)arg;event_free(ev);
}
画hello-world.c代码的改进版的流程图.
总结
对于bufferevent来说, 一个文件描述符, 2个缓冲区, 3个回调函数
文件描述符是用于和客户端进行通信的通信文件描述符, 并不是监听的文件描述符
2个缓冲区是指: 一个bufferevent包括读缓冲区和写缓冲区
3个回调函数指: 读回调函数 写回调函数 和事件回调函数(客户端关闭连接或者是被信号终止进程会触发事件回调函数)
其中写回调基本上没什么用, 事件回调指的是socket上的连接和断开,异常等情况会触发bufferevent的事件回调.
读回调函数的触发时机:
当socket的内核socket读缓冲区中有数据的时候, bufferevent会将内核缓冲区中的数据读到自身的读缓冲区, 会触发bufferevent的读操作, 此时会调用bufferevent的读回调函数.
写回调函数的触发时机:
当往bufferevent的写缓冲区写入数据的时候, bufferevent底层会把缓冲区中的数据写入到内核的socket的写缓冲区中, 此时会触发bufferevent的写回调函数, 最后由内核的驱动程序将数据发送出去.
相关文章:

Linux网络编程 第六天
目录 学习目标 libevent介绍 libevent的安装 libevent库的使用 libevent的使用 libevent的地基-event_base 等待事件产生-循环等待event_loop 使用libevent库的步骤: 事件驱动-event 编写一个基于event实现的tcp服务器: 自带buffer的事件-buff…...

STM32开发(六)STM32F103 通信 —— RS485 Modbus通信编程详解
文章目录一、基础知识点二、开发环境三、STM32CubeMX相关配置1、STM32CubeMX基本配置2、STM32CubeMX RS485 相关配置四、Vscode代码讲解五、结果演示以及报文解析一、基础知识点 了解 RS485 Modbus协议技术 。本实验是基于STM32F103开发 实现 通过RS-485实现modbus协议。 准备…...

AcWing1049.大盗阿福题解
前言如果想看状态机的详解,点机这里:dp模型——状态机模型C详解1049. 大盗阿福阿福是一名经验丰富的大盗。趁着月黑风高,阿福打算今晚洗劫一条街上的店铺。这条街上一共有 N家店铺,每家店中都有一些现金。阿福事先调查得知,只有当…...
python日志模块,loggin模块
python日志模块,loggin模块loggin模块日志的格式处理器种类日志格式的参数使用loggin模块 logging库采用模块化方法,并提供了几类组件:记录器,处理程序,过滤器和格式化程序。 记录器(Logger)&a…...

接口自动化入门-TestNg
目录1.TestNg介绍2、TestNG安装3、TestNG使用3.1 编写测试用例脚本3.2 创建TestNG.xml文件(1)创建testng.xml文件(2)修改testng.xml4、测试报告生成1.TestNg介绍 TestNg是Java中开源的自动化测试框架,灵感来源于Junit…...

Spring AOP —— 详解、实现原理、简单demo
目录 一、Spring AOP 是什么? 二、学习AOP 有什么作用? 三、AOP 的组成 3.1、切面(Aspect) 3.2、切点(Pointcut) 3.3、通知(Advice) 3.4、连接点 四、实现 Spring AOP 一个简…...

(蓝桥真题)异或数列(博弈)
题目链接:P8743 [蓝桥杯 2021 省 A] 异或数列 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 样例输入: 4 1 1 1 0 2 2 1 7 992438 1006399 781139 985280 4729 872779 563580 样例输出: 1 0 1 1 分析:容易想到对于异或最大值…...

4万字数字政府建设总体规划方案WORD
本资料来源公开网络,仅供个人学习,请勿商用。部分资料内容: 我省“数字政府”架构 (一) 总体架构。 “数字政府”总体架构包括管理架构、业务架构、技术架构。其中,管理架构体现“管运分离”的建设运营模式…...
CCF/CSP 201709-2公共钥匙盒100分
试题编号:201709-2试题名称:公共钥匙盒时间限制:1.0s内存限制:256.0MB问题描述:问题描述 有一个学校的老师共用N个教室,按照规定,所有的钥匙都必须放在公共钥匙盒里,老师不能带钥…...
【OC】Blocks模式
1. Block语法 Block语法完整形式如下: ^void (int event) {printf("buttonId:%d event%d\n", i, event); }完整形式的Block语法与一般的C语言函数定义相比,仅有两点不同。 没有函数名。带有“^”(插入记号)。 因为O…...

软件设计师教程(七)计算机系统知识-操作系统知识
软件设计师教程 软件设计师教程(一)计算机系统知识-计算机系统基础知识 软件设计师教程(二)计算机系统知识-计算机体系结构 软件设计师教程(三)计算机系统知识-计算机体系结构 软件设计师教程(…...

蓝桥杯2023/3/2
1. 小蓝正在学习一门神奇的语言,这门语言中的单词都是由小写英文字母组 成,有些单词很长,远远超过正常英文单词的长度。小蓝学了很长时间也记不住一些单词,他准备不再完全记忆这些单词,而是根据单词中哪个字母出现得最…...

【IoT】创业成功不可或缺的两个因素:能力和趋势
今天就来谈谈能力和趋势究竟哪个更重要的问题。 在谈成功的十大要素时,我曾经讲到: 一命、二运、三风水,这三个要素几乎不涉及任何个人的努力。 而趋势跟这三个要素又是息息相关的,这也类似雷军所说的飞猪理论。 只要风足够大&…...

2020蓝桥杯真题日期格式 C语言/C++
问题描述 小蓝要处理非常多的数据, 其中有一些数据是日期。 在小蓝处理的日期中有两种常用的形式: 英文形式和数字形式。 英文形式采用每个月的英文的前三个宁母作为月份标识, 后面跟两位数字 表示日期, 月份标识第一个字母大写, 后两个字母小写, 日期小于 10 时要补 前导 0s…...

总时差与自由时差
定义总时差(总浮动时间)(TF,Total Free Time,不耽误项目总进度)LS(Latest Start)-ES(Earliest Start)LF(Latest Finish)-EF࿰…...
LeetCode两个数组的交集-跳跃游戏- 最长有效括号
两个数组的交集 给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。 示例 1: 输入:nums1 [1,2,2,1], nums2 [2,2] 输出:[2] 示例 2: 输入&…...
mysql普通索引与唯一索引怎么选择
学习mysql普通索引与唯一索引选择记录总结,学习链接:http://gk.link/a/11YG8从mysql查询操作分析:普通索引:查到满足条件的第一条记录后,还会继续查找下一条记录,直到出现满足条件的记录出现后停止检索唯一…...

JavaWeb开发(三)3.5——Java的反射机制
一、反射机制的概念 指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能调用它的任意一个方法。这种动态获取信息,及动态调用对象方法的功能叫java语言的反射机制。 Java反射机制的核心是在程序运行时动…...

Python每日一练(20230305)
目录 1. 正则表达式匹配 ★★★ 2. 寻找旋转排序数组中的最小值 II ★★★ 3. 删除排序链表中的重复元素 II ★★ 1. 正则表达式匹配 给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 . 和 * 的正则表达式匹配。 . 匹配任意单个字符* 匹配零个或多个…...

SpringBoot三种方法实现定时发送邮件的案例
前言 小编我将用CSDN记录软件开发之路上所学的心得与知识,有兴趣的小伙伴可以关注一下!也许一个人独行,可以走的很快,但是一群人结伴而行,才能走的更远!让我们在成长的道路上互相学习,让我们共…...

铭豹扩展坞 USB转网口 突然无法识别解决方法
当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…...
条件运算符
C中的三目运算符(也称条件运算符,英文:ternary operator)是一种简洁的条件选择语句,语法如下: 条件表达式 ? 表达式1 : 表达式2• 如果“条件表达式”为true,则整个表达式的结果为“表达式1”…...
多模态商品数据接口:融合图像、语音与文字的下一代商品详情体验
一、多模态商品数据接口的技术架构 (一)多模态数据融合引擎 跨模态语义对齐 通过Transformer架构实现图像、语音、文字的语义关联。例如,当用户上传一张“蓝色连衣裙”的图片时,接口可自动提取图像中的颜色(RGB值&…...
Java入门学习详细版(一)
大家好,Java 学习是一个系统学习的过程,核心原则就是“理论 实践 坚持”,并且需循序渐进,不可过于着急,本篇文章推出的这份详细入门学习资料将带大家从零基础开始,逐步掌握 Java 的核心概念和编程技能。 …...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
Rapidio门铃消息FIFO溢出机制
关于RapidIO门铃消息FIFO的溢出机制及其与中断抖动的关系,以下是深入解析: 门铃FIFO溢出的本质 在RapidIO系统中,门铃消息FIFO是硬件控制器内部的缓冲区,用于临时存储接收到的门铃消息(Doorbell Message)。…...
GitHub 趋势日报 (2025年06月06日)
📊 由 TrendForge 系统生成 | 🌐 https://trendforge.devlive.org/ 🌐 本日报中的项目描述已自动翻译为中文 📈 今日获星趋势图 今日获星趋势图 590 cognee 551 onlook 399 project-based-learning 348 build-your-own-x 320 ne…...

Razor编程中@Html的方法使用大全
文章目录 1. 基础HTML辅助方法1.1 Html.ActionLink()1.2 Html.RouteLink()1.3 Html.Display() / Html.DisplayFor()1.4 Html.Editor() / Html.EditorFor()1.5 Html.Label() / Html.LabelFor()1.6 Html.TextBox() / Html.TextBoxFor() 2. 表单相关辅助方法2.1 Html.BeginForm() …...

Rust 开发环境搭建
环境搭建 1、开发工具RustRover 或者vs code 2、Cygwin64 安装 https://cygwin.com/install.html 在工具终端执行: rustup toolchain install stable-x86_64-pc-windows-gnu rustup default stable-x86_64-pc-windows-gnu 2、Hello World fn main() { println…...
前端调试HTTP状态码
1xx(信息类状态码) 这类状态码表示临时响应,需要客户端继续处理请求。 100 Continue 服务器已收到请求的初始部分,客户端应继续发送剩余部分。 2xx(成功类状态码) 表示请求已成功被服务器接收、理解并处…...