skynet 中 mongo 模块运作的底层原理解析

文章目录

    • 前言
    • 总览
      • 全流程图
      • 涉及模块关系
      • 连接数据库函数调用流程图
      • 数据库操作函数调用流程图
      • 涉及到的代码文件
    • 建立连接
      • SCRAM
      • SASL
    • 操作数据库
    • 结语
    • 参考链接

前言

这篇文章总结 skynet 中 mongo 的接入流程,代码解析,读完它相信你对 skynet 中的 mongo 调用会更得心应手。

总览

先从宏观的角度介绍下 skynet 中 mongo 模块涉及的组件和代码文件,以及组件间的层次结构。然后再对关键接口进行详细分析。

全流程图

在这里插入图片描述

涉及模块关系

在这里插入图片描述

连接数据库函数调用流程图

在这里插入图片描述

数据库操作函数调用流程图

举例插入数据接口 insert
在这里插入图片描述

涉及到的代码文件

lua side:

  • lualib/skynet/db/mongo.lua 业务层用到的数据库接口
  • lualib/skynet/socketchannel.lua 业务层用到的网络会话接口(维护连接状态,隐藏 socket 细节)
  • lualib/skynet/socket.lua 业务层用到的 socket 接口(封装 c 层提供的 socket 操作接口)

c side:

  • lualib-src/lua-bson.c bson 序列化/反序列化接口具体实现
  • lualib-src/lua-mongo.c mongo 消息序列化/反序列化接口具体实现
  • lualib-src/lua-socket.c lua 服务的网络调用接口封装
  • skynet-src/skynet_socket.c 网络模块对 skynet 服务提供支持的接口封装

建立连接

我们的业务要想可以操作数据库,首先得需要跟数据库建立连接。对于 skynet 中的 lua 服务来说,跟数据库建立连接很简单,只需要调用 mongo.client({}) 接口即可,如下:

dbL = mongo.client({host = "127.0.0.1", port = 27017,authdb = "admin",username = "my_username",password = "my_password",authmod = "my_authmechanism", -- 认证方式,可选 mongodb_cr 和 scram_sha1,默认为 scram_sha1}
)["my_dbname"]

连接接口所调用的流程图前面已经展示了,现在将其中重要的几个函数拿出来介绍一下:

-- file: lualib/skynet/socketchannel.lualocal function connect_once(self)...local function _connect_once(self, addr)-- 1. 建立连接local fd,err = socket.open(addr.host, addr.port)if not fd then...end...self.__sock = setmetatable( {fd} , self.__socket_meta )self.__dispatch_thread = skynet.fork(function()...end)if self.__auth thenself.__authcoroutine = coroutine.running()-- 2. 向数据库申请认证local ok , message = pcall(self.__auth, self)if not ok then...endself.__authcoroutine = falseif ok then...endendreturn trueend_add_backup()return _connect_once(self, { host = self.__host, port = self.__port })
end

这里有两个关键函数调用

  • socket.open(addr.host, addr.port) 建立连接
  • pcall(self.__auth, self) 向数据库申请认证

第一个函数的作用是通知网络模块创建一个 socket 对象,并且调用 connect 接口完成跟数据库的连接,背后网络模块的工作机制就不在这里赘述了,感兴趣的同学可以看一下skynet 网络模块解析:

-- file: lualib-src/lua-socket.cstatic int
lconnect(lua_State *L) {size_t sz = 0;const char * addr = luaL_checklstring(L,1,&sz);char tmp[sz];int port = 0;const char * host = address_port(L, tmp, addr, 2, &port);if (port == 0) {return luaL_error(L, "Invalid port");}struct skynet_context * ctx = lua_touserdata(L, lua_upvalueindex(1)); // skynet 风格,将服务注册为该服务启动的 c 库函数的上值,这样函数调用时,就可以直接从上值获取当前服务句柄int id = skynet_socket_connect(ctx, host, port);  // 调用网络模块提供给服务的接口lua_pushinteger(L, id);return 1;
}

第二个函数相对来说就比较复杂了,作用是向数据库发起认证,认证的方式支持 mongodb_cr 和 scram_sha1 两种,我们这里只介绍默认的认证方式 scram_sha1:

-- file: lualib/skynet/db/mongo.luafunction auth_method:auth_scram_sha1(username,password)--***************************************************************************-- 1. 这一块是按照 SCRAM 认证规格书约定的认证数据编码格式序列化我们传入的用户名和密码local user = string.gsub(string.gsub(username, '=', '=3D'), ',' , '=2C')local nonce = crypt.base64encode(crypt.randomkey())local first_bare = "n="  .. user .. ",r="  .. noncelocal sasl_start_payload = crypt.base64encode("n,," .. first_bare)--***************************************************************************--***************************************************************************-- 2. 这里调用 saslStart 指令向数据库发起认证的第一步,将用户名按照 SCRAM 规范编码后上传给数据库服务器local r = self:runCommand("saslStart",1,"autoAuthorize",1,"mechanism","SCRAM-SHA-1","payload",sasl_start_payload)if r.ok ~= 1 thenreturn falseend--***************************************************************************local conversationId = r['conversationId']local server_first = r['payload']local parsed_s = crypt.base64decode(server_first)local parsed_t = {}for k, v in string.gmatch(parsed_s, "(%w+)=([^,]*)") doparsed_t[k] = vendlocal iterations = tonumber(parsed_t['i'])local salt = parsed_t['s']local rnonce = parsed_t['r']if not string.sub(rnonce, 1, 12) == nonce thenskynet.error("Server returned an invalid nonce.")return falseendlocal without_proof = "c=biws,r=" .. rnoncelocal pbkdf2_key = md5.sumhexa(string.format("%s:mongo:%s",username,password))local salted_pass = salt_password(pbkdf2_key, crypt.base64decode(salt), iterations)local client_key = crypt.hmac_sha1(salted_pass, "Client Key")local stored_key = crypt.sha1(client_key)local auth_msg = first_bare .. ',' .. parsed_s .. ',' .. without_prooflocal client_sig = crypt.hmac_sha1(stored_key, auth_msg)local client_key_xor_sig = crypt.xor_str(client_key, client_sig)local client_proof = "p=" .. crypt.base64encode(client_key_xor_sig)local client_final = crypt.base64encode(without_proof .. ',' .. client_proof)local server_key = crypt.hmac_sha1(salted_pass, "Server Key")local server_sig = crypt.base64encode(crypt.hmac_sha1(server_key, auth_msg))--***************************************************************************-- 3. 这里调用 saslContinue 指令进行认证的第二步,将密码按照 SCRAM 编码规则编码后上传数据库r = self:runCommand("saslContinue",1,"conversationId",conversationId,"payload",client_final)if r.ok ~= 1 thenreturn falseend--***************************************************************************parsed_s = crypt.base64decode(r['payload'])parsed_t = {}for k, v in string.gmatch(parsed_s, "(%w+)=([^,]*)") doparsed_t[k] = vendif parsed_t['v'] ~= server_sig thenskynet.error("Server returned an invalid signature.")return falseendif not r.done then--***************************************************************************-- 4. 这里调用 saslContinue 指令进行认证的第三步r = self:runCommand("saslContinue",1,"conversationId",conversationId,"payload","")if r.ok ~= 1 thenreturn falseend--***************************************************************************if not r.done thenskynet.error("SASL conversation failed to complete.")return falseendendreturn true
end

看过 SCRAM 认证步骤后,我们可以大概了解下 SCRAM 是什么,mongo 的 sasl 认证指令又是什么。

SCRAM

SCRAM(the Salted Challenge Response Authentication Mechanism)解决了部署挑战-响应机制所需的要求,比过去的尝试更广泛。当与传输层安全(TLS;参见[RFC5246])或等效的安全层结合使用时,该机制家族中的一种机制可以改善应用协议身份验证的现状,并为未来应用协议标准的强制实施机制提供合适的选择。

简而言之就是说 SCRAM 是一种认证用的机制,而 mongo 官方支持的认证机制中包括它,我们想要正确的通过该机制完成认证,就需要根据 SCRAM 的认证规格书来编码我们的认证信息,从 RFC5802 文档中可以找到,有兴趣的同学可以点击文末的参考链接跳转查看,我们只截取一下跟 skynet 代码中认证第一步过程相关的内容进行展示:

  • The characters ‘,’ or ‘=’ in usernames are sent as ‘=2C’ and’=3D’ respectively. If the server receives a username that contains ‘=’ not followed by either ‘2C’ or ‘3D’, then the server MUST fail the authentication.

    用户名中如果包含 ‘,’ 或者 ‘=’,则需要将 ‘,’ 替换为 ‘=2C’,将 ‘=’ 替换为 ‘=3D’ ,所以我们看到如下代码:

    local user = string.gsub(string.gsub(username, '=', '=3D'), ',' , '=2C')
    
  • n: This attribute specifies the name of the user whose password is used for authentication (a.k.a. “authentication identity” [RFC4422]).

    n: 此属性指定用于身份验证的用户密码的名称(也称为“身份验证标识”[RFC4422])。就是用该标识来编码用户名,如下:

    local first_bare = "n="  .. user .. ",r="  .. nonce
    
  • r: This attribute specifies a sequence of random printable ASCII characters excluding ‘,’ (which forms the nonce used as input to the hash function).

    r: 该属性指定了一个随机的可打印ASCII字符序列,不包括逗号(逗号用作哈希函数的输入)。

SASL

SASL(Simple Authentication and Security Layer)是一种身份验证框架,允许用户使用不同的机制进行身份验证,而无需重复编写大量代码。SASL机制是在进行身份验证时发生的一系列挑战和响应。SASL支持的身份验证机制有指定的列表,其中就包含了
SCRAM。CyrusSASL为客户端和服务器提供了一个SASL框架,并提供了一组独立的用于不同身份验证机制的包,这些包在运行时动态加载。SASL机制定义了客户端和服务器之间的通信方法。然而,它并没有定义用户凭据可以存储在何处。对于某些SASL机制,例如PLAIN,凭据可以存储在数据库本身或LDAP中。

在运行身份验证之前,服务器在客户端上初始化一个AuthenticationSession。此会话在身份验证步骤之间保持信息,并在身份验证成功或失败时释放。

在身份验证的第一步中,客户端调用{saslStart:…},该调用到达doSaslStart,获取所使用的机制,并通过调用所选机制(在SASL部分中更多介绍)上的step函数(从ServerMechanismBase::step继承)执行实际的身份验证。然后,服务器向客户端发送带有有关身份验证状态的信息的回复。如果身份验证和授权都完成,客户端可以开始对服务器执行命令。如果身份验证需要更多信息才能完成,服务器会请求此信息。如果身份验证失败,则客户端接收到该信息,并可能关闭会话。

如果在第一个SASL步骤之后还有更多工作要做,客户端将向服务器发送一个带有服务器请求的额外信息的指令 saslContinue。然后,服务器执行另一个SASL步骤。然后,服务器向客户端发送与 saslStart 命令类似的回复。saslContinue 阶段重复,直到客户端被身份验证或遇到错误为止。

以上是官方文档说明,详细内容可查看文末链接。但是实际应用时,想要正确的完成 SASL 认证可不容易,因为官方文档对这方面的说明非常少,找到一篇国外的文章,也吐槽了这个,并且老外给了一个详细的认证步骤讲解,避免需要科学上网才看得到,我转载了一下, MongoDB SCRAM-SHA-1 over SASL。所以我们当前看的 skynet 代码中出现了 sasl 鉴权内容,是因为云风大佬是自己手写的 c/lua 版本的 mongodb 驱动,在 bson(MongoDB官方定义的一种二进制序列化格式) 官网所列的驱动列表中还能看到云风大佬的仓库,bson 驱动列表。
在这里插入图片描述

操作数据库

说完了连接数据库,接下来我们分析下操作数据库的主要函数。前面的流程图介绍的是 insert 操作,接下来我们用一个更复杂的指令 update 来讲解。比如,我们要更新一张名字为 “test” 的 table,在 skynet 中可以这么写:

dbL = mongo.client({host = "127.0.0.1", port = 27017,authdb = "admin",username = "my_username",password = "my_password",authmod = "my_authmechanism", -- 认证方式,可选 mongodb_cr 和 scram_sha1,默认为 scram_sha1}
)["my_dbname"]dbL.test.update({_id = "xxx"}, {["$set"] = { name = "test_name" }}, true, false)

其中调用的更新接口在 lualib/skynet/db/mongo.lua 中:

-- file: lualib/skynet/db/mongo.lualocal bson = require "bson"
local driver = require "skynet.mongo.driver"
local bson_encode =	bson.encode
local bson_encode_order	= bson.encode_order-- 指定集合的更新操作
function mongo_collection:update(query,update,upsert,multi)self.database:send_command("update", self.name, "updates", {bson_encode({q = query,u = update,upsert = upsert,multi = multi})})
end-- 发送数据库操作指令
--- send command without response
function mongo_db:send_command(cmd, cmd_v, ...)local conn = self.connectionlocal request_id = conn:genId()local sock = conn.__socklocal bson_cmdif not cmd_v then-- ensure cmd remains in first placebson_cmd = bson_encode_order(cmd, 1, "$db", self.name, "writeConcern", {w=0})elsebson_cmd = bson_encode_order(cmd, cmd_v, "$db", self.name, "writeConcern", {w=0}, ...)endlocal pack = driver.op_msg(request_id, 2, bson_cmd)sock:request(pack)return {ok=1} -- fake successful response
end

这里我们需要拆解几个重要的函数:

  • bson_encode
    实际调用的是 lua-bson.c 中的 lencode,作用是将 lua 的 table 转化为 bson 格式的字符串,然后存放到 lightuserdata 中返回给 lua 层的调用者。

  • bson_encode_order
    实际调用的是 lua-bson.c 中的 lencode_order,作用是将偶数个参数按照 key,val 的先后顺序转化为 bson 格式的字符串,这些字符串组合成一个 bson 的 object,然后存放到 lightuserdata 中返回给 lua 层的调用者。经过转换,上面代码的调用最终可以还原为 shell 上的 json 操作指令:

    self.database:send_command("update", "test", "updates", {bson_encode({q = {_id = "xxx"},u = {["$set"] = { name = "test_name" }},upsert = true,multi = false,
    })})
    等价于在 mongo 客户端的 shell 环境中执行如下指令:
    use my_dbname;
    db.runCommand({update: "test", updates: [{ q: {_id: "xxx"}, u: {"$set": { name: "test_name" }}, upsert: true, multi: false}]})
    

    以上指令转换是经过测试的,测试的mongo版本为v4.4:
    在这里插入图片描述
    支持这个指令的文档可以点击 这里 跳转:
    在这里插入图片描述

  • driver.op_msg
    将上一步生成的 bson 对象,拷贝到将要发送的消息 buff 中,按照 mongo 约定的协议格式生成一个网络消息。对应的消息协议文档可以查看 这里:

    • message header:
      在这里插入图片描述
    • opcode 采用 OP_MSG:
      在这里插入图片描述
    • 对于 OP_MSG ,后续数据定义中注意 sections 字段:
      在这里插入图片描述
    • sections 的结构定义中我们关注 kind 为 0 的数据结构细节,一个单独的 bson object 就是我们上面一个函数最终生成的内容:
      在这里插入图片描述

上面这几张图是按照 skynet 的 mongo 驱动的调用方式,选择的协议类型而针对性的提取出来的文档信息,有了这些文档帮助,我们才能理解 lua-mongo.c 中的 op_msg 这个函数中的行为:

// file: lualib-src/lua-mongo.c// @param 1 request_id int
// @param 2 flags int
// @param 3 command bson document
// @return
static int
op_msg(lua_State *L) {int id = luaL_checkinteger(L, 1);int flags = luaL_checkinteger(L, 2);document cmd = lua_touserdata(L, 3);if (cmd == NULL) {return luaL_error(L, "opmsg require cmd document");}luaL_Buffer b;luaL_buffinit(L, &b);struct buffer buf;buffer_create(&buf);//--------------------------------------// 这一块就是在构建 mongo 消息内容int len = reserve_length(&buf); // 保留四个字节稍后存放数据长度write_int32(&buf, id); // requestID 用于客户端接收数据库消息时明确是哪一次请求发出的回复write_int32(&buf, 0);  // 在数据库返回的消息中才有效,表示回复的是哪个 requestIDwrite_int32(&buf, OP_MSG); // opCodewrite_int32(&buf, flags);  // flagBitswrite_int8(&buf, 0);       // kind 0 表示后面跟着是一个 single BSON objectint32_t cmd_len = get_length(cmd);int total = buf.size + cmd_len; // 消息总长度要加上 BSON object 的长度write_length(&buf, total, len); // 总长度已经计算好,可以插入到之前保留给长度信息的位置了,这里用 len 命名变量有点不恰当,其实表示的是存放 len 的位置的偏移量,total 才是实际的长度信息luaL_addlstring(&b, (const char *)buf.ptr, buf.size); // 将 BSON object 插入到消息最后//--------------------------------------buffer_destroy(&buf);luaL_addlstring(&b, (const char *)cmd, cmd_len);luaL_pushresult(&b);return 1;
}

结语

在上一节中我们没有详细的拆解 bson_encode 和 bson_encode_order 两个函数,他们的作用已经介绍过,读者有兴趣可以自己细看一下代码,要读懂其中的序列化过程,需要参考文末的 BSON 官方文档。值得一提的是,skynet 中,这两个函数内部都用了 pcall 来调用一个子函数的方式来简化流程中出现错误的处理,并且这样做还有个好处是不需要限制外部调用采取 pcall 的形式也能保证系统不在 bson 序列化的过程出现问题而影响到系统本身。

参考链接

  • rfc5802: SCRAM
  • mongo: SCRAM
  • mongo: SASL
  • BSON

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/217502.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JMeter直连数据库

JMeter直连数据库 使用场景操作步骤 使用场景 用作请求的参数化 登录时需要的用户名,密码可以从数据库中查询获取 用作结果的断言 添加购物车下订单,检查接口返回的订单号,是否与数据库中生成的订单号一致 清理垃圾数据 添加商品后&#xff…

汽车IVI中控开发入门及进阶(十一):ALSA音频

前言 汽车中控也被称为车机、车载多媒体、车载娱乐等,其中音频视频是非常重要的部分,音频比如播放各种格式的音乐文件、播放蓝牙接口的音乐、播放U盘或TF卡中的音频文件,如果有视频文件也可以放出音频,看起来很简单,在windows下音乐播放器很多,直接打开文件就能播放各…

生产派工自动化:MES系统的关键作用

随着制造业的数字化转型和智能化发展,生产派工自动化成为了提高生产效率、降低成本,并实现优质产品生产的关键要素之一。制造执行系统(MES)在派工自动化中发挥着重要作用,通过实时数据采集和智能调度,优化生…

项目一:IIC读写EEPROM AT24C02

回头想了想在工作中调过的EEPROM还挺多的,有M24M02 、M28010 、AT24C02等,今天讲一下AT24C02吧 一、AT24C02简介 1.1 特点 文档已经上传了,需要的同学可以自行下载哈,晚点我会把下载链接附上来。 我大概照着文档翻译了一下&am…

排序算法-快速排序

1.快速排序(递归) 快速排序是 Hoare 于 1962 年提出的一种二叉树结构的交换排序方法,其基本思想为: 任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素…

已经写完的论文怎么降低查重率 papergpt

大家好,今天来聊聊已经写完的论文怎么降低查重率,希望能给大家提供一点参考。 以下是针对论文重复率高的情况,提供一些修改建议和技巧: 已经写完的论文怎么降低查重率 背景介绍 在学术界,论文的查重率是评价论文质量的…

mysql的ON DELETE CASCADE 和ON DELETE RESTRICT区别

​​ON DELETE CASCADE​​​ 和 ​​ON DELETE RESTRICT​​ 是 MySQL 中两种不同的外键约束级联操作。它们之间的主要区别在于当主表中的记录被删除时,子表中相关记录的处理方式。 ON DELETE CASCADE: 当在主表中删除一条记录时,所有与之相关的子表中…

Java 入门第二篇,Java发展史

Java 入门第二篇,Java发展史 一,Java之诞生 Java的诞生可以追溯到20世纪90年代初。以下是Java诞生的背景和过程: 背景:在上世纪80年代和90年代初,计算机领域存在着多样化的硬件和操作系统,开发者需要为不同…

计算机操作系统-第十三天

目录 前言 进程通信(IPC) 进程通信的方法 共享存储 消息传递 直接通信方式 间接通信方式(信箱通信方式) 管道通信 本节思维导图 前言 !!!回归!!! …

万界星空科技MES系统中的生产调度流程

MES系统生产调度的目标是达到作业有序、协调、可控和高效的运行效果,作业计划的快速生成以及面向生产扰动事件的快速响应处理是生产调度系统的核心和关键。 为了顺利生成作业计划,需要为调度系统提供完整的产品和工艺信息,MES系统生成作业计…

低多边形植物模型法线贴图

在线工具推荐: 3D数字孪生场景编辑器 - GLTF/GLB材质纹理编辑器 - 3D模型在线转换 - Three.js AI自动纹理开发包 - YOLO 虚幻合成数据生成器 - 三维模型预览图生成器 - 3D模型语义搜索引擎 当谈到游戏角色的3D模型风格时,有几种不同的风格&#xf…

【STM32】DMA直接存储器存取

1 DMA简介 DMA(Direct Memory Access)直接存储器存取 可以直接访问STM32的存储器的,包括运行SRAM、程序存储器Flash和寄存器等等 DMA可以提供外设寄存器和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节…

I.MX RT1170双核学习(1):双核通信之MU消息单元详解

在I.MX RT1170中,它有CM7和CM4核,而消息单元(MU)模块使SoC内的两个处理器能够通过MU接口传递消息以进行通信和协调。 文章目录 1 MU特性2 功能描述3 MU通信实例3.1 轮训实现多核通信3.1.1 MU_SetFlags和MU_GetFlags3.1.2 MU_SendMsg和MU_ReceiveMsg3.1.…

路由基本原理

目录 一、路由器概述 二、路由器的工作原理 三、路由表的形成 四、路由配置 1.连接设备 2.进入系统模式 3.进入接口模式 4.配置网络 5.下一跳的设置 6.设置浮动路由 7.设置默认路由 一、路由器概述 路由器(Router)是一种用于连接不同网络或子…

django实现增删改查分页接口

django实现增删改查分页接口(小白必备) 在上篇文章中我使用nodejs实现了增删改查分页接口,这一篇我们则使用django实现。 1.创建一个django项目,命令如下 python manage.py startapp myapp 2.在你自己的myapp文件夹中的models.py中定义你们自己的模型 f…

看图识药,python开发实现基于VisionTransformer的119种中草药图像识别系统

中药药材图像识别相关的实践在前面的系列博文中已经有了相应的实践了,感兴趣的话可以自行移步阅读即可,每篇文章的侧重点不同: 《python基于轻量级GhostNet模型开发构建23种常见中草药图像识别系统》 《基于轻量级MnasNet模型开发构建40种常…

Kafka系列之:统计kafka集群Topic的分区数和副本数,批量增加topic副本数

Kafka系列之:统计kafka集群Topic的分区数和副本数,批量增加topic副本数 一、创建KafkaAdminClient二、获取kafka集群topic元信息三、获取每个topic的名称、分区数、副本数四、生成增加topic副本的json文件五、执行增加topic副本的命令六、确认topic增加副本是否成功一、创建K…

Linux——基本指令(二)

​ 个人主页:日刷百题 系列专栏:〖C语言小游戏〗〖Linux〗〖数据结构〗 〖C语言〗 🌎欢迎各位→点赞👍收藏⭐️留言📝 ​ ​ 写在前面: 紧接上一章,我们在理解接下来的命令之前&#xff0c…

Baumer工业相机堡盟工业相机如何通过BGAPISDK获取相机的各种信息如SN/ID等等(C#)

Baumer工业相机堡盟工业相机如何通过BGAPISDK获取相机的各种信息如SN/ID等等(C#) Baumer工业相机Baumer工业相机通过SDK获取相关生产信息的技术背景通过SDK获取相机信息的代码分析获取Baumer工业相机相关信息Baumer工业相机相关参数信息获取的测试 Baume…

【EventBus】EventBus源码浅析

二、EventBus源码解析 目录 1、EventBus的构造方法2、订阅者注册 2.1 订阅者方法的查找过程2.2 订阅者的注册过程1. subscriptionsByEventType 映射:2. typesBySubscriber 映射:2.3 总结订阅者的注册过程 3、事件的发送 3.1 使用Post提交事件3.2 使用p…