2024年11月

PostgreSQL中将对象oid转为对象名

使用pg的内部数据类型将对象oid转为对象名,可以简化一些系统视图的关联查询。

数据库类型转换对应类型的oid

可以用以下数据库类型转换对应类型的oid(以pg12为例)

postgres=# select typname from pg_type where typname ~ '^reg';
    typname
---------------
 regclass
 regconfig
 regdictionary
 regnamespace
 regoper
 regoperator
 regproc
 regprocedure
 regrole
 regtype
(10 rows)

对应关系

对象名称 类型 转换规则
pg_class regclass pg_class.oid::regclass
pg_ts_dict regdictionary pg_ts_dict.oid::regdictionary
pg_namespace regnamespace pg_namespace.oid::regnamespace
pg_operator regoperator pg_operator.oid::regoperator
pg_proc regproc pg_proc.oid::regproc
pg_roles
pg_user
regrole pg_roles.oid::regrole
pg_user.usesysid::regrole
pg_type regtype pg_type.oid::regtype
以下几个类型暂不确定用途,待研究:
regprocedure
regoper
regconfig

创建测试数据

psql -U postgres
create user test password 'test';
create database testdb with owner=test;
\c testdb
CREATE SCHEMA AUTHORIZATION test;
psql -U test -d testdb
create table test_t1(id int);
create table test_t2(id int);
create table test_t3(id int);

基于如上测试数据,查询test模式下有哪些表,以及表的owner

传统表关联的方式使用以下SQL,关联pg_class、pg_namespace、pg_roles/pg_user

psql -U test -d testdb
-- 查询用户关联pg_user查询
SELECT
  t3.nspname AS SCHEMA,
  t1.relname AS tablename,
  t2.usename AS OWNER 
FROM
  pg_class t1
  JOIN pg_user t2 ON t1.relowner = t2.usesysid
  JOIN pg_namespace t3 ON t1.relnamespace = t3.OID 
WHERE
  t1.relkind = 'r' 
  AND t3.nspname = 'test';

 schema | tablename | owner
--------+-----------+-------
 test   | test_t1   | test
 test   | test_t2   | test
 test   | test_t3   | test
(3 rows)

-- 查询用户关联pg_roles查询
SELECT
  t3.nspname AS SCHEMA,
  t1.relname AS tablename,
  t2.rolname AS OWNER 
FROM
  pg_class t1
  JOIN pg_roles t2 ON t1.relowner = t2.oid
  JOIN pg_namespace t3 ON t1.relnamespace = t3.OID 
WHERE
  t1.relkind = 'r' 
  AND t3.nspname = 'test';

 schema | tablename | owner
--------+-----------+-------
 test   | test_t1   | test
 test   | test_t2   | test
 test   | test_t3   | test
(3 rows)

如上为了实现查询效果需要关联三张表,查询比较繁琐,如果使用对象转换就很简单了,如下:

psql -U test -d testdb
SELECT
  relnamespace :: REGNAMESPACE AS SCHEMA,
  relname AS tablename,
  relowner :: REGROLE AS OWNER 
FROM
  pg_class 
WHERE
  relnamespace :: REGNAMESPACE :: TEXT = 'test' 
  AND relkind = 'r';

 schema | tablename | owner
--------+-----------+-------
 test   | test_t1   | test
 test   | test_t2   | test
 test   | test_t3   | test
(3 rows)

将对象名转为oid类型

转换关系

对象类型 转换规则
table '表名'::regclass::oid
function/procedure '函数名/存储过程名'::regproc::oid
schema '模式名'::regnamespace::oid
user/role '用户名/角色名'::regrole::oid
type '类型名称'::regtype::oid

测试示例

表转换

drop table if exists test_t;
create table test_t(id int);

postgres=# select oid from pg_class where relname = 'test_t';
  oid
-------
 16508
(1 row)

postgres=# select 'test_t'::regclass::oid;
  oid
-------
 16508
(1 row)

函数转换

CREATE OR REPLACE FUNCTION test_fun(
    arg1 INTEGER,
    arg2 INTEGER,
    arg3 TEXT
)
RETURNS INTEGER
AS $$
BEGIN
    RETURN arg1 + arg2;
END;
$$ LANGUAGE plpgsql;


postgres=# select oid,proname from pg_proc where proname = 'test_fun';
  oid  | proname
-------+----------
 16399 | test_fun
(1 row)

postgres=# select 'test_fun'::regproc::oid;
  oid
-------
 16399
(1 row)

模式转换

create schema test_schema;

postgres=# select oid,nspname from pg_namespace where nspname='test_schema';
  oid  |   nspname
-------+-------------
 16511 | test_schema
(1 row)

postgres=# select 'test_schema'::regnamespace::oid;
  oid
-------
 16511
(1 row)

用户/角色

create user test_user;

postgres=# select usesysid,usename from pg_user where usename='test_user';
 usesysid |  usename
----------+-----------
    16512 | test_user
(1 row)

postgres=# select 'test_user'::regrole::oid;
  oid
-------
 16512
(1 row)

类型

CREATE TYPE type_sex AS ENUM ('male', 'female');

postgres=# select oid,typname from pg_type where typname='type_sex';
  oid  | typname
-------+----------
 16514 | type_sex
(1 row)

postgres=# select 'type_sex'::regtype::oid;
  oid
-------
 16514
(1 row)

整数类型的 UNSIGNED 属性有什么用?

MySQL 中的整数类型可以使用可选的 UNSIGNED 属性来表示不允许负值的无符号整数。使用 UNSIGNED 属性可以将正整数的上限提高一倍,因为它不需要存储负数值。

例如, TINYINT UNSIGNED 类型的取值范围是 0 ~ 255,而普通的 TINYINT 类型的值范围是 -128 ~ 127。INT UNSIGNED 类型的取值范围是 0 ~ 4,294,967,295,而普通的 INT 类型的值范围是 -2,147,483,648 ~ 2,147,483,647。

对于从 0 开始递增的 ID 列,使用 UNSIGNED 属性可以非常适合,因为不允许负值并且可以拥有更大的上限范围,提供了更多的 ID 值可用。

char和varchar的区别

CHAR

  • CHAR类型用于存储固定长度字符串:MySQL总是
    根据定义的字符串长度分配足够的空间
    。当存储CHAR值时,MySQL会删除字符串中的末尾空格同时,CHAR值会根据需要采用空格进行剩余空间填充,以方便比较和检索。但正因为其长度固定,所以会占据多余的空间,也是一种空间换时间的策略;
  • CHAR适合存储很短或长度近似的字符串。例如,
    CHAR非常适合存储密码的MD5值、定长的身份证等,因为这些是定长的值
  • 对于经常变更的数据,CHAR也比VARCHAR更好,因为定长的CHAR类型占用磁盘的存储空间是连续分配的,不容易产生碎片。
  • 对于非常短的列,CHAR比VARCHAR在存储空间上也更有效率。例如用CHAR(1)来存储只有Y和N的值,如果采用单字节字符集只需要一个字节,但是VARCHAR(1)却需要两个字节,因为还有一个记录长度的额外字节。

VARCHAR:

  • VARCHAR类型用于存储可变长度字符串,是最常见的字符串数据类型。它
    比固定长度类型更节省空间
    ,因为它仅使用必要的空间(根据实际字符串的长度改变存储空间)。

  • VARCHAR需要使用1或2个额外字节记录字符串的长度:如果列的最大长度小于或等于255字节,则只使用1个字节表示,否则使用2个字节。假设采用latinl字符集,一个VARCHAR(10)的列需要11个字节的存储空间。VARCHAR(1000)的列则需要1002 个字节,因为需要2个字节存储长度信息。

  • VARCHAR节省了存储空间,所以对性能也有帮助。但是,由于行是变长的,在UPDATE时可能使行变得比原来更长,这就导致需要做额外的工作。如果一个行占用的空间增长,并且在页内没有更多的空间可以存储,在这种情况下,不同的存储引擎的处理方式是不一样的。例如,MylSAM会将行拆成不同的片段存储,InnoDB则需要分裂页来使行可以放进页内。

  • 操作内存的方式:对于varchar数据类型来说,硬盘上的存储空间虽然都是根据字符串的实际长度来存储空间的,但在内存中是根据varchar类型定义的长度来分配占用的内存空间的,而不是根据字符串的实际长度来分配的。显然,这对于排序和临时表会较大的性能影响。

VARCHAR(100)和 VARCHAR(10)的区别是什么?

VARCHAR(100)和 VARCHAR(10)都是变长类型,表示能存储最多 100 个字符和 10 个字符。因此,VARCHAR (100) 可以满足更大范围的字符存储需求,有更好的业务拓展性。而 VARCHAR(10)存储超过 10 个字符时,就需要修改表结构才可以。

虽说 VARCHAR(100)和 VARCHAR(10)能存储的字符范围不同,但二者存储相同的字符串,所占用磁盘的存储空间其实是一样的,这也是很多人容易误解的一点。

不过,VARCHAR(100) 会消耗更多的内存。这是因为 VARCHAR 类型在内存中操作时,通常会分配固定大小的内存块来保存值,即使用字符类型中定义的长度。例如在进行排序的时候,VARCHAR(100)是按照 100 这个长度来进行的,也就会消耗更多内存。

DECIMAL 和 FLOAT/DOUBLE 的区别是什么?

DECIMAL 和 FLOAT 的区别是:
DECIMAL 是定点数,FLOAT/DOUBLE 是浮点数。DECIMAL 可以存储精确的小数值,FLOAT/DOUBLE 只能存储近似的小数值。

DECIMAL 用于存储具有精度要求的小数,例如与货币相关的数据,可以避免浮点数带来的精度损失。

在 Java 中,MySQL 的 DECIMAL 类型对应的是 Java 类
java.math.BigDecimal

int(10)和char(10)的区别?

int(10)中的10表示的是显示数据的长度,而char(10)表示的是存储数据的长度。

为什么不推荐使用 TEXT 和 BLOB?

数据库规范通常不推荐使用 BLOB 和 TEXT 类型,这两种类型具有一些缺点和限制,例如:

  • 不能有默认值。
  • 在使用临时表时无法使用内存临时表,只能在磁盘上创建临时表(《高性能 MySQL》书中有提到)。
  • 检索效率较低。
  • 不能直接创建索引,需要指定前缀长度。
  • 可能会消耗大量的网络和 IO 带宽。
  • 可能导致表上的 DML 操作变慢。
  • ……

DATETIME 和 TIMESTAMP 的区别是什么?

DATETIME 类型没有时区信息,TIMESTAMP 和时区有关。

TIMESTAMP 只需要使用 4 个字节的存储空间,但是 DATETIME 需要耗费 8 个字节的存储空间。但是,这样同样造成了一个问题,Timestamp 表示的时间范围更小。

  • DATETIME:1000-01-01 00:00:00 ~ 9999-12-31 23:59:59
  • Timestamp:1970-01-01 00:00:01 ~ 2037-12-31 23:59:59

Boolean 类型如何表示?

MySQL 中没有专门的布尔类型,而是用 TINYINT(1) 类型来表示布尔值。TINYINT(1) 类型可以存储 0 或 1,分别对应 false 或 true。

为什么不建议使用null作为默认值

Mysql不建议用Null作为列默认值不是因为不能使用索引,而是因为:

  • 索引列存在 NULL 就会导致优化器在做索引选择的时候更加复杂,更加难以优化。比如进行索引统计时,
    count(1),max(),min() 会省略值为NULL 的行
  • NULL 值是一个没意义的值,但是它会占用物理空间,所以会带来的存储空间的问题,因为 InnoDB 存储记录的时候,如果表中存在允许为 NULL 的字段,那么行格式 (opens new window)中
    至少会用 1 字节空间存储 NULL 值列表
    。建议用""或默认值0来代替NULL

不建议使用null作为默认值,并且
建议必须设置默认值
,原因如下:

  • 既然都不可为空了,那就必须要有默认值,否则不插入这列的话,就会报错;
  • 数据库不应该是用来查问题的,不能靠mysql报错来告知业务有问题,该不该插入应该由业务说了算;
  • 对于DBA来说,允许使用null是没有规范的,因为不同的人不同的用法。

但像
合同生效时间

获奖时间
等这种不可控字段,是可以不设置默认值的,但同样需要not null

为什么禁止使用外键

  • 外键会降低数据库的性能。在MySQL中,外键会自动加上索引,这会使得对该表的查询等操作变得缓慢,尤其是在大型数据表中。
  • 外键也会限制了表结构的调整和更改。在实际应用中,表结构经常需要进行更改,而如果表之间使用了外键约束,这些更改可能会非常难以实现。因为更改一个表的结构,需要涉及到所有以其为父表的子表,这会导致长时间锁定整个数据库表,甚至可能会导致数据丢失。
  • 在MySQL中,外键约束可能还会引发死锁问题。当想要对多个表中的数据进行插入、更新、删除操作时,由于外键约束的存在,可能会导致死锁,需要等待其他事务释放锁。
  • MySQL中使用外键还会增加开发难度。开发人员需要处理数据在表之间的关系,而这样的处理需要花费更多的时间和精力,以及对数据库的深入理解。同时,外键也会增加代码的复杂度,使得SQL语句变得难以理解和调试。

在阿里巴巴开发手册中也有提到,
传送门

使用自增主键有什么好处?

自增主键可以让主键索引尽量地保持递增顺序插入,避免了页分裂,因此索引更紧凑,在查询的时候,效率也就更高。

自增主键保存在什么地方?

不同的引擎对于自增值的保存策略不同:

  • MyISAM引擎的自增值保存在数据文件中。
  • 在MySQL8.0以前,InnoDB引擎的自增值是存在内存中。MySQL重启之后内存中的这个值就丢失了,每次重启后第一次打开表的时候,会找自增值的最大值max(id),然后将最大值加1作为这个表的自增值;MySQL8.0版本会将自增值的变更记录在redo log中,重启时依靠redo log恢复。

自增主键一定是连续的吗?

不一定,有几种情况会导致自增主键不连续。

1、唯一键冲突导致自增主键不连续。当我们向一个自增主键的InnoDB表中插入数据的时候,如果违反表中定义的唯一索引的唯一约束,会导致插入数据失败。此时表的自增主键的键值是会向后加1滚动的。下次再次插入数据的时候,就不能再使用上次因插入数据失败而滚动生成的键值了,必须使用新滚动生成的键值。

2、事务回滚导致自增主键不连续。当我们向一个自增主键的InnoDB表中插入数据的时候,如果显式开启了事务,然后因为某种原因最后回滚了事务,此时表的自增值也会发生滚动,而接下里新插入的数据,也将不能使用滚动过的自增值,而是需要重新申请一个新的自增值。

3、批量插入导致自增值不连续。MySQL有一个批量申请自增id的策略:

  • 语句执行过程中,第一次申请自增id,分配1个自增id
  • 1个用完以后,第二次申请,会分配2个自增id
  • 2个用完以后,第三次申请,会分配4个自增id
  • 依次类推,每次申请都是上一次的两倍(最后一次申请不一定全部使用)

如果下一个事务再次插入数据的时候,则会基于上一个事务申请后的自增值基础上再申请。此时就出现自增值不连续的情况出现。

4、自增步长不是1,也会导致自增主键不连续。

InnoDB的自增值为什么不能回收利用?

主要为了提升插入数据的效率和并行度。

假设有两个并行执行的事务,在申请自增值的时候,为了避免两个事务申请到相同的自增 id,肯定要加锁,然后顺序申请。

假设事务 A 申请到了 id=2, 事务 B 申请到 id=3,那么这时候表 t 的自增值是 4,之后继续执行。

事务 B 正确提交了,但事务 A 出现了唯一键冲突。

如果允许事务 A 把自增 id 回退,也就是把表 t 的当前自增值改回 2,那么就会出现这样的情况:表里面已经有 id=3 的行,而当前的自增 id 值是 2。

接下来,继续执行的其他事务就会申请到 id=2,然后再申请到 id=3。这时,就会出现插入语句报错“主键冲突”。

而为了解决这个主键冲突,有两种方法:

  • 每次申请 id 之前,先判断表里面是否已经存在这个 id。如果存在,就跳过这个 id。但是,这个方法的成本很高。因为,本来申请 id 是一个很快的操作,现在还要再去主键索引树上判断 id 是否存在。
  • 把自增 id 的锁范围扩大,必须等到一个事务执行完成并提交,下一个事务才能再申请自增 id。这个方法的问题,就是锁的粒度太大,系统并发能力大大下降。

可见,这两个方法都会导致性能问题。

因此,InnoDB 放弃了“允许自增 id 回退”这个设计,语句执行失败也不回退自增 id。

utf8 、utf8mb3和 utf8mb4的区别

utf8mb3
:只支持最长三个字节的BMP(Basic Multilingual Plane,基本多文种平面)字符(不支持补充字符)。

utf8mb4
:mb4即 most bytes 4,即最多使用4个字节来表示完整的UTF-8,具有以下特征:

  • 支持BMP和补充字符。
  • 每个多字节字符最多需要四个字节。

utf8mb4是utf8的超集并完全兼容它,是MySQL 在 5.5.3 版本之后增加的一个新的字符集,能够用四个字节存储更多的字符,几乎包含了世界上所有能看到见的语言字符。

  • 差异比较
差异点 utf8mb3 utf8mb4
最大使用字节数 3 4
支持字符类型 BMP BMP+其它字符
字符类型 常见的 Unicode 字符 常见的 Unicode 字符 + 部分罕用汉字 + emoji表情 + 新增的 Unicode 字符等
Unicode范围 U0000 - U+FFFF(即BMP) U0000 - U+10FFFF
占用存储空间 略小(如CHAR(10) 需要10 * 3 = 30 个字节的空间;VARCHAR 类型需要额外使用1个字节来记录字符串的长度) 稍大(如CHAR(10) 需要 10 * 4 = 40 个字节的空间;VARCHAR 类型需要额外使用2个字节来记录字符串的长度)
兼容性 切换至utf8mb4 一般不会有问题,但要注意存储空间够不够、排序规则是否变化 切换至utf8mb3可能会有问题,字符丢失、报错或乱码
安全性 稍低,更容易被恶意字符串攻击 较高,保留恶意字符串,然后报错或乱码提示

如何选择?一句话就是,根据具体的业务需求和实际情况,选择最合适的字符集。

面试题专栏

Java面试题专栏
已上线,欢迎访问。

  • 如果你不知道简历怎么写,简历项目不知道怎么包装;
  • 如果简历中有些内容你不知道该不该写上去;
  • 如果有些综合性问题你不知道怎么答;

那么可以私信我,我会尽我所能帮助你。

来源:晓飞的算法工程笔记 公众号,转载请注明出处

论文: Vision-Language Model Fine-Tuning via Simple Parameter-Efficient Modification

创新点


  • 提出了一种
    CLIPFit
    方法以高效地微调
    CLIP
    模型,从而揭示经典模型微调在视觉语言模型(
    VLMs
    )上的潜力。
  • 与现有的提示调整或适配器调整方法不同,
    CLIPFit
    不引入任何外部参数,而仅微调
    CLIP
    固有参数中的一个小特定子集。

内容概述


微调视觉语言模型(
VLMs
)方面的进展见证了提示调优和适配器调优的成功,而经典模型在固有参数上的微调似乎被忽视了。有人认为,使用少量样本微调
VLMs
的参数会破坏预训练知识,因为微调
CLIP
模型甚至会降低性能。论文重新审视了这一观点,并提出了一种新视角:微调特定的参数而不是全部参数将揭示经典模型微调在
VLMs
上的潜力。

通过细致研究,论文提出了
ClipFit
,可以在不引入额外参数开销的情况下微调
CLIP
。仅通过微调特定的偏置项和归一化层,
ClipFit
可以将零样本
CLIP
的平均调和均值准确率提升
7.27%

为了理解
CLIPFit
中的微调如何影响预训练模型,论文进行了广泛的实验分析以研究内部参数和表示的变化。在文本编码器中,当层数增加时,偏置的变化减少。在图像编码器中,
LayerNorm
也有同样的结论。进一步的实验表明,变化较大的层对知识适应更为重要。

CLIPFit


在不引入任何外部参数的情况下,
CLIPFit
仅对文本编码器中
FNN
的投影线性层的偏置项进行微调,并更新图像编码器中的
LayerNorm

文本编码器

对于文本编码器,
CLIPFit
并不是对所有偏置项进行微调,而仅对文本编码器中
FFNs
的投影线性层(即第二层)的偏置项进行微调。仅微调部分偏置项将减少训练参数的数量,相较于微调所有偏置项。此外,实验表明,微调部分偏置项可以实现比微调所有偏置项更好的性能。

图像编码器

BitFit
证明了在不引入任何新参数的情况下,仅微调预训练语言模型中的偏置项可以与完全微调的表现相媲美。然而,
BitFit
是为大型语言模型(
LLM
)微调设计的,直接将
BitFit
应用于视觉语言模型(
VLM
)微调可能会损害模型的泛化能力。

为此,
CLIPFit
并没有对图像编码器的偏置项进行微调,而是对
LayerNorm
进行微调。在
LayerNorm
中,两个可学习参数增益
\(\boldsymbol{g}\)
和偏置
\(\boldsymbol{b}\)
用于对标准化输入向量
\(\boldsymbol{x}\)
进行仿射变换,以进行重新中心化和重新缩放,这有助于通过重新塑形分布来增强表达能力。在训练过程中,不同的数据分布应该在
LayerNorm
中产生不同的增益和偏置,以实现分布的重新塑形。

如果在推理过程中应用偏移的增益和偏置,可能会导致次优解。因此,
CLIPFit
对图像编码器中的
LayerNorm
进行微调。

损失函数

在微调阶段,通用的预训练知识很容易被遗忘。因此,论文探索了两种不同的策略来减轻这种遗忘。

第一种策略是使用知识蒸馏损失来指导
CLIPFit
从原始的零样本
CLIP
中学习。设
\(\{\boldsymbol{w}_i^\mathrm{clip}\}_{i=1}^K\)
为原始
CLIP
的文本特征,
\(\{\boldsymbol{w}_{i}\}_{i=1}^K\)

CLIPFit
的文本特征。
CLIPFit
的训练损失和知识蒸馏损失定义为:

\[\begin{equation}
\mathcal{L}=\mathcal{L}_{\mathrm{ce}}+\beta \mathcal{L}_{\mathrm{k g}},
\end{equation}
\]

\[\begin{equation}
\mathcal{L}_\mathrm{k g} = \frac{1}{K}\sum_{i=1}^{K}\cos(\boldsymbol{w}_i^{\mathrm{clip}},\boldsymbol{w}_i),
\end{equation}
\]

第二种策略是使用均方误差(
MSE
)损失来惩罚文本编码器的变化。设
\(\{\boldsymbol{b}_i^\mathrm{clip}\}_{i=1}^L\)
为来自预训练
CLIP
的未固定文本偏置项,
\(\{\boldsymbol{b}_i\}_{i=1}^L\)
为来自
CLIPFit
的未固定文本偏置项,其中
\(L\)
是未固定偏置层的数量。均方误差损失定义为:

\[\begin{equation}
\mathcal{L}_\mathrm{m s e} = \frac{1}{L}\sum_{i=1}^{L}||\boldsymbol{b}_i^\mathrm{clip}-\boldsymbol{b}_i||^2.
\end{equation}
\]

这两种策略都能缓解遗忘问题,而知识蒸馏损失的效果更佳。因此,选择将知识蒸馏损失作为
CLIPFit
的最终解决方案。

主要实验




如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.

Navigation作为路由容器,其生命周期承载在NavDestination组件上,以组件事件的形式开放。其生命周期大致可分为三类,自定义组件生命周期、通用组件生命周期和自有生命周期。其中,aboutToAppear和aboutToDisappear是自定义组件的生命周期(NavDestination外层包含的自定义组件),OnAppear和OnDisappear是组件的通用生命周期。剩下的六个生命周期为NavDestination独有。生命周期时序如下图所示
img1

  • aboutToAppear:在创建自定义组件后,执行其build()函数之前执行(NavDestination创建之前),允许在该方法中改变状态变量,更改将在后续执行build()函数中生效。
  • onWillAppear:NavDestination创建后,挂载到组件树之前执行,在该方法中更改状态变量会在当前帧显示生效。
  • onAppear:通用生命周期事件,NavDestination组件挂载到组件树时执行。
  • onWillShow:NavDestination组件布局显示之前执行,此时页面不可见(应用切换到前台不会触发)。
  • onShown:NavDestination组件布局显示之后执行,此时页面已完成布局。
  • onWillHide:NavDestination组件触发隐藏之前执行(应用切换到后台不会触发)。
  • onHidden:NavDestination组件触发隐藏后执行(非栈顶页面push进栈,栈顶页面pop出栈或应用切换到后台)。
  • onWillDisappear:NavDestination组件即将销毁之前执行,如果有转场动画,会在动画前触发(栈顶页面pop出栈)。
  • onDisappear:通用生命周期事件,NavDestination组件从组件树上卸载销毁时执行。
  • aboutToDisappear:自定义组件析构销毁之前执行,不允许在该方法中改变状态变量。

一、概述

上篇文章介绍了木舟通过HTTP网络组件接入设备,那么此篇文章将介绍如何利用Tcp或者UDP网络组件接入设备.

木舟 (Kayak) 是什么?

木舟(Kayak)是基于.NET6.0软件环境下的surging微服务引擎进行开发的, 平台包含了微服务和物联网平台。支持异步和响应式编程开发,功能包含了物模型,设备,产品,网络组件的统一管理和微服务平台下的注册中心,服务路由,模块,中间服务等管理。还有多协议适配(TCP,MQTT,UDP,CoAP,HTTP,Grpc,websocket,rtmp,httpflv,webservice,等),通过灵活多样的配置适配能够接入不同厂家不同协议等设备。并且通过设备告警,消息通知,数据可视化等功能。能够让你能快速建立起微服务物联网平台系统。

那么下面就为大家介绍如何从创建组件、协议、设备网关,设备到设备网关接入,再到设备数据上报,把整个流程通过此篇文章进行阐述。

二、网络组件

1.编辑创建Tcp协议的网络组件,可以选择共享配置和独立配置(独立配置是集群模式). 下图是解析方式选择了自定义脚本进行解码操作。

还可以选择其它解析方式:如下图

2. 编辑创建UDP协议的网络组件,可以选择共享配置和独立配置(独立配置是集群模式). 可以选择单播或组播。

三、自定义协议

  • 如何创建自定义协议模块

如果是网络编程开发,必然会涉及到协议报文的编码解码处理,那么对于平台也是做到了灵活处理,首先是协议模块创建,通过以下代码看出协议模块可以添加协议说明md文档, 身份鉴权处理,消息编解码,元数据配置。下面一一介绍如何进行编写

 public classDemo3ProtocolSupportProvider : ProtocolSupportProvider
{
public override IObservable<ProtocolSupport>Create(ProtocolContext context)
{
var support = newComplexProtocolSupport();
support.Id
= "demo_3";
support.Name
= "演示协议3";
support.Description
= "演示协议3";
support.AddAuthenticator(MessageTransport.Tcp,
newDemo5Authenticator());
support.AddDocument(MessageTransport.Tcp,
"Document/document-tcp.md");
support.Script
= "\r\nvar decode=function(buffer)\r\n{\r\n parser.Fixed(5).Handler(\r\n function(buffer){ \r\n var bytes = BytesUtils.GetBytes(buffer,1,4);\r\n var len = BytesUtils.LeStrToInt(bytes,1,4);//2. 获取消息长度.\r\n var buf = BytesUtils.Slice(buffer,0,5); \r\n parser.Fixed(len).Result(buf); \r\n }).Handler(function(buffer){ parser.Result(buffer).Complete(); \r\n }\r\n )\r\n}\r\nvar encode=function(buffer)\r\n{\r\n}";
support.AddMessageCodecSupport(MessageTransport.Tcp, ()
=> Observable.Return(newScriptDeviceMessageCodec(support.Script)));
support.AddConfigMetadata(MessageTransport.Tcp, _tcpConfig);

support.AddAuthenticator(MessageTransport.Udp,
newDemo5Authenticator());
support.Script
= "\r\nvar decode=function(buffer)\r\n{\r\n parser.Fixed(5).Handler(\r\n function(buffer){ \r\n var bytes = BytesUtils.GetBytes(buffer,1,4);\r\n var len = BytesUtils.LeStrToInt(bytes,1,4);//2. 获取消息长度.\r\n var buf = BytesUtils.Slice(buffer,0,5); \r\n parser.Fixed(len).Result(buf); \r\n }).Handler(function(buffer){ parser.Result(buffer).Complete(); \r\n }\r\n )\r\n}\r\nvar encode=function(buffer)\r\n{\r\n}";
support.AddMessageCodecSupport(MessageTransport.Udp, ()
=> Observable.Return(newScriptDeviceMessageCodec(support.Script)));
support.AddConfigMetadata(MessageTransport.Udp, _udpConfig);
returnObservable.Return(support);
}
}

1. 添加协议说明文档如代码:
support.AddDocument(MessageTransport.Tcp,
"
Document/document-tcp.md
"
);,文档仅支持
markdown文件,如下所示

### 认证说明

CONNECT报文:
```text
clientId: 设备ID
password: md5(timestamp
+"|"+secureKey)
```

2. 添加身份鉴权如代码:
support.AddAuthenticator(MessageTransport.Http, new Demo5Authenticator()) ,自定义身份鉴权
Demo5Authenticator 代码如下:

public classDemo5Authenticator : IAuthenticator
{
public IObservable<AuthenticationResult>Authenticate(IAuthenticationRequest request, IDeviceOperator deviceOperator)
{
var result = Observable.Return<AuthenticationResult>(default);if (request isDefaultAuthRequest)
{
var authRequest = request asDefaultAuthRequest;
deviceOperator.GetConfig(authRequest.GetTransport()
==MessageTransport.Http?"token": "key").Subscribe( config =>{var password = config.Convert<string>();if(authRequest.Password.Equals(password))
{
result
=result.Publish(AuthenticationResult.Success(authRequest.DeviceId));
}
else{
result
= result.Publish(AuthenticationResult.Failure(StatusCode.CUSTOM_ERROR, "验证失败,密码错误"));
}
});
}
elseresult= Observable.Return<AuthenticationResult>(AuthenticationResult.Failure(StatusCode.CUSTOM_ERROR, "不支持请求参数类型"));returnresult;
}
public IObservable<AuthenticationResult>Authenticate(IAuthenticationRequest request, IDeviceRegistry registry)
{
var result = Observable.Return<AuthenticationResult>(default);var authRequest = request asDefaultAuthRequest;
registry
.GetDevice(authRequest.DeviceId)
.Subscribe(
async p =>{var config= await p.GetConfig(authRequest.GetTransport() == MessageTransport.Http ? "token" : "key");var password= config.Convert<string>();if(authRequest.Password.Equals(password))
{
result
=result.Publish(AuthenticationResult.Success(authRequest.DeviceId));
}
else{
result
= result.Publish(AuthenticationResult.Failure(StatusCode.CUSTOM_ERROR, "验证失败,密码错误"));
}
});
returnresult;
}
}

3.添加消息编解码代码
support.AddMessageCodecSupport(MessageTransport.Tcp, () => Observable.Return(new ScriptDeviceMessageCodec(support.Script)));, 可以自定义编解码,
ScriptDeviceMessageCodec
代码如下:

usingDotNetty.Buffers;usingJint;usingJint.Parser;usingMicrosoft.CodeAnalysis.Scripting;usingMicrosoft.Extensions.Logging;usingRulesEngine.Models;usingSurging.Core.CPlatform.Codecs.Core;usingSurging.Core.CPlatform.Utilities;usingSurging.Core.DeviceGateway.Runtime.Device.Message;usingSurging.Core.DeviceGateway.Runtime.Device.Message.Event;usingSurging.Core.DeviceGateway.Runtime.Device.Message.Property;usingSurging.Core.DeviceGateway.Runtime.Device.MessageCodec;usingSurging.Core.DeviceGateway.Runtime.RuleParser.Implementation;usingSurging.Core.DeviceGateway.Utilities;usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingSystem.Reactive.Linq;usingSystem.Reactive.Subjects;usingSystem.Runtime;usingSystem.Text;usingSystem.Text.Json;usingSystem.Text.RegularExpressions;usingSystem.Threading.Tasks;namespaceSurging.Core.DeviceGateway.Runtime.Device.Implementation
{
public classScriptDeviceMessageCodec : DeviceMessageCodec
{
public string GlobalVariable { get; private set; }public string EncoderScript { get; private set; }public string DecoderScript { get; private set; }public IObservable<Task<RulePipePayloadParser>>_rulePipePayload;private readonly ILogger<ScriptDeviceMessageCodec>_logger;public ScriptDeviceMessageCodec(stringscript) {

_logger
= ServiceLocator.GetService<ILogger<ScriptDeviceMessageCodec>>();
RegexOptions options
= RegexOptions.Singleline |RegexOptions.IgnoreCase;string matchStr = Regex.Match(script, @"var\s*[\w$]*\s*\=.*function.*\(.*\)\s*\{[\s\S]*\}.*?v", options).Value;if (!string.IsNullOrEmpty(matchStr))
{
DecoderScript
= matchStr.TrimEnd('v');
DecoderScript
= Regex.Replace(DecoderScript, @"var\s*[\w$]*\s*\=[.\r|\n|\t|\s]*?(function)\s*\([\w$]*\s*\)\s*\{", "", RegexOptions.IgnoreCase);
DecoderScript
= DecoderScript.Slice(0, DecoderScript.LastIndexOf('}'));
EncoderScript
= script.Replace(DecoderScript, "");

}
var matchStr1 = Regex.Matches(script, @"(?<=var).*?(?==)|(?=;)|(?=v)", options).FirstOrDefault(p=>!string.IsNullOrEmpty(p.Value))?.Value;if (!string.IsNullOrEmpty(matchStr1))
{
GlobalVariable
= matchStr1.TrimEnd(';');
}
var ruleWorkflow = newRuleWorkflow(DecoderScript);
_rulePipePayload
=Observable.Return( GetParser( GetRuleEngine(ruleWorkflow), ruleWorkflow));
}
public override IObservable<IDeviceMessage>Decode(MessageDecodeContext context)
{
var result = Observable.Return<IDeviceMessage>(null);
_rulePipePayload.Subscribe(
async p =>{var parser = awaitp;
parser.Build(context.GetMessage().Payload);
parser.HandlePayload().Subscribe(
async p =>{try{var headerBuffer=parser.GetResult().FirstOrDefault();var buffer =parser.GetResult().LastOrDefault();var str =buffer.GetString(buffer.ReaderIndex, buffer.ReadableBytes, Encoding.UTF8);var session = awaitcontext.GetSession();if (session?.GetOperator() == null)
{
var onlineMessage = JsonSerializer.Deserialize<DeviceOnlineMessage>(str);
result
=result.Publish(onlineMessage);
}
else{var messageType = headerBuffer.GetString(0, 1, Encoding.UTF8);if (Enum.Parse<MessageType>(messageType.ToString()) ==MessageType.READ_PROPERTY)
{
var onlineMessage = JsonSerializer.Deserialize<ReadPropertyMessage>(str);
result
=result.Publish(onlineMessage);
}
else if (Enum.Parse<MessageType>(messageType.ToString()) ==MessageType.EVENT)
{
var onlineMessage = JsonSerializer.Deserialize<EventMessage>(str);
result
=result.Publish(onlineMessage);
}
}
}
catch(Exception e)
{

}
finally{
p.Release();
parser.Close();
}
});
});
returnresult;
}
public override IObservable<IEncodedMessage>Encode(MessageEncodeContext context)
{
context.Reply(((RespondDeviceMessage
<IDeviceMessageReply>)context.Message).NewReply().Success(true));return Observable.Empty<IEncodedMessage>();
}
privateRulesEngine.RulesEngine GetRuleEngine(RuleWorkflow ruleWorkflow)
{
var reSettingsWithCustomTypes = new ReSettings { CustomTypes = new Type[] { typeof(RulePipePayloadParser) } };var result = new RulesEngine.RulesEngine(new Workflow[] { ruleWorkflow.GetWorkflow() }, null, reSettingsWithCustomTypes);returnresult;
}
private async Task<RulePipePayloadParser>GetParser(RulesEngine.RulesEngine engine, RuleWorkflow ruleWorkflow)
{
var payloadParser = newRulePipePayloadParser();var ruleResult = await engine.ExecuteActionWorkflowAsync(ruleWorkflow.WorkflowName, ruleWorkflow.RuleName, new RuleParameter[] { new RuleParameter("parser", payloadParser) });if (ruleResult.Exception != null &&_logger.IsEnabled(LogLevel.Error))
_logger.LogError(ruleResult.Exception, ruleResult.Exception.Message);
returnpayloadParser;
}
}
}

4.添加元数据配置代码
support.AddConfigMetadata(MessageTransport.Tcp, _tcpConfig)
;
_tcpConfig
代码如下:

        private readonly DefaultConfigMetadata _tcpConfig = newDefaultConfigMetadata("TCP认证配置","key为tcp认证密钥")
.Add(
"tcp_auth_key", "key", "TCP认证KEY", StringType.Instance);

_udpConfig代码如下:

     private readonly DefaultConfigMetadata _udpConfig = newDefaultConfigMetadata("udp认证配置","key为udp认证密钥")
.Add(
"udp_auth_key", "key", "TCP认证KEY", StringType.Instance);

  • 如何加载协议模块,协议模块包含了协议模块支持自定义脚本、添加引用、上传热部署加载。

自定义脚本,选择了自定义脚本解析,如果本地有设置消息编解码,会进行覆盖

引用加载模块

上传热部署协议模块

首先利用以下命令发布模块:

然后打包上传协议模块

四、设备网关

创建TCP设备网关

创建UDP设备网关

五、产品管理

以下是添加产品。

设备接入

六、设备管理

添加设备

Tcp认证配置

添加告警阈值

事件定义

七、测试

利用测试工具进行Tcp测试,以调用
tcp://127.0.0.1:993
为例,

测试设备上线

字符串: 293\0\0{"MessageType":2,"Headers":{"token":"123456"},"DeviceId":"scro-34","Timestamp":1726540220311}

说明:第一个字符表示类型,第二个表示消息内容长度

16进制:32393300007b224d65737361676554797065223a322c2248656164657273223a7b22746f6b656e223a22313233343536227d2c224465766963654964223a227363726f2d3334222c2254696d657374616d70223a313732363534303232303331317d

结果如下:

测试上报属性

字符串:195\0\0{"MessageType":1,"Properties":{"temp":"38.24"},"DeviceId":"scro-34","Timestamp":1726560007339}

16进制:31393500007b224d65737361676554797065223a312c2250726f70657274696573223a7b2274656d70223a2233382e3234227d2c224465766963654964223a227363726f2d3334222c2254696d657374616d70223a313732363536303030373333397d

结果如下:

测试事件

字符串:8307\0{"MessageType":8,"Data":{"deviceId":"scro-34","level":"alarm","alarmTime":"2024-11-07 19:47:00","from":"device","alarmType":"设备告警","coordinate":"33.345,566.33","createTime":"2024-11-07 19:47:00","desc":"温度超过阈值"},"DeviceId":"scro-34","EventId":"alarm","Timestamp":1726540220311}

16进制:38333037007b224d65737361676554797065223a382c2244617461223a7b226465766963654964223a227363726f2d3334222c226c6576656c223a22616c61726d222c22616c61726d54696d65223a22323032342d31312d30372031393a34373a3030222c2266726f6d223a22646576696365222c22616c61726d54797065223a22e8aebee5a487e5918ae8ada6222c22636f6f7264696e617465223a2233332e3334352c3536362e3333222c2263726561746554696d65223a22323032342d31312d30372031393a34373a3030222c2264657363223a22e6b8a9e5baa6e8b685e8bf87e99888e580bc227d2c224465766963654964223a227363726f2d3334222c224576656e744964223a22616c61726d222c2254696d657374616d70223a313732363534303232303331317d

结果如下:

可以在平台界面看到上报的数据

利用测试工具进行Udp测试,以调用udp
://127.0.0.1:267为例,

测试设备上线

字符串:295\0\0{"MessageType":2,"Headers":{"token":"123456"},"DeviceId":"srco-2555","Timestamp":1726540220311}

说明:第一个字符表示类型,第二个表示消息内容长度

16进制:32393500007b224d65737361676554797065223a322c2248656164657273223a7b22746f6b656e223a22313233343536227d2c224465766963654964223a227372636f2d32353535222c2254696d657374616d70223a313732363534303232303331317d

结果如下:

测试上报属性

字符串:197\0\0{"MessageType":1,"Properties":{"temp":"38.24"},"DeviceId":"srco-2555","Timestamp":1726560007339}

说明:第一个字符表示类型,第二个表示消息内容长度

16进制:31393700007b224d65737361676554797065223a312c2250726f70657274696573223a7b2274656d70223a2233382e3234227d2c224465766963654964223a227372636f2d32353535222c2254696d657374616d70223a313732363536303030373333397d

结果如下:

测试事件

字符串:8301\0{"MessageType":8,"Data":{"deviceId":"srco-2555","level":"alarm","alarmTime":"2024-11-07 19:47:00","from":"device","alarmType":"设备告警","coordinate":"33.345,566.33","createTime":"2024-11-07 19:47:00","desc":"温度超过阈值"},"DeviceId":"srco-2555","EventId":"alarm","Timestamp":1726540220311}

说明:第一个字符表示类型,第二个表示消息内容长度

16进制:38333031007b224d65737361676554797065223a382c2244617461223a7b226465766963654964223a227372636f2d32353535222c226c6576656c223a22616c61726d222c22616c61726d54696d65223a22323032342d31312d30372031393a34373a3030222c2266726f6d223a22646576696365222c22616c61726d54797065223a22e8aebee5a487e5918ae8ada6222c22636f6f7264696e617465223a2233332e3334352c3536362e3333222c2263726561746554696d65223a22323032342d31312d30372031393a34373a3030222c2264657363223a22e6b8a9e5baa6e8b685e8bf87e99888e580bc227d2c224465766963654964223a227372636f2d32353535222c224576656e744964223a22616c61726d222c2254696d657374616d70223a313732363534303232303331317d

结果如下:

可以在平台界面看到上报的数据

七、总结

以上是基于Tcp和UDP网络组件设备接入,现有平台网络组件可以支持TCP,MQTT,UDP,CoAP,HTTP,Grpc,websocket,rtmp,httpflv,webservice,tcpclient, 而设备接入支持TCP,UDP,HTTP网络组件,年前尽量完成国标28181和MQTT,也会开始着手开始开发规则引擎,  然后定于11月20日发布1.0测试版平台。也请大家到时候关注捧场。