网络编程:反应堆reactor框架设计
https://app.yinxiang.com/fx/7e601cad-6501-4fe7-8e4e-f0fbd9d02c4b
TCP 高性能网络框架需要满足的需求有以下三点:
1)采用 reactor 模型,可以灵活使用 poll/epoll 作为事件分发实现。
2)必须支持多线程,从而可以支持单线程单 reactor 模式,也可以支持多线程主 - 从 reactor 模式。可以将套接字上的 I/O 事件分离到多个线程上。
3)封装读写操作到 Buffer 对象中。
按照这三个需求,正好可以把整体设计思路分成三块来讲解,分别包括反应堆模式设计、I/O 模型和多线程模型设计、数据读写封装和 buffer。
主要设计思路
反应堆模式设计
反应堆模式主要是设计一个基于事件分发和回调的反应堆框架。这个框架里面的主要对象包括:
event_loop
它就是一个无限循环着的事件分发器,一旦有事件发生,它就会回调预先定义好的回调函数,完成事件的处理。具体来说,event_loop 使用 poll 或者 epoll 方法将一个线程阻塞,等待各种 I/O 事件的发生。channel
对各种注册到 event_loop 上的对象,我们抽象成 channel 来表示,例如注册到 event_loop 上的监听事件,注册到 event_loop 上的套接字读写事件等。在各种语言的 API 里,你都会看到 channel 这个对象,大体上它们表达的意思跟我们这里的设计思路是比较一致的。acceptor
acceptor 对象表示的是服务器端监听器,acceptor 对象最终会作为一个 channel 对象,注册到 event_loop 上,以便进行连接完成的事件分发和检测。event_dispatcher
event_dispatcher 是对事件分发机制的一种抽象,也就是说,可以实现一个基于 poll 的 poll_dispatcher,也可以实现一个基于 epoll 的 epoll_dispatcher。在这里,我们统一设计一个 event_dispatcher 结构体,来抽象这些行为。channel_map
channel_map 保存了描述字到 channel 的映射,这样就可以在事件发生时,根据事件类型对应的套接字快速找到 channel 对象里的事件处理函数。
I/O 模型和多线程模型设计
I/O 线程和多线程模型,主要解决event_loop 的线程运行问题,以及事件分发和回调的线程执行问题。
thread_pool
thread_pool 维护了一个 sub-reactor 的线程列表,它可以提供给主 reactor 线程使用,每次当有新的连接建立时,可以从 thread_pool 里获取一个线程,以便用它来完成对新连接套接字的 read/write 事件注册,将 I/O 线程和主 reactor 线程分离。event_loop_thread
event_loop_thread 是 reactor 的线程实现,连接套接字的 read/write 事件检测都是在这个线程里完成的。
Buffer 和数据读写
buffer
buffer 对象屏蔽了对套接字进行的写和读的操作,如果没有 buffer 对象,连接套接字的 read/write 事件都需要和字节流直接打交道,这显然是不友好的。所以,我们也提供了一个基本的 buffer 对象,用来表示从连接套接字收取的数据,以及应用程序即将需要发送出去的数据。tcp_connection
tcp_connection 这个对象描述的是已建立的 TCP 连接。它的属性包括接收缓冲区、发送缓冲区、channel 对象等。这些都是一个 TCP 连接的天然属性。
tcp_connection 是大部分应用程序和我们的高性能框架直接打交道的数据结构。我们不想把最下层的 channel 对象暴露给应用程序,因为抽象的 channel 对象不仅仅可以表示 tcp_connection,前面提到的监听套接字也是一个 channel 对象,后面提到的唤醒 socketpair 也是一个 channel 对象。所以,我们设计了 tcp_connection 这个对象,希望可以提供给用户比较清晰的编程入口。
反应堆模式设计
概述
以 event_loop 为核心的反应堆模式设计,这里有一张 event_loop 的运行详图:

当 event_loop_run 完成之后,线程进入循环,首先执行 dispatch 事件分发,一旦有事件发生,就会调用 channel_event_activate 函数,在这个函数中完成事件回调函数 eventReadcallback 和 eventWritecallback 的调用,最后再进行 event_loop_handle_pending_channel,用来修改当前监听的事件列表,完成这个部分之后,又进入了事件分发循环。
event_loop 分析
在这个数据结构中,最重要的莫过于 event_dispatcher 对象了,可以简单地把 event_dispatcher 理解为 poll 或者 epoll,它可以让我们的线程挂起,等待事件的发生
这里有一个小技巧,就是 event_dispatcher_data,它被定义为一个 void * 类型,可以按照我们的需求,任意放置一个我们需要的对象指针。这样,针对不同的实现,例如 poll 或者 epoll,都可以根据需求,放置不同的数据对象。
event_loop 中还保留了几个跟多线程有关的对象,如 owner_thread_id 是保留了每个 event loop 的线程 ID,mutex 和 con 是用来进行线程同步的。
socketPair 是父线程用来通知子线程有新的事件需要处理。pending_head 和 pending_tail 是保留在子线程内的需要处理的新事件。
struct event_loop {
int quit;
const struct event_dispatcher *eventDispatcher;
/** 对应的event_dispatcher的数据. */
void *event_dispatcher_data;
struct channel_map *channelMap;
int is_handle_pending;
struct channel_element *pending_head;
struct channel_element *pending_tail;
pthread_t owner_thread_id;
pthread_mutex_t mutex;
pthread_cond_t cond;
int socketPair[2];
char *thread_name;
};
event_loop 最主要的方法 event_loop_run 方法,event_loop 就是一个无限 while 循环,不断地在分发事件
/**
*
* 1.参数验证
* 2.调用dispatcher来进行事件分发,分发完回调事件处理函数
*/
int event_loop_run(struct event_loop *eventLoop) {
assert(eventLoop != NULL);
struct event_dispatcher *dispatcher = eventLoop->eventDispatcher;
if (eventLoop->owner_thread_id != pthread_self()) {
exit(1);
}
yolanda_msgx("event loop run, %s", eventLoop->thread_name);
struct timeval timeval;
timeval.tv_sec = 1;
while (!eventLoop->quit) {
//block here to wait I/O event, and get active channels
dispatcher->dispatch(eventLoop, &timeval);
//handle the pending channel
event_loop_handle_pending_channel(eventLoop);
}
yolanda_msgx("event loop end, %s", eventLoop->thread_name);
return 0;
}
代码很明显地反映了这一点,这里我们在 event_loop 不退出的情况下,一直在循环,循环体中调用了 dispatcher 对象的 dispatch 方法来等待事件的发生。
event_dispacher 分析
为了实现不同的事件分发机制,这里把 poll、epoll 等抽象成了一个 event_dispatcher 结构。event_dispatcher 的具体实现有 poll_dispatcher 和 epoll_dispatcher 两种
/** 抽象的event_dispatcher结构体,对应的实现如select,poll,epoll等I/O复用. */
struct event_dispatcher {
/** 对应实现 */
const char *name;
/** 初始化函数 */
void *(*init)(struct event_loop * eventLoop);
/** 通知dispatcher新增一个channel事件*/
int (*add)(struct event_loop * eventLoop, struct channel * channel);
/** 通知dispatcher删除一个channel事件*/
int (*del)(struct event_loop * eventLoop, struct channel * channel);
/** 通知dispatcher更新channel对应的事件*/
int (*update)(struct event_loop * eventLoop, struct channel * channel);
/** 实现事件分发,然后调用event_loop的event_activate方法执行callback*/
int (*dispatch)(struct event_loop * eventLoop, struct timeval *);
/** 清除数据 */
void (*clear)(struct event_loop * eventLoop);
};
channel 对象分析
channel 对象是用来和 event_dispather 进行交互的最主要的结构体,它抽象了事件分发。一个 channel 对应一个描述字,描述字上可以有 READ 可读事件,也可以有 WRITE 可写事件。channel 对象绑定了事件处理函数 event_read_callback 和 event_write_callback。
typedef int (*event_read_callback)(void *data);
typedef int (*event_write_callback)(void *data);
struct channel {
int fd;
int events; //表示event类型
event_read_callback eventReadCallback;
event_write_callback eventWriteCallback;
void *data; //callback data, 可能是event_loop,也可能是tcp_server或者tcp_connection
};
channel_map 对象分析
event_dispatcher 在获得活动事件列表之后,需要通过文件描述字找到对应的 channel,从而回调 channel 上的事件处理函数 event_read_callback 和 event_write_callback,为此,设计了 channel_map 对象。
/**
* channel映射表, key为对应的socket描述字
*/
struct channel_map {
void **entries;
/* The number of entries available in entries */
int nentries;
};
channel_map 对象是一个数组,数组的下标即为描述字,数组的元素为 channel 对象的地址。
比如描述字 3 对应的 channel,就可以这样直接得到。
struct chanenl * channel = map->entries[3];
这样,当 event_dispatcher 需要回调 channel 上的读、写函数时,调用 channel_event_activate 就可以,下面是 channel_event_activate 的实现,在找到了对应的 channel 对象之后,根据事件类型,回调了读函数或者写函数。注意,这里使用了 EVENT_READ 和 EVENT_WRITE 来抽象了 poll 和 epoll 的所有读写事件类型。
int channel_event_activate(struct event_loop *eventLoop, int fd, int revents) {
struct channel_map *map = eventLoop->channelMap;
yolanda_msgx("activate channel fd == %d, revents=%d, %s", fd, revents, eventLoop->thread_name);
if (fd < 0)
return 0;
if (fd >= map->nentries)return (-1);
struct channel *channel = map->entries[fd];
assert(fd == channel->fd);
if (revents & (EVENT_READ)) {
if (channel->eventReadCallback) channel->eventReadCallback(channel->data);
}
if (revents & (EVENT_WRITE)) {
if (channel->eventWriteCallback) channel->eventWriteCallback(channel->data);
}
return 0;
}
增加、删除、修改 channel event
那么如何增加新的 channel event 事件呢?下面这几个函数是用来增加、删除和修改 channel event 事件的。
int event_loop_add_channel_event(struct event_loop *eventLoop, int fd, struct channel *channel1);
int event_loop_remove_channel_event(struct event_loop *eventLoop, int fd, struct channel *channel1);
int event_loop_update_channel_event(struct event_loop *eventLoop, int fd, struct channel *channel1);
前面三个函数提供了入口能力,而真正的实现则落在这三个函数上:
int event_loop_handle_pending_add(struct event_loop *eventLoop, int fd, struct channel *channel);
int event_loop_handle_pending_remove(struct event_loop *eventLoop, int fd, struct channel *channel);
int event_loop_handle_pending_update(struct event_loop *eventLoop, int fd, struct channel *channel);
看一下其中的一个实现,event_loop_handle_pending_add 在当前 event_loop 的 channel_map 里增加一个新的 key-value 对,key 是文件描述字,value 是 channel 对象的地址。之后调用 event_dispatcher 对象的 add 方法增加 channel event 事件。注意这个方法总在当前的 I/O 线程中执行。
// in the i/o thread
int event_loop_handle_pending_add(struct event_loop *eventLoop, int fd, struct channel *channel) {
yolanda_msgx("add channel fd == %d, %s", fd, eventLoop->thread_name);
struct channel_map *map = eventLoop->channelMap;
if (fd < 0)
return 0;
if (fd >= map->nentries) {
if (map_make_space(map, fd, sizeof(struct channel *)) == -1)
return (-1);
}
//第一次创建,增加
if ((map)->entries[fd] == NULL) {
map->entries[fd] = channel;
//add channel
struct event_dispatcher *eventDispatcher = eventLoop->eventDispatcher;
eventDispatcher->add(eventLoop, channel);
return 1;
}
return 0;
}
网络编程:反应堆reactor框架设计的更多相关文章
- iOS:网络编程的第三方框架:AFNetworking、SDWebImage
网络编程第三方框架:AFNetworking.SDWebImage 介绍:这些框架是开源的,经过前人的封装.改进,成为使用次数很多的一个性能好的源代码框架,只需要将它导入项目中,就可以使用.因此,在做 ...
- 前端思想实现:面向UI编程_____前端框架设计开发
引子,我去小说看多了,写博客竟然写引子了!!!不过,没引子不知道怎么写了.言归正传吧,前端这个职业,也就这几年刚刚火起来的职业,以前那个混乱的年代,前端要么是UI设计师代劳解决问题,要么就是后端程序员 ...
- 两种unix网络编程线程池的设计方法
unp27章节中的27.12中,我们的子线程是通过操作共享任务缓冲区,得到task的,也就是通过线程间共享的clifd[]数组,这个数组其实就是我们的任务数组,得到其中的connfd资源. 我们对这个 ...
- Java SE之网络编程:知识框架
- 这份书单会告诉你,Java网络编程其实很重要
- Java网络编程和NIO详解9:基于NIO的网络编程框架Netty
Java网络编程和NIO详解9:基于NIO的网络编程框架Netty 转自https://sylvanassun.github.io/2017/11/30/2017-11-30-netty_introd ...
- java网络编程框架
虽然写过一些网络编程方面的东西,但还没有深入研究过这方面的内容,直接摘录一些文章,后续整理 原文地址:http://blog.csdn.net/lwuit/article/details/730613 ...
- POCO库中文编程参考指南(11)如何使用Reactor框架?
1 Reactor 框架概述 POCO 中的 Reactor 框架是基于 Reactor 设计模式进行设计的.其中由 Handler 将某 Socket 产生的事件,发送到指定的对象的方法上,作为回调 ...
- (46)LINUX应用编程和网络编程之一Linux应用编程框架
3.1.1.应用编程框架介绍 3.1.1.1.什么是应用编程 (1)整个嵌入式linux核心课程包括5个点,按照学习顺序依次是:裸机.C高级.uboot和系统移植.linux应用编程和网络编程.驱动. ...
- Java Socket 网络编程心跳设计概念
Java Socket 网络编程心跳设计概念 1.一般是用来判断对方(设备,进程或其它网元)是否正常动行,一 般采用定时发送简单的通讯包,如果在指定时间段内未收到对方响应,则判断对方已经当掉.用于 ...
随机推荐
- Azkaban - [01] 概述
简单的任务调度使用crontab.复杂的任务调度使用oozie.azkaban等开发调度系统. 一.为什么学习Azkaban 一个完整的数据分析系统通常都是由大量任务单元(shell脚本.java ...
- Redis集群(cluster模式)搭建(三主三从)
上一篇搭建了一主二从,并加入了哨兵,任何一个节点挂掉都不影响正常使用,实现了高可用.仍然存在一个问题,一主二从每个节点都存储着全部数据,随着业务庞大,数据量会超过节点容量,即便是redis可以配置清理 ...
- 李沐动手学深度学习V2-chapter_linear-networks
李沐动手学深度学习V2 文章内容说明 本文主要是自己学习过程中的随手笔记,需要自取 课程参考B站:https://space.bilibili.com/1567748478?spm_id_from=3 ...
- Avalanche公链深度解析:创新共识、亚秒级最终性与生态竞争力
摘要:Avalanche定位为一个高性能.可扩展的Layer 1区块链平台,但它并不是一个新公链,其主网于2020年9月21日正式上线,有Ava Labs开发.Ava Labs成立于2018年,总部位 ...
- nnUNet相关方法
- 【FAQ】HarmonyOS SDK 闭源开放能力 —Map Kit(6)
1.问题描述: 使用华为内置的MapComponent, 发现显示不出来.查看日志, MapRender底层有报错. 解决方案: 麻烦按以下步骤检查下地图服务,特别是签名证书指纹那部分. 1.一般没有 ...
- 【FAQ】HarmonyOS SDK 闭源开放能力 —Push Kit(10)
1.问题描述: 离线推送,锁屏的时候没有弹出消息,只有下拉在通知中心里面显示.请问是否是正常的? 解决方案: 检查一下是否存在图片风控:https://developer.huawei.com/con ...
- linux命令:lsof命令
lsof(list open files)是一个列出当前系统打开文件的工具.在linux环境下,任何事物都以文件的形式存在,通过文件不仅仅可以访问常规数据,还可以访问网络连接和硬件.所以如传输控制协议 ...
- 项目实战 TS
项目实战 TS 通用技巧 新手先 any 再填坑,老手先定义数据结构写逻辑 遇到新场景,没把握快速,先用 any 再填坑,填坑的过程也是 TS 技能满满提升的过程. TS 发现潜在问题 1)复杂逻辑, ...
- ASP.NET Core 阿弥陀佛中间件
Amitabha n. <梵>(佛)阿弥陀佛 佛曰: 因果 那么一天 看见文章1. 发现还有2.这么个中间件 于是追寻源码,发现了3. 再然后寻思自己也可以写一个什么中间件,以便学习掌握 ...