news 2026/4/15 14:20:11

ZLToolKit模块(三) NoticeCenter(事件广播)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ZLToolKit模块(三) NoticeCenter(事件广播)

在 ZLMediaKit/ZLToolKit 的架构中,模块之间的解耦至关重要。NoticeCenter(通知中心)正是为此而生。它实现了一个观察者模式发布-订阅模式(Publish-Subscribe Pattern),充当了整个系统的“中枢神经”,负责将事件从发生地精准地投递给感兴趣的监听者。

1. 核心设计理念与原理

NoticeCenter的核心目标是:让事件的产生者(Emitter)不需要知道谁在监听,事件的消费者(Listener)也不需要知道谁发出的事件。

1.1 双层映射结构

为了高效管理成千上万的事件,NoticeCenter采用了双层 Hash Map的结构:

  1. 第一层(事件层)NoticeCenter内部维护一个unordered_map,Key 是事件名称(string),Value 是EventDispatcher(事件分发器)。
    • 作用:根据事件名快速找到对应的分发器。
  2. 第二层(监听层):每个EventDispatcher内部维护一个unordered_multimap,Key 是监听者标识(void* tag),Value 是回调函数(Any)
    • 作用:存储该特定事件的所有监听回调。

1.2 类型擦除与变参模板

C++ 是强类型语言,但通用的事件中心需要支持任意参数的回调。NoticeCenter结合了以下技术:

  • Any类型:类似于 C++17 的std::any,用于存储任意类型的函数对象(std::function),实现了回调函数的类型擦除。
  • 变参模板(Variadic Templates)emitEvent方法接受ArgsType &&...args,允许发射携带任意数量和类型参数的事件。

2. 核心类分析

2.1EventDispatcher(幕后英雄)

它是实际干活的类,对应“某一个特定事件”的管理者。

  • 职责:存储、删除、触发监听器。
  • 关键点
    • 防死锁机制:在emitEvent触发回调时,它会先拷贝一份监听列表,然后释放锁,再遍历拷贝的列表执行回调。
    • 为什么这么做?如果在回调函数中又调用了addListenerdelListener,如果不释放锁,就会导致死锁(Self-Deadlock)。
    • 中断机制:支持InterruptException。如果某个监听器抛出此异常,事件广播将立即停止,不再通知后续监听器。

2.2NoticeCenter(大管家)

它是单例模式(Singleton)的对外入口。

  • 职责:管理所有的EventDispatcher
  • 接口
    • addListener: 注册监听。
    • delListener: 移除监听(支持按 tag 移除单个,或按 tag 移除所有事件的监听)。
    • emitEvent: 发射事件。

2.3NoticeHelper(语法糖)

这是一个模板辅助类,配合宏NOTICE_EMIT使用。

  • 作用:简化代码书写,利用模板推导自动匹配参数类型,让调用看起来更像函数调用。

3. 类图概览

manages >
1
n
stores callbacks >
1
n
«Singleton»
NoticeCenter
-std::unordered_map _mapListener
-std::recursive_mutex _mtxListener
+static Instance()
+addListener(tag, event, func)
+delListener(tag, event)
+emitEvent(event, args...)
EventDispatcher
-std::unordered_multimap _mapListener
-std::recursive_mutex _mtxListener
+addListener(tag, func)
+delListener(tag, empty)
+emitEvent(safe, args...)
«Type Erasure»
Any
+set(value)
+get(safe)

4. 关键实现细节解析

4.1 线程安全与性能平衡

NoticeCenter在多线程环境下被高频使用,因此线程安全至关重要。

  • 使用了std::recursive_mutex(递归锁)。这允许同一个线程在持有锁的情况下再次获取锁,防止在复杂的嵌套调用(如回调中移除监听)中发生死锁。
  • Copy-On-Write (类似思想):如前所述,EventDispatcher::emitEvent中:
{std::lock_guard<std::recursive_mutex>lck(_mtxListener);copy=_mapListener;// 1. 临界区内只做拷贝,速度快}// 2. 临界区外执行回调,避免阻塞其他线程的添加/删除操作for(auto&pr:copy){...}

4.2tag指针的作用

addListener的第一个参数是void *tag

  • 通常用法:传入this指针。
  • 目的:当对象析构时,需要取消它注册的所有监听。通过tag(即对象的地址),NoticeCenter可以快速定位并移除该对象注册的所有回调,防止悬垂指针导致的崩溃。

5. 使用场景与示例

场景一:系统启动/停止通知

当服务器初始化完成或准备关闭时,通知各个模块。

监听者 (Listener):

classPlayer{public:Player(){// 关注 "SERVER_INIT" 事件// tag 传 this,方便析构时移除NoticeCenter::Instance().addListener(this,"SERVER_INIT",[](intport,conststring&ip){printf("Server started at %s:%d\n",ip.c_str(),port);});}~Player(){// 移除该对象的所有监听NoticeCenter::Instance().delListener(this);}};

触发者 (Emitter):

voidstartServer(){intport=8080;string ip="0.0.0.0";// 广播事件,参数自动透传NoticeCenter::Instance().emitEvent("SERVER_INIT",port,ip);}

场景二:使用宏简化调用

ZLToolKit 提供了NOTICE_EMIT宏,让代码更优雅。

// 定义事件参数签名// 这里的 void(int, string) 对应回调函数的签名NOTICE_EMIT(void(int,string),"SERVER_INIT",8080,"0.0.0.0");

场景三:中断事件传播

假设有一个鉴权事件,如果第一个监听者鉴权失败,不希望后续监听者继续处理。

NoticeCenter::Instance().addListener(this,"AUTH_USER",[](conststring&user){if(user=="admin"){// 鉴权通过}else{// 抛出中断异常,后续的回调将不会被执行throwEventDispatcher::InterruptException();}});

6. 总结

NoticeCenter是 ZLToolKit 中一个短小精悍但功能强大的组件。

  • 优点:解耦性强、线程安全、支持任意参数、支持事件中断。
  • 注意
    • 回调函数是在触发线程中执行的。如果回调处理耗时过长,会阻塞触发线程。对于耗时操作,建议在回调中抛转到线程池处理。
    • 务必在对象析构时调用delListener(this),否则会导致野指针回调崩溃。

通过理解NoticeCenter,你就能掌握 ZLMediaKit 中流媒体状态变化、Hook 机制等底层通信的脉络。

7. C++11

7.1 using和typedef的区别

using可以直接定义模板函数的别名:

template<typename T> using ptr = std::unique_ptr<T>; ptr<int> p(new int(10)); //正确 template<typename T> typedef std::unique_ptr<T> ptr; //错误 template<typename T> struct A { typedef std::unique_ptr<T> ptr; }; A<int>::ptr x(new int(10)); //正确

7.2 function_traits

function_traits是 ZLToolKit 中一个非常核心的 C++模板元编程(Template Metaprogramming)工具类。

它的主要作用是类型萃取(Type Extraction):在编译期解析出任何“可调用对象”(函数、函数指针、std::function、Lambda 表达式、仿函数、成员函数)的详细类型信息,例如返回值类型、参数个数、参数类型等。

template<typename T> struct function_traits; //普通函数, lambda表达式 template<typename Ret, typename... Args> struct function_traits<Ret(Args...)> //特化版本 { public: enum { arity = sizeof...(Args) }; typedef Ret function_type(Args...); typedef Ret return_type; using stl_function_type = std::function<function_type>; typedef Ret(*pointer)(Args...); template<size_t I> struct args { static_assert(I < arity, "index is out of range, index must less than sizeof Args"); using type = typename std::tuple_element<I, std::tuple<Args...> >::type; }; }; //函数指针 template<typename Ret, typename... Args> struct function_traits<Ret(*)(Args...)> : function_traits<Ret(Args...)>{}; //std::function template <typename Ret, typename... Args> struct function_traits<std::function<Ret(Args...)>> : function_traits<Ret(Args...)>{}; //member function #define FUNCTION_TRAITS(...) \ template <typename ReturnType, typename ClassType, typename... Args>\ struct function_traits<ReturnType(ClassType::*)(Args...) __VA_ARGS__> : function_traits<ReturnType(Args...)>{}; \ FUNCTION_TRAITS() FUNCTION_TRAITS(const) FUNCTION_TRAITS(volatile) FUNCTION_TRAITS(const volatile) //函数对象 template<typename Callable> struct function_traits : function_traits<decltype(&Callable::operator())>{};

可以通过这个类获取将任何函数类型转为stl的function类型。也可以活获取参数类型。
noticeCenter里有如下使用:

template<typename FUNC> void addListener(void *tag, FUNC &&func) { using funType = typename function_traits<typename std::remove_reference<FUNC>::type>::stl_function_type; std::shared_ptr<void> pListener(new funType(std::forward<FUNC>(func)), [](void *ptr) { funType *obj = (funType *) ptr; delete obj; }); std::lock_guard<std::recursive_mutex> lck(_mtxListener); _mapListener.emplace(tag, pListener); }

这里通过function_traitsstl_function_type得到stl的function类型,并构造了该类型的对象。

7.3 function

可以存储各种函数、函数指针、lambda 表达式等可调用对象。
例如,你可以使用std::function<int(int, float)>来存储一个参数类型为intfloat,返回值类型为int的可调用对象。

  • 将函数、函数指针、lambda 表达式等存储在一个变量中,方便传递和调用;
  • 在不确定可调用对象的具体类型的情况下,接受各种类型的可调用对象;
  • 在运行时动态绑定可调用对象;
  • 用于实现回调函数等;
  • 可以使用std::bind将函数或函数对象绑定到一个function变量中

例如:

#include <functional> int func(int x, float y) { return static_cast<int>(x + y); } int func2(int x, int y, int z) { return x + y + z; } struct FuncObj { int operator()(int x, float y) { return static_cast<int>(x * y); } }; int main() { std::function<int(int, float)> f1 = func; f1(1, 2.5f); // Output: 3 std::function<int(int, float)> f2 = [](int x, float y) { return static_cast<int>(x * y); }; f2(3, 0.5f); // Output: 1 std::function<int(int, float)> f3 = std::bind(func, std::placeholders::_1, std::placeholders::_2); std::cout << f1(1, 2.5f) << std::endl; // Output: 3 std::function<int(int, float)> f4 = std::bind(FuncObj(), std::placeholders::_1, std::placeholders::_2); std::cout << f4(3, 0.5f) << std::endl; // Output: 1 std::function<int(int)> f5 = std::bind(func2, 1, 2, std::placeholders::_1); std::cout << f5(3) << std::endl; // Output: 6 return 0; }

7.4 tuple_element

可以用于提取元组类型中某一位置的元素的类型。

using TupleType = std::tuple<int, float, double>; using ElementType = std::tuple_element<1, TupleType>::type; std::cout << typeid(ElementType).name() << std::endl; // Output: "float"
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/11 9:09:10

37.泛化实现(上)-泛化数据库设计单表vs多表策略-性能对比分析

37 泛化实现(上):泛化的数据库设计(单表 vs 多表策略) 你好,欢迎来到第 37 讲。 在完成了对泛化建模的“道”(适用场景)和“法”(权衡抉择)的探讨之后,我们终于来到了“术”的层面——实现。 假设经过审慎的权衡,我们最终决定在领域模型中使用**泛化(继承)**来…

作者头像 李华
网站建设 2026/4/12 23:38:49

38.泛化实现(下)-泛化代码实现与继承封装技巧-附设计模式应用

38 泛化实现(下):泛化的代码实现与继承封装技巧 你好,欢迎来到第 38 讲。 在上一讲中,我们已经为泛化模型,设计了两种主流的数据库持久化策略:单表继承和类表继承。 现在,是时候将这些策略,通过代码真正地实现出来了。本讲,我们将聚焦于泛化实现的代码层面,以 Ja…

作者头像 李华
网站建设 2026/4/11 5:56:56

41.迭代三概述-大规模系统中DDD如何支撑架构演进-从单体到微服务的演进路径

41 迭代三概述:大规模系统中,DDD 如何支撑架构演进? 你好,欢迎来到课程的第四大部分——架构升级。 如果说,第一部分“基础筑基”是让我们学会了 DDD 的“基本功”,第二部分“核心突破”是让我们精通了构建健壮模型的“招式”,那么从这一部分开始,我们将从“战术”层…

作者头像 李华
网站建设 2026/4/15 5:31:19

49.CQRS入门(下)-CQRS适用场景与收益-什么时候该用CQRS

49 CQRS 入门(下):CQRS 的适用场景与收益 你好,欢迎来到第 49 讲。 在上一讲,我们已经理解了 CQRS 的核心思想——将改变系统状态的**命令(Command)和不改变状态的查询(Query)**进行彻底的分离。我们知道,这种分离可以为我们带来模型清晰、可独立优化等一系列好处。…

作者头像 李华
网站建设 2026/4/12 7:40:19

46.上下文映射-限界上下文之间的5种集成模式-附适用场景与代码示例

46 上下文映射:限界上下文之间的 5 种集成模式(附适用场景) 你好,欢迎来到第 46 讲。 通过前面的学习,我们已经掌握了如何使用“限界上下文”这把手术刀,将一个庞大的业务系统,精准地切割成一个个高内聚、低耦合的业务单元(未来的微服务)。 但是,切割完成之后,工…

作者头像 李华