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.lua

local 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 then
			self.__authcoroutine = coroutine.running()
			-- 2. 向数据库申请认证
			local ok , message = pcall(self.__auth, self)
			if not ok then
				...
			end
			self.__authcoroutine = false
			if ok then
				...
			end
		end
		return true
	end
	_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.c

static 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.lua

function 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="  .. nonce
	local 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 then
		return false
	end
	--***************************************************************************

	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+)=([^,]*)") do
		parsed_t[k] = v
	end
	local iterations = tonumber(parsed_t['i'])
	local salt = parsed_t['s']
	local rnonce = parsed_t['r']

	if not string.sub(rnonce, 1, 12) == nonce then
		skynet.error("Server returned an invalid nonce.")
		return false
	end
	local without_proof = "c=biws,r=" .. rnonce
	local 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_proof
	local 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 then
		return false
	end
	--***************************************************************************
	parsed_s = crypt.base64decode(r['payload'])
	parsed_t = {}
	for k, v in string.gmatch(parsed_s, "(%w+)=([^,]*)") do
		parsed_t[k] = v
	end
	if parsed_t['v'] ~= server_sig then
		skynet.error("Server returned an invalid signature.")
		return false
	end
	if not r.done then
		--***************************************************************************
		-- 4. 这里调用 saslContinue 指令进行认证的第三步
		r = self:runCommand("saslContinue",1,"conversationId",conversationId,"payload","")
		if r.ok ~= 1 then
			return false
		end
		--***************************************************************************
		if not r.done then
			skynet.error("SASL conversation failed to complete.")
			return false
		end
	end
	return 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.lua

local 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.connection
	local request_id = conn:genId()
	local sock = conn.__sock
	local bson_cmd
	if not cmd_v then
		-- ensure cmd remains in first place
		bson_cmd = bson_encode_order(cmd, 1, "$db", self.name, "writeConcern", {w=0})
	else
		bson_cmd = bson_encode_order(cmd, cmd_v, "$db", self.name, "writeConcern", {w=0}, ...)
	end

	local 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);  // 在数据库返回的消息中才有效,表示回复的是哪个 requestID
		write_int32(&buf, OP_MSG); // opCode
		write_int32(&buf, flags);  // flagBits
		write_int8(&buf, 0);       // kind 0 表示后面跟着是一个 single BSON object

		int32_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.mfbz.cn/a/240162.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python:核心知识点整理大全16-笔记

目录 8.2.3 默认值 8.2.4 等效的函数调用 8.2.5 避免实参错误 8.3 返回值 8.3.1 返回简单值 formatted_name.py 8.3.2 让实参变成可选的 8.3.3 返回字典 往期快速传送门👆(在文章最后): 8.2.3 默认值 编写函数时&#xff…

Docker镜像构建:深入Dockerfile创建自定义镜像

Docker的强大之处在于其能够通过Dockerfile定义和构建自定义镜像,为应用提供独立、可移植的运行环境。在这篇博客文章中,将深入探讨Docker镜像构建的核心概念,通过更加丰富的示例代码,帮助大家全面理解和掌握构建自定义镜像的技术…

机器学习笔记 - 基于C# + .net framework 4.8的ONNX Runtime进行分类推理

该示例是从官方抄的,演示了如何使用 Onnx Runtime C# API 运行预训练的 ResNet50 v2 ONNX 模型。 我这里的环境基于.net framework 4.8的一个winform项目,主要依赖下面版本的相关库。 Microsoft.Bcl.Numerics.8.0.0 Microsoft.ML.OnnxRuntime.Gpu.1.16.3 SixLabors.ImageShar…

掌握iText:轻松处理PDF文档-高级篇-添加水印

前言 iText作为一个功能强大、灵活且广泛应用的PDF处理工具,在实际项目中发挥着重要作用。通过这些文章,读者可以深入了解如何利用iText进行PDF的创建、编辑、加密和提取文本等操作,为日常开发工作提供了宝贵的参考和指导。 掌握iText&…

并发编程-线程等待唤醒机制

目录 前言 ​编辑 线程等待和唤醒的方法 wait() 方法: notify() 方法: 注意事项和建议: 我的其他博客 前言 程等待唤醒机制是多线程编程中用于线程之间协调和通信的一种机制。在多线程环境中,有时候一个线程需要等待某个条件…

【大数据】Doris 架构

Doris 架构 Doris 的架构很简洁,只设 FE(Frontend)、BE(Backend)两种角色、两个进程,不依赖于外部组件,方便部署和运维,FE、BE 都可线性扩展。 ✅ Frontend(FE&#xff0…

C++_类的定义和使用

目录 1、类的引用 1.1 类的成员函数 1.2 类成员函数的声明和定义 2、类的定义 2.1 类的访问限定(封装) 3、类重名问题 4、类的实例化 4.1 类的大小 5、隐含的this指针 5.1 空指针问题 结语: 前言: C的类跟c语言中的结…

Standoff 12 网络演习

在 11 月 21 日至 24 日于莫斯科举行的 "Standoff 12 "网络演习中,Positive Technologies 公司再现了其真实基础设施的一部分,包括软件开发、组装和交付的所有流程。安全研究人员能够在安全的环境中测试系统的安全性,并尝试将第三方…

GO闭包实现原理(汇编级讲解)

go语言闭包实现原理(汇编层解析) 1.起因 今天开始学习go语言,在学到go闭包时候,原本以为go闭包的实现方式就是类似于如下cpp lambda value通过值传递,mutable修饰可以让value可以修改,但是地址不可能一样value通过引用传递,但是在其他地方调用时,这个value局部变量早就释放,…

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

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

深度学习在人体动作识别领域的应用:开源工具、数据集资源及趋动云GPU算力不可或缺

人体动作识别检测是一种通过使用计算机视觉和深度学习技术,对人体姿态和动作进行实时监测和分析的技术。该技术旨在从图像或视频中提取有关人体姿态、动作和行为的信息,以便更深入地识别和理解人的活动。 人体动作识别检测的基本步骤包括: 数…

web279(s2-001)

目前java小白一个,主要是学学别人的思路 进入题目,登录框一个 抓包也没发现什么东西 网上说是struts2框架 Struts2是用Java语言编写的一个基于MVC设计模式的Web应用框架 判断是不是基于struts2的一些方法: 1.通过页面回显的错误消息来判断…

MySQL一行记录是怎么存储的?

文章目录 MySQL 一行记录是怎么存储的?MySQL 的数据存放在哪个文件?表空间文件结构 InnoDB行格式有哪些Compact行格式varchar(n) 中 n 最大取值为多少?行溢出后,MySQL是怎么处理的? MySQL 一行记录是怎么存储的&#x…

IDEA 出现问题:git提交commit时Perform code analysis卡住解决方案

问题 git提交commit时Perform code analysis卡住很久 解决方案一 1、打开 IntelliJ IDEA,进入 File -> Settings(或者使用快捷键 CtrlAltS)。 2、在弹出的 Settings 窗口中,找到 Version Control -> Commit Dialog 选项…

Flink 有状态流式处理

传统批次处理方法 【1】持续收取数据(kafka等),以window时间作为划分,划分一个一个的批次档案(按照时间或者大小等); 【2】周期性执行批次运算(Spark/Stom等)&#xff1b…

机器学习---Adaboost算法

1. Adaboost算法介绍 Adaboost是一种迭代算法,其核心思想是针对同一个训练集训练不同的分类器(弱分类器),然 后把这些弱分类器集合起来,构成一个更强的最终分类器(强分类器)。Adaboost算法本身…

CSS学习

CSS学习 1. 什么是css?2.css引入方式2.1 内嵌式2.2 外联式2.3 行内式2.4 引入方式特点 3. 基础选择器3.1 标签选择器3.2 类选择器3.3 id选择器3.4 通配符选择器 1. 什么是css? 2.css引入方式 2.1 内嵌式 2.2 外联式 提示: 需要在html文件中link目标样式表; 2.3 行内式 注意:…

【EventBus】EventBus源码浅析

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

【STM32】DMA直接存储器存取

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

【lombok】从easyExcel read不到值到cglib @Accessors(chain = true)隐藏的大坑

背景: 在一次使用easyExcel.read 读取excel时,发现实体类字段没有值,在反复测试后,发现去掉Accessors(chain true)就正常了,为了验证原因,进行了一次代码跟踪 由于调用链路特别长,只列举出部分代码&#x…
最新文章