C++学习之本地套接字libevent
1.本地套接字介绍
``c
本地套接字是进程间通信的一种实现方式:
- 管道: 有名管道, 匿名管道 -> 简单, 推荐使用
- 内存映射区 -> 不阻塞
- 信号 -> 携带的信息量少, 并且信号优先级太高, 打乱程序的执行顺序 -> 不推荐
- 本地套接字 -> 基于网络套接字来实现的
- 程序实现比较复杂
- 共享内存 -> 项目1中会用, 效率很高, 但是也不阻塞
- 消息队列 -> 没讲
本地套接字实现方式:
- 基于tcp实现 -> 推荐
- 不需要使用IP和端口, 需要使用套接字文件
- 套接字文件中不存储数据(类似于有名管道文件)
- 数据在内核的某块内存中存储着, 套接字文件关联着内核中的内存
- 通过文件描述符就可以对内核的内存进行读写了
- 基于udp实现
```
2.本地套接字服务器端通信流程
3.本地套接字客户端通信流程
- 结构体
```c
// 头文件: sys/un.h
#include <sys/un.h>
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family; // AF_UNIX, AF_LOCAL
char sun_path[UNIX_PATH_MAX]; // 套接字文件的路径(绝对/相对)
};
```
- 通信流程 - 基于tcp
4.本地套接字客户端和服务器代码
- 服务器端
```c
// 1. 创建监听的套接字(文件描述符)
int lfd = socket(AF_UNIX, SOCK_STREAM, 0);
- 第一个参数使用: AF_UNIX 或者 AF_LOCAL
- 第二个参数: 基于tcp使用: SOCK_STREAM, 基于udp使用: SOCK_DGRAM
- 第三个参数: 0
// 2. 监听的套接字和本地的套接字文件绑定
// 本地套接字文件不需要程序猿创建, 绑定成功会自动生成
struct sockaddr_un addr;
bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
// 3. 设置监听
listen();
// 4. 等待并接受连接
// 第二个参数传出的是客户端进程绑定的套接字文件的地址
accept(lfd, (struct sockaddr*)&cliaddr, &len);
// 5. 通信
接收数据: read/recv
发送数据: write/send
// 6. 断开连接, 关闭文件描述符
close();
```
5.本地套接字测试-bug修改
- 客户端
```c
// 1. 创建通信的套接字(文件描述符)
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
- 第一个参数使用: AF_UNIX 或者 AF_LOCAL
- 第二个参数: 基于tcp使用: SOCK_STREAM, 基于udp使用: SOCK_DGRAM
- 第三个参数: 0
// 2. 通信的套接字和本地的套接字文件绑定 -> 客户端进程也需要一个套接字文件
// 本地套接字文件不需要程序猿创建, 绑定成功会自动生成
struct sockaddr_un addr;
bind(fd, (struct sockaddr*)&addr, sizeof(addr));
// 3. 连接服务器进程
struct sockaddr_un seraddr; // 初始化服务器绑定的套接字文件
connect(fd, (struct sockaddr*)&seraddr, sizeof(seraddr));
6.进程间通信的场景
// 4. 通信
接收数据: read/recv
发送数据: write/send
// 5. 断开连接, 关闭文件描述符
close();
7.libevent特点介绍
- 特点
> Libevent 是一个用C语言编写的、轻量级的开源高性能事件的框架,主要有以下几个亮点:
>
> - 事件驱动( event-driven),高性能;
> - 基于回调
> - 需要编写函数的处理动作, 调用程序猿不需要管(程序猿不知道调用的时机)
> - 当某个具体的事件产生之后(事件的产生是随机的), 这个函数才会被调用
> - 我可以将这个事件的处理动作注册给操作系统或者是框架
> - 委托框架或者操作系统检测这个事件, 事件被检测到之后, 处理函数被调用
> - 轻量级,专注于网络;
> - 源代码相当精炼、易读;
> - 现阶段不建议看
> - 如果看建议看低版本的代码
> - 跨平台,支持 Windows、 Linux、 BSD(是Unix的衍生系统) 和 Mac OS;
> - 支持多种 I/O 多路复用技术, epoll、 poll、 select 和 kqueue 等;
> - linux/max: epoll、 poll, select
> - windows: select
> - BSD: kqueue
> - I/O操作是异步的, 并且是非阻塞
> - read/recv 接收数据, 不是数据到达之后马上就接收了
> - 委托内核检测 -> 内核检测到了之后 -> 通知程序猿写的程序 -> 开始处理
> - 在单线程/进程的IO转接服务器端, 文件描述的处理是线性的
> - 不是时时的通信
> - 支持 I/O,定时器和信号等事件;
> - IO: 读事件, 写事件
> - 定时器: 超时处理, 强制解除阻塞的
> - 信号: linux中的信号, window中没有信号
> - 异常事件
> - 支持注册事件优先级。
> - 可以通过提供的函数进行优先级设置
> - 默认只有一个优先级, 所有事件默认优先级相同
8.libevent的安装和解决动态库找不到的问题
```shell
# 下载地址: http://libevent.org/
# 源码的方式进行安装
- 将压缩包解压: libevent-2.1.8-stable.tar.gz
$ tar zxvf libevent-2.1.8-stable.tar.gz
- 进入到解压目录 - libevent-2.1.8-stable
$ cd libevent-2.1.8-stable
- 找 README / README.md / INSTALL -> 从里边找安装步骤
- 如果没找到, 安照默认的安装流程安装即可
# 标准的源码安装:
1. 从当前源码目录中找一个叫 configure 可执行文件, 执行该文件
$ ./configure # 检测当前系统的安装环境, 检测没问题会生成 makefile 文件
2. 根据makefile中的规则构建项目
# 构建之后会得到可执行程序/动态库/静态库
$ make
3. 安装
- 头文件: 从安装目录拷贝到系统目录头文件目录
- 库文件: 动态库/静态库, 从安装目录拷贝到系统的库目录
- 可执行程序: 从安装目录拷贝到系统的二进制目录中 /bin , /usr/bin
$ sudo make install # 实际就是文件拷贝
9.事件处理框架的创建
3. 安装
- 头文件: 从安装目录拷贝到系统目录头文件目录
- 库文件: 动态库/静态库, 从安装目录拷贝到系统的库目录
- 可执行程序: 从安装目录拷贝到系统的二进制目录中 /bin , /usr/bin
$ sudo make install # 实际就是文件拷贝
# 如何验证这个库装好了, 并且可以使用?
进入到安装目录的 sample 目录中, 可以编译 hello-world.c , 动态库名叫: libevent.so
$ gcc hello-world.c -o hello -levent
10.启动事件循环event_base_dispatch()函数
# 2. 事件处理框架 - event_base
> 使用 libevent函数之前需要分配一个或者多个 event_base 结构体。每个event_base 结构体持有一个事件集合,可以检测以确定哪个事件是激活的。每个 event_base 都有一种用于检测哪种事件已经就绪的 “方法”,或者说后端。
- API函数
> 在 event_base 底层封装了IO多路转接模型, 并且可以对事件进行检测和处理的
```c
// 头文件
#include <event2/event.h>
// 创建一个事件处理框架
struct event_base * event_base_new(void);
// 释放一个事件处理框架
// 参数: event_base_new() 函数的返回值
void event_base_free(struct event_base * base);
// 查看底层支持的IO转接模型
const char** event_get_supported_methods(void);
// 查看当前实际处理框架使用的IO转接模型
const char *event_base_get_method(const struct event_base *base);
```
11.事件的终止函数
```c
// 测试程序
int main()
{
// 1. 创建事件处理框架
struct event_base* base = event_base_new();
// 2. 查看支持什么样的IO转接函
// 二级指针指向: char * xx[];
const char** methods = event_get_supported_methods();
printf("支持的IO转接模型: \n");
for(int i=0; methods[i] != NULL; ++i)
{
printf(" %s\n", methods[i]);
}
printf("当前event_base使用的IO转接模型: %s\n", event_base_get_method(base));
// 释放资源
event_base_free(base);
return 0;
}
```
12.事件的创建和销毁struct event
> event_base不停的检测委托的检测是实际是不是发生了, 如果发生了, event_base会调用对应的回调函数, 这个回调函数的用户委托检测事件的时候给的.
- 启动事件循环
```c
// 头文件
#include <event2/event.h>
// 启动事件处理框架中的事件循环
// 参数: event_base_new() 的返回值
/*
事件检测过程中的特点:
举例: 检测读事件
- 如果对方没有发送数据过来, 会一直持续检测, 等待事件被触发
- 检测到有读事件, 默认情况下只会被处理一次, 事件循环就停止了, 只能接收一次数据
- 如果要持续的接收数据, 需要额外设置, 给创建的事件指定 EV_PERSIST 属性
*/
// 这个函数被调用, 内部会进行事件检测, 代码阻
13.将创建的事件添加到事件处理框架event_add()
// 这个函数被调用, 内部会进行事件检测, 代码阻塞在这一行
// 条件满足, 检测的就停止了, 应用程序就结束了
int event_base_dispatch(struct event_base* base);
// 相似的操作 -qt
int main()
{
QApplication a;
a.exec(); // 应用程序对象开始内部的事件循环
return 0;
}
```
14.事件和事件处理框架之间的框架
- 终止事件循环
```c
// 头文件
#include <event2/event.h>
// 表示一个时间段, tv_sec + tv_usec
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微秒
};
// 假设事件循环正在继续, 并且在处理一个实际有效的事件, 代用这个函数终止事件循环
// 事件循环不会马上终止, 在tv指定的时长之后, 事件就别终止了
int event_base_loopexit(struct event_base * base, const struct timeval * tv);
参数:
- base: 事件处理框架
- tv: 在指定的时间段之后, 退出事件循环
// 马上终止事件循环
int event_base_loopbreak(struct event_base * base);
```
15.通过event事件写管道代码
# 4. 事件
- 事件的创建和释放
```c
// 头文件
#include <event2/event.h>
#define EV_TIMEOUT 0x01 // 定时器超时
#define EV_READ 0x02 // 读, 检测读缓冲区是否有数据
#define EV_WRITE 0x04 // 写, 检测写缓冲区是否可写
#define EV_SIGNAL 0x08 // 信号
#define EV_PERSIST 0x10 // 设置事件被重复检测
#define EV_ET 0x20 // 边沿模式
16.使用EVENT事件读写管道测试
// 事件的处理函数, 被libevent框架调用, 实参的指定不是程序猿做的
// 程序猿可以直接使用回调函数的参数 ===> 需要知道参数的意义
typedef void (*event_callback_fn)(evutil_socket_t fd, short what, void *arg);
参数:
- fd: 文件描述符, 是event_new() 第二个参数
- what: 记录了实际文件描述符触发的事件
- arg: event_new() 的最后一个参数
// 创建一个需要检测的事件, struct event 就是创建出的事件
// evutil_socket_t == int
// event_new()函数的本质就是对一个文件描述符进行封装
// 事件被创建之后, 不能被事件处理框架直接检测
struct event* event_new(struct event_base * base,evutil_socket_t fd,
short what,event_callback_fn cb,void * arg);
17.event_add第二个参数超时时长的说明
参数:
- base: 事件处理框架
- fd: 一个文件描述符, 比如管道的文件描述符, 套接字通信的文件描述符
- what: 要检测的事件, 检测第二个参数 fd 的事件
- EV_READ: 读事件
- EV_WRITE: 写事件
- EV_SIGNAL: 信号事件(linux)
- EV_ET: 设置边沿模式
- cb: 函数指针对应一个回调函数, 当检测的事件被触发, 这个函数就被调用
- arg: 作为实参传递给回调函数cb
// 释放事件资源
void event_free(struct event * event);
18.struct event中的超时处理
- 事件的添加和删除
```c
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微妙
};
// 事件被创建之后, 不能被事件处理框架直接检测, 需要做添加处理
int event_add(struct event * ev,const struct timeval * tv);
参数:
- ev: 通过 event_new() 创建得到的事件
- tv: 超时时长, 如果不用指定为NULL
- 检测了一个事件, 并且在tv指定的时间段中没有被触发, 这个事件对应的回调函数会被强制调用
- 如果指定为NULL, 事件不触发, 对应的回调函数就不被调用
// 将检测的事件从事件处理框架上删除
int event_del(struct event * ev);
19.带缓冲的事件bufferevent
20.创建带缓冲的事件bufferevent_socket_new()
```c
/*
事件和事件处理框架的关系: struct event 和 struct event_base
1. 创建事件处理框架
struct event_base* base = event_base_new();
2. 创建事件
struct event* ev = event_new();
3. 添加到事件处理框架中
event_add()
4. 启动事件循环
event_base_dispatch();
- 添加的事件默认只能被检测一次
- 如果要持续检测
- 事件处理完毕之后, 再次添加 event_add()
- 在创建事的时候, 指定为持续检测 EV_PERSIST
5. 释放资源
event_free();
event_base_free();
*/