# packet **Repository Path**: hutiebin/packet ## Basic Information - **Project Name**: packet - **Description**: 在对象与字节报文间转换的Clojure库 - **Primary Language**: Unknown - **License**: EPL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-01-14 - **Last Updated**: 2025-04-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 解析与构建二进制报文或文本报文的通用库 **2.0.0版相比旧版有重大修改,与旧版不兼容** 一个用来解析和生成报文的库,可用于解析和构建二进制通信报文或文本报文,具有以下特点: - 提供简单但功能强大的DSL来定义报文的结构。 - 具有丰富的字段类型。包括整数、bcd编码的整数、十进制数、日期时间、ip地址、字符串等。 - 具有位类型,用于细分多至64位宽的内部结构,且位类型具有与普通类型一样的能力。 - 具有丰富的高阶类型,包括分支、重复、多选一等,分支和重复条件可由前面的字段值进行计算。 - 可以定义与业务值的转换,用户面对的是有意义的业务值,而不是原始的数值。 - 辅助性的字段(如表示其他字段的字节数、重复次数的字段等)可以没有名称,构建报文时也不用提供它们的值,用户只需关注业务对象。 - 提供自定义类型的简单方法。 - 可以嵌入高层规约。 - 提供了多种标准的校验码计算方式,包括cs16和各种crc校验等。 # 初步上手 只需要两个步骤: 1. 用defpacket宏定义规约的结构 2. 调用parse解析报文得到各字段的详细解析信息,或调用build将业务对象转换为报文 先看一个简单示例。 首先定义规约格式: ```clj (ns your.ns.name (:require [packet.core :refer [defpacket parse build]] [packet.types :refer [u]])) (defpacket foo (skip-until 0x68) ;跳过无关字节直到遇到有效报文起始标志0x68 (u 2 as a) ;u是预定义的字段类型,表示无符号整数,2表示本字段占用2字节的空间,a是字段的名称,由as引入 (u 1 as b) 0x16) ;最后以0x16结束 ``` defpacket定义规约结构,foo是规约名称,后面是各个字段的定义。 上面格式要求报文以1字节的0x68开头,后面跟着2字节的无符号整数(默认为高字节在前,可以改变),接着是1字节的无符号整数,最后以0x16结尾。 定义好规约格式后,就可以用parse来解析字节序列: ```clj (parse foo [1 2 0x68 0 10 11 0x16 3 4]) ;=> {:type :group :value [{:value 104, :pos 2, :len 1} {:value 10, :pos 3, :len 2, :name :a} {:value 11, :pos 5, :len 1, :name :b} {:value 22, :pos 6, :len 1}] :pos 2 :len 5} ``` parse按foo规定的报文结构解析一个字节序列,得到解析结果,解析结果中: - :type 结构类型,值:group表示由多个子字段组成的组字段,值:repeat表示重复字段,空表示简单字段。 - :value 解析值,对简单类型为普通值;对组字段,为一个向量,其元素对应各子字段的解析结果;对重复字段,也为一向量,其元素是被重复字段的解析结果。 - :pos 对应整个结构的起始位置。 - :len 整个结构占用的字节数。 解析过程如果出现异常情况,如提供的报文不满足规约要求,则结果中包含:error键,其值是一个序列,包含从外层结构到内层简单字段的错误信息。 要得到业务对象,可使用get-parser-value函数: ```clj (get-parser-value *1) ;=> {:a 10 :b 11} ``` 如果已知各字段的值,要构建对应的报文,可用build函数: ```clj (build foo {:a 10, :b 11}) ;=> [0x68 0 10 11 0x16] ``` 如果构建过程中发生错误,则返回一个映射,其中包含:error键,其值是一个序列,包含从外层结构到内层简单字段的错误信息。 下面介绍详细用法 # 定义规约结构 下面以二进制规约为例来介绍。对文本规约,介绍中出现的数字或字节都可替换为token字符串。 用defpacket宏定义规约的结构,语法为 ```clj (defpacket name doc-string? flags & fields) ``` 其中name是规约名,fields是规约报文各个组成部分(以下称为字段)的描述,按先后顺序依次排列。 有三种方式描述一个字段: - 连续出现的数字和符号 数字对应报文中的固定值,符号对应报文中的一个字节,相同的符号对应相同的字节,除非这个符号是**?**,它可以对应任意字节。 - `(type len & args)` 其中 - type 字段类型。放置字段类型的这个位置称为类型位置。 - len 本字段占用的字节数。如果当前类型的字节数是固定的,如ipaddr类型,则不需该信息。 - args 某些类型需要的其他参数,如d类型需要的系数等。 - 结构字段,在讲到具体应用时再描述。 **后面描述的结构字段采用了一些与clojure标准库函数(或宏)同名的符号,如if、when、case、cond、repeat、while等,它们出现在类型位置时表示的是结构类型,它们出现在表达式的函数位置时表示的是通常的函数或宏,注意不要混淆。** ## 基本类型 本库在packet.types空间下预定义了一些基本类型,如下所示,其中n表示**长度,即字段所占用的字节数** - `(i n)` 表示有符号整数。 - `(u n)` 表示无符号整数。 - `(bcd n)` 压缩bcd编码的有符号整数,最高字节的最高位为1表示负数,因此最高字节最大能表示的数的绝对值为79。 - `(ubcd n)` 压缩bcd编码的无符号整数,每一个字节表示两位10进制数。无效的格式与nil对应,nil与全0xff对应。 - `(ud n coef)` 用无符号压缩bcd码表示的浮点数,该浮点数是bcd码对应的整数乘以系数coef得到的。 - `(d n coef)` 用有符号压缩bcd码表示的浮点数,该浮点数是bcd码对应的整数乘以系数coef得到的。 - `(char)` 用一个字节表示的一个字符。 - `(s n)` 占用n个字节的字符串,多余的空间用0填充。 - `(zstr)` 以0字节结束的字符串类型,占用空间可变。 - `(lstr)` 第一个字节表示字符串的长度,后跟字符串内容,占用空间可变。 - `(dstr n)` 用n字节字符串保存的正负浮点数。 - `(ipaddr)` 用4个字节存储的ip地址,业务类型为java.net.InetAddress。 - `(datetime format)` 日期时间类型,在后面专门描述。 - `(integer)` 整数token,token是一个字符串。 - `(decimal)` 浮点数token。 - `(regex re)` 匹配正则表达式re的token。 如果nil是有效的业务值,则不能使用u、i两个类型,只能使用内部用bcd码表示的几个数值类型。这些类型用全0xff字节表示nil。 **基本类型须带名字空间使用,除非在当前名字空间中`use`或`refer`它们** ## 修饰符与修饰语 修饰语附加在字段定义中,为字段提供额外功能。修饰符就是引导修饰语的特殊符号。修饰符与它的参数一起称为修饰语。不要用下面介绍的各类修饰符作为字段的名称。一般用法为 `(u 2 as a)` 其中`as`就是一个修饰符,`a`是其参数,`as a`称为修饰语,这个修饰语是为当前字段命名。前面的基本部分`(u 2)`称为宿主字段。下面分别解释各类修饰符。 ### as 为宿主字段指定名称,有名称的字段可以被后续字段引用。字段名不能包含.和$字符。字段名称不是必须的。举例如下: `(u 2 as a)` 将宿主字段命名为a。as修饰语可出现在任意位置,但通常作为第一个修饰语出现。后面介绍修饰符时不再写出宿主字段。 ### prompt - `prompt "prompt-string"` 用于指定错误提示信息。当字段解析或构建错误时,返回的错误信息中包含"prompt-string"字符串。 对二进制规约,错误提示不必以prompt修饰符来引导,宿主字段的最后一个字符串将作为该字段的错误提示信息。如果宿主字段包含字符串类型的参数(如datetime类型)而又不想为它设置错误提示信息,为了避免该字符串参数被误认为是错误提示信息,必须在字段参数后人为增加一个空字符串(或packet.core/NOP)。 ### with 指定与业务值间的转换,可有几种形式: - `with [:a :b :c]` 表示报文中的数字值依次对应向量中提供的业务值。 - `with {17 :tcp 11 :udp}` 表示报文中的数字值按照给定映射对应于业务值。提供的映射的键的类型必须与宿主字段的类型相符,但通常应该是整数。 - `with f1 ...` 将报文中的值d用(f1 d ...)转换为业务值,f1应该是一个严格单调的连续函数。业务值向报文值的转换是通过自动分析f1及其参数得到的逆函数来进行的。 - `with f1 ... and f2 ...` 如果f1不是严格单调的连续函数,则无法自动分析得到其逆函数,此时需要提供f2将业务值v用(f2 v ...)转换为数字值。请确保f2的返回值的类型与当前宿主字段相适应。业务值可以是任何类型,不限于数值类型。 在通过向量或映射进行转换的情况下,如果没有为某数字值指定业务值,解析时会直接输出原始数字值。构建时只需为该字段提供业务值,如果提供的业务值没有对应的键值,也会直接使用提供的值构建报文,这可能会带来意想不到的结果,要谨慎对待。 如果通过向量或映射给定的业务值有重复,则在构建时无法保证选择哪个数字值,因此应避免这种情况。 向量或映射中不能有变量引用,否则无法通过编译。 组字段和重复字段也可以有with修饰语,f1的参数是映射(对组字段)或向量(对重复字段),经过变换后,这些字段将作为整体视为简单字段。f2的输出应该是一个映射(对组字段)或向量(对重复字段)。 **不能使用返回转换函数的表达式,如`(partial * 100)`,须写成`#(* 100 %)`** ### default 为宿主字段指定默认值, `default 3` 表示宿主字段的默认值为3。构建时如果没有指定该字段的值,则使用默认值。 可以用包含变量引用的表达式来指定默认值,这样默认值就是与其他字段的值有关的动态值。 该修饰语通常放在with修饰语之后,这样指定默认值时可使用业务值。 ### should 对字段值进行检验,用法如下: `should f ...` 对字段值v应用(f v ...),如果结果为任何真值,则有效,否则判定该字段的值无效。如果该修饰语在with修饰语后,v是业务值,否则是原始值。通常将它放在with修饰语后,以便针对业务值进行检验。 如果函数f是=或zero?,则构建时可以不提供该字段的值,本系统会自动计算满足要求的值。 构建时也会检查提供的值是否满足要求。某些情况下可能需要跳过这种检查,则可以如下实现: ```clj (binding [packet.core/*skip-check* true] (build ...)) ``` ### count-of 表示宿主字段的值是另外一个重复字段的重复次数。用法举例: ```clj (u 1 count-of a +1) ``` 表示宿主字段的值是a字段的重复次数加1,这里a字段必须是一个重复字段。如repeat和while字段等。 构建时该字段的值可以省略,自动根据a字段的重复次数计算。 ### length-of 表示宿主字段的值是另外一个字段的字节数,用法举例: ```clj (u 1 length-of a +4) ;表示宿主字段的值是a字段的字节数加4 ``` 构建时该字段的值可以省略,自动根据a字段的字节数计算。 ### length - `length from a to b excludes c d -4` - `length from a until b excludes c d -4` 表示该字段的值是从a字段到b字段总的字节数除开c、d字段的字节数再减4,前者包含b字段,后者不包含。 from a可以省略,表示从报文有效起始位置开始。to b或until b也可以省略,表示到当前报文段结束。 不能同时用to和until指定结束位置。 构建时该字段的值可以省略。 ### checksum - `checksum from a to b use cs16` - `checksum from a until b use cs16` 表示宿主字段的值是从a字段到b字段所有字节的校验码,校验码的计算方式为cs16。 from a可以省略,表示从报文有效起始位置开始。to b或until b也可以省略,表示到当前字段之前。 不能同时用to和until指定结束位置。 构建时该字段的值可以省略。 在packet.checksum空间下定义了一些常用的校验码计算函数,有cs16、lrc、crc8、crc16、crc32、crc16-modbus。 如果不指定校验函数,则使用cs16。 ### (break ...) 任何字段前面加上break的作用是,如果这个字段成功解析且消耗了字节数据,则不再解析同组的后续字段,即终止当前组。 break修饰符没有参数,且必须防止字段的最前面。如: ```clj (group ... (break when (> $a 1) (u 2)) ...) ``` 如果前面a字段的值大于1,则在解析了两个字节的无符号整数后,不再解析同组的后面的所有字段。 ### (expect ...) 任何字段前面加上expect的作用是,测试当前位置是否能满足指定字段,但不消耗数据。 - `(expect bcd 2)` 测试当前位置是否是一个两字节bcd数 - `(expect 1)` 测试当前位置是否是字节1 expect修饰符没有参数,且必须防止字段的最前面。 ### sep-by sep-by后面的内容形成一个作为分隔符的字段,它将宿主字段变为用指定分隔符分隔的重复字段。 - `sep-by u 2` 用两字节无符号整数分隔 - `sep-by 1 2` 用两字节1和2分隔 - `sep-by ","` 用逗号分隔 ### 修饰语总结 带修饰语的字段的一般格式为 ```clj (break/expect u 2 as field-name prompt prompt-string with f1 ... and f2 ... default ... should predicate ... count-of ... length ... length-of ... checksum ... sep-by ...) ``` 根据逻辑语义,count-of、length、length-of、checksum只能用于简单字段,且最多只能出现其中一个。 除以上基本类型外,还有一些特殊类型和组合类型以及位类型,它们没有名字空间,可直接使用。 ## 特殊类型 除上面的基本类型外,本库还定义了一些特殊类型,下面用举例来说明它们的用法。 ### token 该类型定义在packet.core空间,用法举例: - `(token)` 返回一个字节或一个token。 ### raw 该类型定义在packet.core空间,用法举例: - `(raw 4)` 该类型以序列形式返回4个字节的原始内容 - `(raw as a)` 由前面某个具有`length-of a`或`count-of a`修饰语的字段的值决定字节数 第一种用法通常用于自定义字段类型,第二种用法通常用于嵌入规约。 对嵌入规约,通常用法如下: 底层规约中通常有一个字段指定嵌入规约的名称,如`(u 1 as protocol with {11 :udp 17 :tcp})`,另一字段指定嵌入规约的长度,如`(u 2 length-of a)`。 解析时,先提取原始字节序列(长度由前面那个具有`length-of a`修饰语的字段的值决定),再根据protocol字段指定的规约类型进一步解析这些原始字节序列。 构建时,先构建嵌入规约的字节序列,再将该序列作为raw字段的值并相应设置protocol字段的值,以此来构建底层规约。 ### fixed 匹配指定的内容,如 ```clj (fixed 0x16) ``` 如果当前位置不是指定的内容,则失败。 ### pattern 匹配一个模式,用法举例: `(pattern 0x68 0x10 a b ? ? a b 0x68)` 数字(值必须在0-255字节范围内)必须准确匹配,符号可以匹配任意字节,相同的符号匹配相同的字节,?可随意匹配任意字节。 符号匹配的字节可以在后面的字段中引用。 它的输出和组字段类似,包含各个部分的内容。构建时需要用映射提供其中符号部分的值。 通常你不必显式地使用它,你可以这样: ```clj (defpacket prot ... 0x68 0x10 a b ? ? a b 0x68 ... ) ``` defpacket会自动将其转换为 ```clj (defpacket prot ... (pattern 0x68 0x10 a b ? ? a b 0x68) ... ) ``` 如果pattern内只有一个数值,如`(pattern 0x68)`,则它与`(fixed 0x68)`等效。 ### skip 跳过一个或多个无关字节,用法举例: ```clj (skip 3) ``` 上例表示跳过3个字节。如果不提供参数,则跳过1个字节。构建时用0填充。 ### reserved 保留字段,用法举例: ```clj (reserved 0x55 0x66) ``` 上例表示解析时跳过两个字节,构建时用0x55和0x66填充这个字段。 ### padding 对齐字段,用法: ```clj (padding n v) ``` 使得下一个字段位于n字节的整数倍边界上,v是构建时用于填充的字节,如果不提供则用0填充 举例如下: ```clj (defpacket foo (u 5) (padding 4) ;填充合适数量的(这里是3个)字节使得后面的a字段对齐4字节边界。 (u 2 as a)) ``` ### label `(label a)` 定义一个没有任何内容的字段,主要目的就是定义一个名称共其他字段使用,如给checksum修饰语引用用于指定校验范围。 ### success 不消耗内容,总是成功的类型,通常只用在or、case、cond字段的最后,用于保证这些字段不会失败。用例如下: ```clj (success) ``` ### fail 不消耗内容,总是失败的类型。这个类型极少使用,用例如下: ```clj (fail "some reason") ``` ## 组合类型 组合类型表示其他字段有条件地存在、或重复、或选择等。 下面出现的expr均表示一个普通clojure表达式,里面函数位置的符号与这里定义的类型无关,就是通常的clojure函数、宏、或特殊形式。 表达式里面可以使用与字段名相同的符号,但须前缀$,代表对应字段的值。**当然这些字段必须在当前字段之前定义。** 如果要引用的字段处于不同的层次,则用.分开各个层次。也可以引用def定义的全局变量。 ### [& fields] 将多个字段组合起来作为一个整体,视为一个字段。某些结构由固定数量(而不是任意数量)的字段组成,如if结构只能提供1到2个字段供条件满足和不满足时分别应用。case结构和cond结构中与值或条件对应的也只能是一个字段。这时可将多个字段放入[]中,将这些字段视为一个整体,作为一个组合字段。如 ```clj (if (> $a 100) [(u 1) (i 2)] (bcd 3)) (case $a 1 [(u 1) (i 2)] 2 (u 1) ...) ``` ### (group :unordered :optional & fields) 除了用[]来组合字段外,也可显式地用group来组合字段。与前者相比,后者的优势在于可以为这个组命名和指定选项,如 `(group as a (u 1) (i 2))` 前面两个可选的关键字对组的功能进行了微调。如果指定了:unordered,则组内字段的顺序可以变化。如果指定了:optional,则组内的一个或多个甚至全部字段可以不出现。 ### skip-until 跳过字节直到满足一定的条件,只能用于规约的第一个字段,用于寻找报文的有效起始位置。 用法举例: - `(skip-until 0x68 0x10 a b ? ? a b 0x68)` skip-until后的数字和符号被打包到一个pattern结构中,因此它就相当于`(skip-until (pattern 0x68 0x10 a b ? ? a b 0x68))`,表示跳过字节直到满足这个9字节的模式。 - `(skip-until 0x68 (u 2 as len) (u 2 should = $len) 0x68)` 跳过字节直到遇到0x68和两个相同的2字节整数以及另一个0x68。skip-until内可包含一个或多个任意字段,这些字段必须依次同时满足。 ### (packet name) 嵌入先前由`(defpacket name ...)`定义的协议报文。 **如果没有为该类型的字段指定名称,则自动用name作为字段名称,这是解析结果的安放之处。** ### (if expr field1 field2) 当表达式expr计算结果为真值时,应用field1字段,否则应用field2字段。如果两种情况需要应用多个字段,可以将多个字段放在向量中设为一个字段。 field2可以省略,此时如果expr计算结果为假,则忽略本字段。expr是任何合法的clojure表达式,其中可以引用前面字段的值,方法是在字段名前加$符号,如$a表示a字段的值。层次结构内部的字段可以用$a.b.c的形式指定,表示a字段内部的b字段内部的c字段的值。举例如下: ```clj (u 1 as dir) (if (= $dir 1) [(u 1 as a) (bcd 2 as b) (i 1 as c)] [(d 3 0.001 as d) (i 2 as e)]) ``` 当dir的值为1时,应用a、b、c三个字段,否则应用d、e两个字段 ### (when expr & fields) 当表达式expr的计算结果为真值时,应用后续字段,否则忽略本字段。举例如下: ```clj (u 1 as dir) (when (= $dir 1) (u 1 as a) (bcd 2 as b) (i 1 as c)) ``` 当dir的值为1时,应用a、b、c三个字段。 ### (break-when expr & fields) **已作废,请使用`(break when ...)`** 如果条件满足,在应用完fields后直接跳出当前字段组,忽略当前字段组内本字段后面的同级字段。 ### (or & fields) 依次应用各个字段,直到某一个成功,余下的字段不应用。如果均不成功,则总体失败。 ### (option & fields) 可选字段,即如果fields中任一字段无法正常解析就忽略所有字段。 ```clj (option (u 1 should = 1) (bcd 1 as a)) ``` 在构建时,如果没有提供某个子字段的值且该值无法自动推断,则不构建整个option字段。 ### (repeat n as a & fields) n是一个整数,表示重复次数。如果前面某个字段有`count-of a`修饰语,则可以省略n,由那个字段指定重复次数,这是通常的应用模式,因为一般来说事先并不知道重复次数。 如果n为负数,则重复次数必须至少为其绝对值。实际重复次数仍由前面某个字段有`count-of a`修饰语的字段指定。 如果既没有直接指定重复次数,也没有用具有count-of修饰语的字段指定重复次数,则将一直重复直到失败为止,返回失败之前的结果。 **该类型的字段如果没有with修饰语将其转化为普通字段,则必须用as修饰语指定名称,因为解析结果是一个数组(即使只有一个值甚至没有值),必须有地方安放这个数组。** ```clj (repeat 5 as a ...) (u 1 count-of b) ... (repeat as b ...) ``` 表示字段a重复5次,字段b重复的次数由前面那个具有count-of修饰语的字段的值决定。 表示重复字段b的重复次数由前面那个带有count-of修饰语的字段的值决定,那个字段不需要有名称,构建时也不需要给出那个字段的值。 ### (attempt-repeat & fields) 重复多个fields字段,重复次数要保证attempt-repeat后面的字段能解析成功。如果重复0次也无法保证后续的字段解析成功,则本字段解析失败。 ### (while expr as a & fields) 在expr计算结果为真时应用fields,如此重复,直到计算结果为假。 **该类型的字段必须有名称,因为解析结果是一个数组(即使只有一个值甚至没有值),必须有地方安放这个数组。** while字段的expr通常会引用预定义的变量$=,这个特殊变量表示报文的当前位置,其值在每次重复后都会改变,因此每次计算expr才可能得到不同的结果,最终终止重复。 有时候报文中没有一个字段明确表示重复次数,但有一个与报文长度有关的字段, 并且这个字段计及的报文范围涵盖了重复字段。假设该字段名为len,并且假设涵盖范围**之前**的字段长度为outer-before, 涵盖范围**之内**处于重复字段之后的字段的长度为inner-after,则重复条件可这样设置: `(< (+ $= inner-after) (+ outer-before len))` 其中inner-after和outer-before都可能是一个表达式,举例如下: ```clj (defpacket aname (i 4 as p) (u 2 as len length-of data) (when (>= $p 0) as data ... (while (< (+ $= (if (zero? $p) 2 0)) ;if表达式是b字段的长度 (+ 6 $len)) as a ...) (when (zero? $p) (u 2)))) ... ``` ### (case expr v1 field1 v2 field2 ...) 首先计算expr,应用与该值对应的field,如果没有对应的值,并且最后有兜底的field,则应用该field,否则失败。如果要避免失败,可最后放一个`(succeed)`字段来兜底。 如果多个值对应同样的field,可以用这样的形式: ```clj (case expr (1 5 - 10 13) field1 18 field2 ...) ``` 上面表示expr的值为1、或5到10、或13时应用field1,为18时应用field2。 如果某个值需要对应多个字段,可以把这些字段放在向量中。 如果某个值对应的字段是其他多个值对应的字段的组合,则可用这样的形式: ```clj (case expr 1 field1 2 field2 3 field3 4 field4 5 field5 7 field7 9 (++ 1 - 5 7) ...) ``` 表示9对应的字段是1、2、3、4、5、7各字段的顺序叠加。 ### (cond expr1 field1 expr2 field2 ... else field) 依次计算各个表达式expr,如果某个结果为真,则应用对应的field,否则应用else对应的field,如果没有提供else字段,则本字段解析或构建失败。如果要避免失败,可在最后加上`else (succeed)`来兜底。 ## 标志类型 有的字段是由一个或多个位段组成的,这时可用标志类型,如下所示: ```clj (flag (b0 as df with [:disable :enable]) ;表示df位段占用第0位。位序号从0开始,它是低字节的最低位。低字节是第一个字节还是最后一个字节由规约的:little-endian标志决定。 (b3-15=0 as ofs with * 8) ;表示ofs位段占用第3到15位,默认值为0,业务值要乘以8 (if expr ;根据条件选用的字段,这是唯一允许出现在位字段中的类型 ... ...) ) ``` 标志类型由无符号整数表示,字节数由最高位的序号决定,以能提供最大序号的位为限。内部的位字段可用 `bm-n=d` 表示,m为起始位序号,n为终止位序号,d为默认值。如果-n省略,则只占1位。如果-d省略,则不指定默认值。d是原始值。可以联合使用with和default修饰语用业务值来指定默认值。位字段也可以具有length等修饰语。 注意各部分之间没有空格。 ## 特殊事项 **仅针对二进制字节报文:**有的规约对报文特定范围内的字节进行了加扰处理,如对字节进行加0x33处理,对这样的规约,可在需要加扰的范围前后分别插入 `(encode + 0x33)` 和 `(end-of-encode)` 来规定需要加扰的范围。对这样的报文,解析时会自动去扰。 **注意,加扰范围不可嵌套和重叠,但可以有多个分离的区域** ## 层次关系 一个字段可以包含多个子字段,如 ```clj (defpacket foo (u 1 as a) (when as b (= $a 1) (u 1 as b1) (u 2 as b2))) ``` 表示b字段仅在a字段为1时才存在,如果b字段存在的话,则它包含b1和b2两个字段共3个字节。 解析结果中b字段的值以 ```clj {:a 1 :b {:b1 ... :b2 ...}} ``` 的形式存在。如果不需要这个多余的层次,则在定义报文结构时可省略b,如下所示: ```clj (defpacket foo (u 1 as a) (when (= $a 1) (u 1 as b1) (u 2 as b2))) ``` 此时解析结果为 ```clj {:a 1 :b1 ... :b2 ...} ``` # 解析报文 调用形式为 ```clj (parse name bytes env) ``` 其中 - name 前面用defpacket定义的规约名。 - bytes 一个实现了IndexedBytes协议(可以只实现其读取部分)的对象。 - env 一个映射,可以提供规约定义中引用的非字段的值。可以省略。 IndexedBytes协议定义了三个方法: - `(size [this] "返回可供读取的字节数量")` - `(getb [this index] "返回指定位置处的字节")` - `(setb [this index val] "设置指定位置处的字节,返回更新后的对象")` 本库已经将该协议扩展到字节数组、java.nio.ByteBuffer、io.netty.buffer.ByteBuf和实现了clojure.lang.Indexed接口的类(如向量)上,可以直接使用这些对象传递报文。 如果只用于解析,bytes的类型可以只实现前两个方法。如果要用于构建,则必须实现所有方法。 对java.nio.ByteBuffer和io.netty.buffer.ByteBuf来说,解析并不消耗字节,即不会移动其当前位置(即不会改变position或readerIndex。 # 构建报文 调用形式为 ```clj (build name domain env) ``` 其中 - name 前面用defpacket定义的规约名, - domain 业务对象(一个映射),键是字段名对应的关键字。注意提供的值是业务值而不是报文中的数值。业务值可能是一个复杂的对象,由字段类型决定。 - env 环境变量,就是一些预设的字段值或其他信息。如果提供报文缓冲区,则它放在:dest键下。 如果构建过程没有错误,则返回包含报文内容的字节向量,否则,返回一个映射,包含错误信息。 提供的业务对象中可以不提供某些字段的值,这些字段有以下几种情况: - 用default修饰语指定了默认值的字段以及指定了默认值的位字段 - 表示其他字段的重复次数,即有count-of修饰语的字段 - 表示其他字段的字节数,即有length或length-of修饰语的字段 - 检查和字段,即有checksum修饰语的字段,这种字段将根据报文内容自动计算 - 有should修饰语并使用=函数进行检验的字段,将自动提供满足检验要求的值 **除了这些类型的字段以及固定内容的字段外,其他字段都应该有名称,否则,解析时无法返回其值,构建时无法指定其值。** # 大端小端(字节序) 多字节的整数按照网络传输的惯例默认高字节在前,如果要改为低字节在前,在定义规约时要加上:little-endian标志,如 ```clj (defpacket name :little-endian ... ) ``` # 时间类型 如果用bcd码保存年月日时分秒,并且业务值用java.util.Date类型来表示时间,则可使用packet.types中预定义的datatime,用法如下: `(datetime format)` format是类似"YYMDhms"这样的字符串,每个字符对应一个字节,表示各字节对应的时间信息,其中 - Y 年,如果有两个连续的Y,表示用两字节表示4位年,如2023。如果只有一个Y,则没有世纪年,如23。 - M 月 - D 日 - h 时 - m 分 - s 秒 java.util.Date类型的字面量格式为`#inst "2021-05-10T09:15:00.000-00:00"`表示,并总是采用0时区, 如果换成东八区的时间,则为`#inst "2021-05-10T17:15:00.000+08:00"`。 前导的0不能省略。日期要完整,时间后面的部分可省略。如`#inst "2023-04-02T05"`是有效的日期。 # 自定义类型 如果要定义自己的类型,可用packet.types/deftype定义,如下所示: ```clj (deftype name doc-string? [& args]? & fields) ``` - name 类型名 - doc-string 可选的文档字符串 - args 可选的类型参数,使用时需要提供对应的参数 - fields 构成本类型的一个或多个字段 举例如下: ``` (deftype d "bcd码表示的十进制类型,n为占用的字节数,coef为系数" [n coef] (bcd n with * coef)) (d 3 0.001) ``` 上面先定义了一个d类型,后面使用这个类型,指定用3个字节表示,系数为0.001 也可以从原始类型来定义自己的类型,如 ```clj ;;下面p指packet.core命名空间,u指packet.utils命名空间 (defn u "n字节无符号整数" [n] (if p/*build* (p/map-input (p/raw n) u/uint->bytes n) (p/map-value (p/raw n) u/bytes->uint))) ``` 为了使类型名称更有业务含义,可以如下定义(voltage是电压的意思): ```clj (deftype voltage (d 2 0.1)) ``` 有了上面的定义,则可以如下使用 ```clj (voltage) ``` 对于需要重复使用的字段组,也可用deftype定义,如下所示 ```clj (deftype name doc-string [& args] & fields) ``` doc-string为文档字符串,可以省略。如果不需要参数,也可以省略[& args],连空的[]都可以不要。 fields的格式同defpacket。定义之后,name可作为类型名使用 # 一些校验函数 在packet.checksum空间下预定义了一些校验函数 ## cs8 所有字节按模256相加,丢弃进位,只保留低字节。 ## lrc 纵向冗余校验,就是所有字节按位异或,得到一个字节的结果。 ## cs16 计算16位校验和,固定以高字节在前的方式计算,按两字节一组相加,前面的是高字节。字节数为奇数时在最后补一个0字节。计算结果超出16位时多出的加到低16位,最后按位取反。 验证时如果将原字节序列和校验码放在一起再次应用校验函数,所得结果应为0。 这是ip协议的报头的校验方式。 下面是几种循环冗余校验方式,由于计算复杂,一般用于有硬件支持的底层协议中。 ## crc8 8位循环冗余校验,生成多项式为(x^8 + x^2 + x + 1) ## crc16-ccitt 16位循环冗余校验,生成多项式为(x^16 + x^12 + x^5 + 1) ## crc16-modbus 16位循环冗余校验,生成多项式为(x^16 + x^15 + x^2 + 1) # 文本型规约 **如果定义文本型规约,必须在当前空间最前面增加`(def define-text-packet true)`,由于有这个要求,不能在同一个命名空间中同时定义二进制规约和文本规约** 解析文本规约前必须自己将文本字符串分割为token序列,构建文本规约时得到的也是token序列。 文本型规约的示例可以参考gitee.com/hutiebin/dxf项目,它是一个svg文件与dxf文件互转的库,它从规约解析和构建的角度实现文件格式的转换。 # 其他工具 为自定义类型可以使用以下函数在已有类型的基础上定义新的类型。 - mapping - map-input - map-value - flatmap # 示例 以解析ip报文为例 ```clj (ns example.ip (:require [packet.core :as p :refer [defpacket parse build]] [packet.types :as t :refer [deftype u bcd ipaddr]] [packet.checksum :as cs])) (deftype header-options "头部选项,仅做示例" (option 1 ;假设1是该选项的特定标志 (u 1 as opt-1) (padding 4)) (option 2 ;假设2是该选项的特定标志 (bcd 2 as opt-2) (padding 4))) (defpacket ip (flag (b4-7 as ver "版本号" with {4 :ipv4 6 :ipv6} default :ipv4) (b0-3 "头部长度" with * 4 length until data)) (u 1 as tos "区分服务") (u 2 "数据长度" length-of data) (u 2 as id "标识符") (flag (b1 as df "允许分片" with [:disable-fragment :enable-fragment]) (b2 as mf "更多分片" with [:no-more-fragment :more-fragment]) (b3-15 as frag-offset "分片偏移" with * 8)) (u 1 as ttl "生存时间") (u 1 as protocol "上层协议" with {17 :tcp} default :tcp) (u 2 checksum to dest-ip use cs/cs16) (ipaddr as source-ip "源地址") (ipaddr as dest-ip "目标地址") (header-options) (raw as data "上层报文")) (def m {:tos 2 :id 1111 :df :enable-fragment :mf :more-fragment :frag-offset 96 :ttl 4 :source-ip (t/ip-reader "192.168.0.1") :dest-ip (t/ip-reader "10.18.16.3") :opt-2 55 :data [9 8 7 6] ;模拟上层报文 }) (build ip m) => [70 2 0 4 4 87 0 102 4 17 215 109 -64 -88 0 1 10 18 16 3 2 0 85 0 9 8 7 6] (get-parser-value (parse ip *1)) => {:frag-offset 96, :dest-ip #ip "10.18.16.3", :protocol :tcp, :tos 2, :df :enable-fragment, :mf :more-fragment, :ttl 4, :hlen 24, :id 1111, :source-ip #ip "192.168.0.1", :ver :ipv4, :opt-2 55, :data (9 8 7 6)} ``` 在报文格式定义中一些没有名称的字段通常用来说明其他字段的长度、重复次数、或者是校验码等,它们都是自动计算的字段。构建时不必提供他们的值,在报文解析时会比较实际值与计算值是否相符。 以上示例在src/packet/example下。其下还定义了电表DLT645规约和计量自动化终端上行规约,可以参考。 # 许可协议(License) Copyright © 2023-01-18 This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License, v. 2.0 are satisfied: GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version, with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html.