linux 网络编程 2019.2.2(libevent(使用),如果动态库找不到,如何解决?)

本文深入解析libevent库的功能特性,包括事件处理框架、事件创建、事件循环及bufferevent的使用。并通过实例演示libevent在管道通信、服务器监听与客户端连接的应用,适合网络编程与高并发服务开发者参考。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

libevent是干什么的

 

开源的库, 提高开发效率

  • 封装了socket通信
  • 封装了IO多路转接

精简, 专注于网络, 性能高

事件驱动

 

memcached——一个缓存数据库,数据存在内存中,网络模块封装了libevent

 

 

libevent安装

 

官网:http://libevent.org/

 

安装:

./configure
  • --prefix == /usr/xxxxx    自定义安装目录的命令
  • 用于检测安装环境
  • 生成makefile
make
  • 编译源代码
  • 生成一些库
  • 动态, 静态
  • 可执行程序
sudo make install
  • 将数据拷贝到对应的目录
  • 如果目录不存在, 创建该目录

 

检测安装是否完成:

进入当前目录下的sample目录(该目录用于存放测试程序)

执行一下目录中的hello-word(9995是hello-word源码中看到的端口号)

此时服务端做出反应,输出flushed answer

 

补充:安装的默认目录

/usr是linux的资源目录

/usr/local——放用户安装的软件

  • /usr/local/include——头文件
  • /usr/local/bin——可执行程序
  • /usr/local/lib——库(动态和静态)

 

 

 

libevent的使用

自己写的 .c 文件如何调用libevent接口

编译时,需要指定库,否则编译时只能找到函数声明,无法找到函数实现

gcc hello-word.c -o hello -levent

其中,指定动态库的名字时要掐头去尾,则下图中的libevent.so被指定的动态库名字就是 event,编译时指定库的名字需要加参数 l ,因此上述shell命令中用 -levent指定库

 

常用的头文件是

#include <event2/event.h>

#include <event2/listener.h>

 

 

 

如果动态库找不到,如何解决?

1、找到xxx.so放到 /usr/lib   /lib -- 不推荐

sudo find /usr/local -name  "libevent.so"

2、将xxx.so放到环境变量中

方法一——LD_LIBRARY_PATH

export LD_LIBRARY_PATH=xxxx    //将该命令放在下面的哪一个文件中都可以
  • ~/.bashrc  ——用户级别
  • /etc/profile ——系统级别

方法二——使用命令重新加载

  • . ~/.bashrc
  • . /etc/profile

等价于 source

3、修改/etc/ld.so.conf

  • 动态库绝对路径添加到该文件中
  • sudo ldconfig -v    //-v参数用于显示信息,没别的用

 

 

libevent使用流程

1、创建一个事件处理框架——使用event_base_new函数创建struct event_base结构体变量

2、创建一事件

  • 带缓冲区的——bufferevent_socket_new创建一个带缓冲区的事件 -> 给缓冲区设置回调函数(用bufferevent_setcb()函数来进行设置) -> 开始时间循环
  • 不带缓冲区的——struct event(通过event_new函数获取该结构体变量)-> event_add将创建出的事件添加到事件处理框架中 -> event_base_duspatch函数启动事件循环来处理添加的事件

3、释放资源——event_base_free(释放框架),event_free(释放事件)

 

 

创建事件处理框架——event_base

 

使用 libevent 函数之前需要分配一个或者多个 event_base 结构体。每个event_base 结构体持有一个事件集合,可以检测以确定哪个事件是激活的。事件处理框架里面封装了很多IO转接函数,select、poll、epoll。

事件处理框架在用户眼中就是一个结构体,里面封装了大量的回调函数,他就相当于之前写的epoll模型。

  • 相当于epoll红黑树的树根
  • 底座
  • 抽象层, 完成对event_base的封装
  • 每个 event_base 都有一种用于检测哪种事件已经就绪的 “方法”,或者说后端.

 

1、创建event_base,此时消息循环还没启动

struct event_base* event_base_new(void);
  • 失败返回NULL

2、释放event_base

event_base_free(struct event_base* base);

3、循环监听base对应的事件, 等待条件满足

event_base_dispatch();

 

补充:

如何查看当前平台上支持的IO转接函数

//char **指向 char* str[];
const char **event_get_supported_methods(void);

通过下面的函数可以获取到当前创建的base结构体变量所使用的IO转接函数是哪一个

const char * event_base_get_method(const struct event_base *base);

在进程中使用libevent的时候,fork会使得子进程也有一个base结构体,此时在使用时,需要对子进程中的base重新初始化

  • 子进程创建成功之后, 父进程可以继续使用event_base
  • 子进程中需要继续使用event_base需要重新进程初始化
int event_reinit(struct event_base* base);

 

 

 

事件创建 ——event_new

 

创建新事件——没有缓冲区

#define  EV_TIMEOUT         0x01    // 废弃
#define  EV_READ            0x02
#define  EV_WRITE           0x04
#define  EV_SIGNAL          0x08    // 信号,libevent封装了信号相关的操作
#define  EV_PERSIST         0x10    // 持续触发
#define  EV_ET              0x20    // 边沿模式

//回调函数的函数原型,回调函数用于处理事件
//三个参数分别是int类型的文件描述符,文件描述符对应的事件以及操作
typedef void (*event_callback_fn)(evutil_socket_t, short, void *); 

struct event *event_new(  //对epollin和epollout进行了封装
        struct event_base *base, 
        evutil_socket_t fd,   // 文件描述符 实际上是int类型
        short what,           // 对应事件,读事件,写事件
        event_callback_fn cb, // 事件的处理动作
        void *arg
); 

调用event_new()函数之后, 新事件处于已初始化非未决状态

  • 未决就是有资格被处理,但是还没没被处理的事件
  • 非未决就是现在还没有资格被处理的事件

 

释放事件——释放event_base事件

void event_free(struct event *event); 

 

设置未决事件——把event_new事件怼到事件处理框架上

构造事件之后,在将其添加到 event_base 之前实际上是不能对其做任何操作的。使用event_add()将事件添加到event_base,之后 非未决事件 状态变成 未决事件.

int event_add(
        struct event *ev, 
        const struct timeval *tv   
); 

对于上侧结构体中的参数——tv:

  • NULL: 事件被触发, 对应的回调被调用
  • tv = {0, 100}, 如果设置了时间, 在该时间段内检测的事件没被触发, 时间到达之后, 回调函数还是会被调用
  • 函数调用成功返回0, 失败返回-1

 

设置非未决——从树上摘下event_new事件

int event_del(struct event *ev); 
  • 对已经初始化的事件调用 event_del()将使其成为非未决和非激活的。
  • 如果事件不是未决的或者激活的,调用将没有效果。
  • 成功时函数返回 0,失败时返回-1。

 

 

 

事件循环—— event_loop

 

event_base内部维护了一个消息循环,消息循环用于检测事件是否被触发了。

一旦有了一个已经注册了某些事件的 event_base, 就需要让 libevent 等待事件并且通知事件的发生。

 

启动event_base的事件循环

#define EVLOOP_ONCE                    0x01
		事件只会被触发一次
		事件没有被触发, 阻塞等
#define EVLOOP_NONBLOCK                0x02
		非阻塞 等方式去做事件检测
		不关心事件是否被触发了
#define EVLOOP_NO_EXIT_ON_EMPTY  0x04
		没有事件的时候, 也不退出轮询检测

int event_base_loop(struct event_base *base, int flags);  //用得少,可以指定循环的状态
  • 正常退出返回0, 失败返回-1
int event_base_dispatch(struct event_base* base);  //常用
  • 等同于没有设置标志的 event_base_loop ( )
  • 将一直运行,直到没有已经注册的事件了,或者调用 了event_base_loopbreak()或者 event_base_loopexit()为止。

循环过程中调用回调函数来处理事件

 

循环停止

  • 返回值: 成功 0, 失败 -1

方法一:

如果 event_base 当前正在执行激活事件的回调 ,它将在执行完当前正在处理的事件后立即退出(相对柔和)

int event_base_loopexit(
        struct event_base *base,
        const struct timeval *tv   //设置多久之后退出循环
);

struct timeval {
        long    tv_sec;                    
        long    tv_usec;            
};

方法二:

让event_base 立即退出循环(强退)

 int event_base_loopbreak(struct event_base *base);

 

 

libevent内部时间的状态转换流程

 

  1. 通过event_base产生事件处理框架
  2. 通过event_new来创建事件,此时事件是非未决状态
  3. 通过event_add函数将事件变成未决状态(此时事件才有被处理的资格)。
  4. 当事件循环被开启(dispatch函数的作用)并且对应的事件被触发时,事件被激活。
  5. 被激活的事件通过回调函数来进行处理,回调函数是事件循环时调用的。
  6. 被处理过的事件又变成了非未决状态
  7. event_add指定事件为持续触发:EV_PERSIST(这是一个宏)

 

 

 

管道通信实例

 

使用event读管道

管道的默认大小是4k

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#include <event2/event.h>

//回调函数的函数原型,回调函数用于处理事件
//三个参数分别是int类型的文件描述符,文件描述符对应的事件以及操作
//typedef void (*event_callback_fn)(evutil_socket_t, short, void *);
// 对操作处理函数  回调函数read_cb
void read_cb(evutil_socket_t fd, short what, void *arg) {
    // 读管道
    char buf[1024] = {0};
    //将管道中的数据读入buf中
    int len = read(fd, buf, sizeof(buf));
    printf("data len = %d, buf = %s\n", len, buf);
    //判断事件what里面是否有标志位EV_READ
    printf("read event: %s", what & EV_READ ? "Yes" : "No");
}

// 读管道
int main(int argc, const char* argv[]) {
    unlink("myfifo");
    //创建有名管道,0664是用户读写权限,最终的权限不是0664,
    mkfifo("myfifo", 0664);

    // open file,设置非阻塞O_NONBLOCK是为了让我们观察清除,默认是阻塞的
    int fd = open("myfifo", O_RDONLY | O_NONBLOCK);
    if(fd == -1){
        perror("open error");
        exit(1);
    }

    // 读管道  event_base创建事件处理框架
    struct event_base* base = NULL;
    base = event_base_new();

    // event_new 创建事件
    struct event* ev = NULL;
    //读管道,所以开启读操作EV_READ  EV_PERSIST设置持续检测  
    //read_cb是回调函数   NULL的位置可以用来传参,没有参数就传null
    ev = event_new(base, fd, EV_READ | EV_PERSIST, read_cb, NULL);

    // 添加事件,第二个参数设置事件超时,设置为NULL则一直阻塞等待事件的发生
    //如果设置成10,则阻塞等待10秒,十秒只能事件没有被触发,则强制调用该函数
    event_add(ev, NULL);

    // 事件循环
    event_base_dispatch(base);

    // dispatch退出之后,才会执行下面的代码
    //释放资源
    event_free(ev);        //释放事件
    event_base_free(base); //释放事件处理框架
    close(fd);             //释放文件描述符
    
    return 0;
}

编译时记得参数  -levent

gcc read_fifo.c read -levent

 

使用event写管道

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#include <event2/event.h>

// 对操作处理函数
void write_cb(evutil_socket_t fd, short what, void *arg){
    // write管道
    char buf[1024] = {0};
    static int num = 0;
    sprintf(buf, "hello, world == %d\n", num++);
    write(fd, buf, strlen(buf)+1);
}


// 写管道
int main(int argc, const char* argv[]){
    // open file  设置为非阻塞样式的写O_WRONLY | O_NONBLOCK
    int fd = open("myfifo", O_WRONLY | O_NONBLOCK);
    if(fd == -1){
        perror("open error");
        exit(1);
    }

    // 写管道
    struct event_base* base = NULL;
    base = event_base_new();

    // 创建事件
    struct event* ev = NULL;
    // EV_WRITE ——检测的写缓冲区是否有空间写  这里只写一次,因为没设置EV_PERSIST
    ev = event_new(base, fd, EV_WRITE , write_cb, NULL);

    // 添加事件
    event_add(ev, NULL);

    // 事件循环
    event_base_dispatch(base);

    // 释放资源
    event_free(ev);
    event_base_free(base);
    close(fd);
    
    return 0;
}

 

 

带缓冲区的事件bufferevent

 

套接字通讯时,我们不常使用上面介绍的event事件,而是使用带缓冲区的事件(bufferevent)。

一个文件描述符对应内核中的一块缓冲区,文件描述符可以操作读写缓冲区。libevent也提供了一块缓冲区,在收到数据的时候,libevent会将数据从内核中读取,然后存入自己的缓冲区中。

bufferevent自带缓冲区,缓冲区分为两部分,一部分是读缓冲区,一部分是写缓冲区。event不带缓冲区。

 

头文件

#include<event2/bufferevent.h>

 

 

bufferevent 理解

  • 是libevent为IO缓冲区操作提供的一种通用机制
  • bufferevent 由一个底层的传输端口(如套接字 ),一个读取缓冲区一个写入缓冲区组成。
  • 读缓冲区和写缓冲区内部用队列实现的。因此数据先进先出,且读过之后数据就没了。
  • 与通常的事件在底层传输端口已经就绪,可以读取或者写入的时候执行回调不同的是, bufferevent 在读取或者写入了足够量的数据之后调用用户提供的回调

 

 

  • 当读缓冲区中有数据时,读缓冲区对应的回调函数会被调用。在该回调函数的作用是读数据,对应的函数是bufferevent_read()(默认)
  • 当写缓冲区中的数据被发送出去时,写缓冲区的回调函数(默认)就会被调用,该函数用于通知你数据已经被发送出去了。
  • 向写缓冲区写入数据的函数是bufferevent_write()
  • 在写缓冲区中只要有数据,数据就会被自动的发送出去,发送数据的工作不是bufferevent_write()做的。

 

 

 

使用 bufferevent

 

1、创建带缓冲区的事件

可以使用 bufferevent_socket_new()函数创建一个带缓存的事件

struct bufferevent *bufferevent_socket_new(

        struct event_base *base,   

        evutil_socket_t fd,               //文件描述符

        enum bufferevent_options options  //对应的枚举选项

);
  • struct bufferevent也是一个 event
  • 成功时函数返回一个 bufferevent,失败则返回 NULL。

用的最多的options(就是上面函数的第三个参数)—— BEV_OPT_CLOSE_ON_FREE(libevent参考手册中文版第四章page53 - bufferevent的选项标志)。该宏用于释放 bufferevent 时关闭底层传输端口。这将关闭底层套接字,释放底层 bufferevent 等

 

2、给申请的bufferevent的缓冲区中的读写缓冲区设置回调函数

void bufferevent_setcb(
        struct bufferevent *bufev,    // bufferevent_socket_new的返回值
        bufferevent_data_cb readcb,   // 读缓冲区对应的回调函数,bufferevent_data_cb是函数指针的名字
                                      // 调用bufferevent_read()函数读读缓冲区中的取数据
        bufferevent_data_cb writecb,  // 写缓冲区对应的回调函数
                                      // 该回调函数通知你你写入了数据,如果不用该参数,传入NULL即可
        bufferevent_event_cb eventcb, // 事件对应的回调函数,事件比如连接成功,断开连接,操作异常
        void *cbarg                   // 该指针指向的内存会传给函数的第二个参数,即readcb
);

 

读缓冲区数据调用的回调函数(系统默认调用的)

当读缓冲区中有数据时,读缓冲区对应的回调函数会被调用。

size_t bufferevent_read(
        struct bufferevent *bufev,  //bufferevent函数地址
        void *data,                 //buffer的地址
        size_t size
);

写缓冲区调用的回调函数

int bufferevent_write(
        struct bufferevent *bufev,
        const void *data, 
        size_t size
);

 

读缓冲区数据的回调函数怎么写?

bufferevent_data_cb(这是一个函数指针的名字)的函数原型为

typedef void (*bufferevent_data_cb)(
        struct bufferevent *bev,     //bufferevent的地址
        void *ctx                    //接受传给回调函数的参数
);
  • ctx会收到bufferevent_setcb()函数(该函数的第二个参数是一个回调函数readcb,该回调函数的函数原型就是上面的代码)的第五个参数cbarg(该参数指向一块内存)传递给给回调函数readcb的内存。

 

bufferevent_setcb函数的第四个参数bufferevent_event_cb的函数原型

typedef void (*bufferevent_event_cb)(
        struct bufferevent *bev,
        short events,               //通过判断标志位可以确定发生了什么事件
        void *ctx
);

events参数:

  • EV_EVENT_READING:读取操作时发生某事件,具体是哪种事件请看其他标志。
  • BEV_EVENT_WRITING:写入操作时发生某事件,具体是哪种事件请看其他标志。
  • BEV_EVENT_ERROR:操作时发生错误。关于错误的更多信息,请调用EVUTIL_SOCKET_ERROR()。
  • BEV_EVENT_TIMEOUT:发生超时。
  • BEV_EVENT_EOF:遇到文件结束指示。
  • BEV_EVENT_CONNECTED:请求的连接过程已经完成

 

3、套接字通信,客户端连接服务器的函数

int bufferevent_socket_connect(
        struct bufferevent *bev,  // 封装了用于通讯的文件描述符
        struct sockaddr *address, // server端的IP和端口
        int addrlen               // 地址长度,即第二个参数的结构体大小,sizeof即可获取
); 
  • address 和 addrlen 参数跟标准调用 connect()的参数相同。如果还没有为bufferevent 设置套接字,调用函数将为其分配一个新的流套接字,并且设置为非阻塞
  • 如果已经为 bufferevent 设置套接字,调用bufferevent_socket_connect() 将告知libevent 套接字还未连接,直到连接成功之前不应该对其进行读取或者写入操作。
  • 连接完成之前可以向输出缓冲区添加数据。

 

4、设置bufferevent缓冲区的禁用和启用

如果设置缓冲区不可用,则设置的对应回调函数就不会被调用了。

默认情况下,写缓冲区(EV_WRITE)是可用的,读缓冲区(EV_READ)是不可用的

void bufferevent_enable(              //设置缓冲区是否可可用
        struct bufferevent *bufev,    //bufferevent的地址
        short events                  //对应的事件
); 

void bufferevent_disable(
        struct bufferevent *bufev, 
        short events
); 

short bufferevent_get_enabled(       //用于判断缓冲区是否可用
        struct bufferevent *bufev
); 
  • 可以启用或者禁用 bufferevent 上的 EV_READ、EV_WRITE 或者 EV_READ | EV_WRITE 事件。
  • 没有启用读取或者写入事件时,  bufferevent 将不会试图进行数据读取或者写入。

 

5、释放bufferevent操作

void bufferevent_free(struct bufferevent *bev); 

 

 

服务端的链接监听器 - evconnlistener

 

1、创建evconnlistener

在写socket的server端时,需要四步

  1. 创建监听socket
  2. 绑定
  3. 监听
  4. 等待并接受连接请求

链接监听器只需要调用下面的函数即可完成上述四步,这就是链接监听器的优点

struct evconnlistener *evconnlistener_new_bind(
        struct event_base *base,      // 事件处理框架
        evconnlistener_cb cb,         // 回调函数,接受连接请求之后,用户接下来要做的事情,写在这个回调函数里面即可
        void *ptr,                    // 用于给回调函数传参
        unsigned flags,               // 常用的是 LEV_OPT_CLOSE_ON_FREE 和 LEV_OPT_REUSEABLE
        int backlog,                  // 绑定需要一个backlog,该值不能超过128,传入-1,则使用默认的最大的值
        const struct sockaddr *sa,    // 服务器的 IP 和 端口 信息
        int socklen                   // struct sockaddr 的内存大小
);

evconnlistenner_cb的函数原型(回调函数由系统调用,因此传参不会出现问题,但是实现回调函数的函数体时,需要知道参数都是什么。

typedef void (*evconnlistener_cb)(
        struct evconnlistener *listener,   // 传入参数,evconnlistener_new_bind的返回值
        evutil_socket_t sock,              // 用于通信的文件描述符
        struct sockaddr *addr,             // 客户端的IP和端口信息
        int len,                           // addr的地址大小
        void *ptr                          // 用于获取传给回调函数的内存
); 

另一个相同功能的函数(不需要掌握)(该函数不负责绑定,需要在外部绑定之后,把绑定好的文件描述符作为参数传入,而上面的函数不需要传入文件描述符,因为他会给你创建一个)

struct evconnlistener * evconnlistener_new(
        struct event_base *base,
        evconnlistener_cb cb, 
        void *ptr, 
        unsigned flags, 
        int backlog,
        evutil_socket_t fd
);

 

2、启用和禁用 evconnlistener(用得少)

int evconnlistener_disable(struct evconnlistener *lev);

int evconnlistener_enable(struct evconnlistener *lev); 

 

3、调整 evconnlistener 的回调函数(用得少)

链接监听器在创建的时候指定了一个回调函数,如果你不想回调函数按照原来指定的方式去工作了,可以重新设置回调函数

void evconnlistener_set_cb(
        struct evconnlistener *lev,   //地址
        evconnlistener_cb cb,         //新的处理方式
        void *arg                     //回调函数传入的参数
); 

函数调整 evconnlistener 的回调函数和其参数

 

4、释放evconnlistener对应的资源

void evconnlistener_free(struct evconnlistener *lev); 

 

 

 

bufferevent的使用实例

 

服务端

相当于一个封装了epoll的,可以处理高并发的服务器

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <event2/event.h>
#include <event2/listener.h>
#include <event2/bufferevent.h>

// 读缓冲区回调
void read_cb(struct bufferevent *bev, void *arg){
    char buf[1024] = {0};   
    //读取缓冲区中的数据
    bufferevent_read(bev, buf, sizeof(buf));
    char* p = "我已经收到了你发送的数据!";
    printf("client say: %s\n", p);

    // 向缓冲区中写数据
    bufferevent_write(bev, p, strlen(p)+1);
    printf("====== send buf: %s\n", p);
}

// 写缓冲区回调
void write_cb(struct bufferevent *bev, void *arg){
    printf("我是写缓冲区的回调函数...\n"); 
}

// 事件回调  events可用于判断当前发生了什么事件
void event_cb(struct bufferevent *bev, short events, void *arg){
 
    //events & BEV_EVENT_EOF的结果非零时,则进入该if语句
    if (events & BEV_EVENT_EOF){
        printf("connection closed\n");  
    }
    else if(events & BEV_EVENT_ERROR)   {
        printf("some other error\n");
    }
    
    //释放资源
    bufferevent_free(bev);    
    printf("buffevent 资源已经被释放...\n"); 
}


//连接完成之后,对应通信操作
//evconnlistener_new_bind函数将base里面的用于通信的文件描述符fd作为参数传递给了回调函数 cb_listener
void cb_listener(struct evconnlistener *listener,  evutil_socket_t fd, 
        struct sockaddr *addr, int len, void *ptr){
   printf("connect new client\n");

   //获取传给回调函数的base(ptr指针指向了这块base)
   struct event_base* base = (struct event_base*)ptr;

   // 通信操作(主要是接受和发送数据)
   // 添加新事件
   struct bufferevent *bev=NULL;
   //将文件描述符封装成带缓冲区的事件bufferevent
   //BEV_OPT_CLOSE_ON_FREE设置自动释放资源
   //fd是用于通信的文件描述符
   bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);

   // 此时已经获取了bufferevent缓冲区,该缓冲区分为读缓冲区和写缓冲区两部分
   //给bufferevent缓冲区注册回调函数,注册之后不会被马上调用,操作系统会在时机适合的时候去调用回调函数
   bufferevent_setcb(bev,  //bufferevent_socket_new得到的缓冲区
	   read_cb,            //读回调函数
	   write_cb,           //写回调函数,可以设置为NULL,这样写缓冲区就不设置回调函数了
	   event_cb,           //事件回调函数
	   NULL);              //用于设置是否需要往回调里面传数据

   //启用读缓冲区为可用的,否则读回调函数不会被调用,写回调默认是启用的
   bufferevent_enable(bev, EV_READ);
}


int main(int argc, const char* argv[]){

    //初始化server信息
    struct sockaddr_in serv;
    memset(&serv, 0, sizeof(serv));
    serv.sin_family = AF_INET;   //地址族协议
    serv.sin_port = htons(9876); //设置端口
    serv.sin_addr.s_addr = htonl(INADDR_ANY);  //设置IP

    //创建事件处理框架
    struct event_base* base= event_base_new();

    // 创建监听的套接字、绑定、接收连接请求
    struct evconnlistener* listener=NULL;

    //evconnlistener_new_bind函数会自动创建并接受链接请求,因此函数内部会存在阻塞
    //当有新连接的时候,回调函数cb_listener就会被调用
    //下行注释从左到右依次是函数evconnlistener_new_bind中的参数解释
    //时间处理框架,回调函数,传入回调函数的参数,设置端口自动释放和复用,backlog设置为-1则使用默认最大的值,服务器IP地址和端口
    listener = evconnlistener_new_bind(base, cb_listener, base, 
                                  LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, 
                                  -1, (struct sockaddr*)&serv, sizeof(serv));

    //进入事件循环
    event_base_dispatch(base);

    evconnlistener_free(listener);
    event_base_free(base);

    return 0;
}

编译

gcc server.c -o server -levent

测试

nc 127.1 9876 //用于连接服务器
hello         //用于向服务器发送数据

 

 

客户端

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <event2/event.h>
#include <event2/bufferevent.h>
#include<event2/listener.h>
#include<arpa/inet.h>

void read_cb(struct bufferevent *bev, void *arg){
    char buf[1024] = {0}; 
	//接受服务端发来的数据 len是读到的字节数
    int len=bufferevent_read(bev, buf, sizeof(buf));
    printf("Server say: %s\n", buf);

    //给对方发数据  写这句会出现bug,因为从终端给服务器发数据之后,服务器会有返回信息
    //收到服务器的返回信息之后,读缓冲区中有了数据,会调用读回调函数(即read_cb)
    //完成读取之后,下面的函数会在写缓冲区中写入数据,这就被误以为是调用了回调函数send_cb造成的
    //因此会形成闭环,无限的发送数据给服务器(服务器对于收到的数据会进行反馈,因此会无限的回复数据)
    //bufferevent_write(bev, buf, len + 1);
}

//写回调函数
void write_cb(struct bufferevent *bev, void *arg){
    printf("I am Write_cb function....\n");
}

//事件回调函数
void event_cb(struct bufferevent *bev, short events, void *arg){
    if (events & BEV_EVENT_EOF){
        printf("connection closed\n");  
    }
    else if(events & BEV_EVENT_ERROR)   {
        printf("some other error\n");
    }
    else if(events & BEV_EVENT_CONNECTED){
        printf("成功连接到服务器\n");
        return;
    }
    
    bufferevent_free(bev);
    printf("free bufferevent...\n");
}

//终端接受输入,将接受的数据发送给server
void send_cb(evutil_socket_t fd, short what, void *arg){
    char buf[1024] = {0}; 
    //获取bev地址
    struct bufferevent* bev = (struct bufferevent*)arg;
    printf("请输入要发送的数据: \n");
    //读取终端中的数据
    int len=read(fd, buf, sizeof(buf));
    //将从终端读到的数据写到bufferevent对应的写缓冲区
    bufferevent_write(bev, buf, len+1);
}

int main(int argc, const char* argv[]){
    //创建时间处理框架
    struct event_base* base= event_base_new();

	int fd = socket(AF_INET, SOCK_STREAM, 0);
    //BEV_OPT_CLOSE_ON_FREE可用帮助我们在释放资源的时候,释放文件描述符指向的资源
    struct bufferevent* bev= bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);

    //连接服务器
    struct sockaddr_in serv;
    memset(&serv, 0, sizeof(serv));
    serv.sin_family = AF_INET;
    serv.sin_port = htons(9876);
    //点分十进制转成大端整型
    inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);
    //链接服务器 ,fd被封装成bev了,所以这个和connect函数差不多
    bufferevent_socket_connect(bev, (struct sockaddr*)&serv, sizeof(serv));

    // 给缓冲区设置回调
    bufferevent_setcb(bev, read_cb, write_cb, event_cb, NULL);
    //启动读缓冲区,否则读回调函数不会被调用,默认情况下,写缓冲区是启用的,读缓冲区是关闭的
    bufferevent_enable(bev, EV_READ | EV_PERSIST);

    //接受键盘输入
    // 创建一个事件  STDIN_FILENO对应于终端的读操作,因为我们向终端写入数据
    //EV_READ | EV_PERSIST 表示持续的读   send_cb是回调函数
    struct event* ev = event_new(base, STDIN_FILENO, 
                                 EV_READ | EV_PERSIST, 
                                 send_cb, bev);
    //将事件添加到base上
    //第二个参数是事件等待的时间,如果该时间内事件没被触发,则强制执行该函数,如果不设置时间,则阻塞等待事件被触发
    event_add(ev, NULL);
    //启动事件循环
    event_base_dispatch(base);

    event_base_free(base);

    return 0;
}

 

 

 

小技巧——设置代码片段,这样可以通过快捷键款速生成自己想要的代码片段

代码片段生成回调函数参数的示例

至于c.sinppets里面怎么写,写什么,以后再研究一下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值