Liking cljdoc? Tell your friends :D

解析与构建二进制报文的通用库

2.0.0版相比旧版有重大修改,与旧版不兼容

一个用来解析和生成字节报文的库,可用于解析和构建二进制通信报文,具有以下特点:

  • 提供简单但功能强大的内部DSL来定义报文的结构
  • 具有丰富的字段类型。包括整数、bcd编码的整数、十进制数、日期时间、ip地址、字符串等。
  • 具有位类型,用于细分多至64位宽的内部结构,且位类型具有与普通类型一样的能力。
  • 具有丰富的高阶类型,包括分支、重复、多选一等,分支和重复条件可由前面的字段值进线计算。
  • 可以定义与业务值的转换,用户面对的是有意义的业务值,而不是原始的数值。
  • 辅助性的字段(如表示其他字段的字节数、重复次数的字段等)可以没有名称,构建报文时也不用提供它们的值,用户只需关注业务对象。
  • 提供自定义类型的简单方法。
  • 可以嵌入高层规约。
  • 提供了多种标准的校验码计算方式,包括cs16和各种crc校验等。

初步上手

只需要两个步骤:

  1. 用defpacket宏定义规约的结构
  2. 调用parse解析二进制报文得到各字段的详细解析信息,或调用build将业务对象转换为二进制报文

先看一个简单示例。

首先定义规约格式:

(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)

defpacket定义规约结构,foo是自定义的规约名称,后面是各个字段的定义。

上面格式要求报文以1字节的0x68开头,后面跟着2字节的无符号整数(默认为高字节在前,可以改变),接着是1字节的无符号整数,最后以0x16结尾。

定义好规约格式后,就可以用parse来解析字节流:

(parse foo [1 2 0x68 0 10 11 0x16 3 4])
;=> {:type :group
     :start 2
	 :value [{:type :group, :value [], :pos 2, :len 1}
	         {:value 10, :pos 3, :len 2, :name :a}
	         {:value 11, :pos 5, :len 1, :name :b}
			 {:type :group, :value [], :pos 6, :len 1}]
     :pos 2
     :len 5}

parse按foo规定的报文结构解析一个字节序列,得到解析结果,解析结果中:

  • :type 结构类型,:group表示由多个子字段组成的结构类型,:repeat表示重复字段,空表示简单字段。
  • :start 有效报文的起始位置。
  • :value 解析值,对简单类型为普通值;对结构类型,为一个向量,其元素对应各子字段的解析结果;对重复字段,也为一向量,其元素是被重复字段的解析结果。
  • :pos 对应整个结构的起始位置。
  • :len 整个结构占用的字节数。

解析过程如果出现异常情况,如提供的报文不满足规约要求,则结果中包含:error键,其值是一个序列,包含从外层结构到内层简单字段的错误信息。

要得到业务对象,可使用get-parser-value函数:

(get-parser-value *1)
;=> {:a 10 :b 11}

如果已知各字段的值,要构建对应的报文,可用build函数:

(build foo {:a 10, :b 11})
;=> [0x68 0 10 11 0x16]

返回映射中的:value对应的是构建的报文内容。如果构建过程中发生错误,则返回的映射中包含:error键,其值是一个序列,包含从外层结构到内层简单字段的错误信息。

下面介绍详细用法

定义规约结构

用defpacket宏定义规约的结构,语法为

(defpacket name flags & fields)

其中name是规约名,fields是规约报文各个组成部分(以下称为字段)的描述,按先后顺序依次排列。

有三种方式描述一个字段:

  • 连续出现的数字和符号 数字对应报文中的固定值,符号对应报文中的一个字节,相同的符号对应相同的字节,除非这个符号是**?**,它可以对应任意字节。
  • (type len & args as name) 字段名称和类型信息,其中
    • type 字段类型。放置字段类型的这个位置称为类型位置。
    • len 本字段占用的字节数。如果当前类型的字节数是固定的,如ipaddr类型,则不需该信息。
    • args 某些类型需要的其他参数,如d类型需要的系数等。
    • name 表示字段名称的符号,由as引入。字段名不能包含.和$字符。字段名称不是必须的。
    • 其他修饰符,在讲到具体应用时再描述。
  • 结构字段,在讲到具体应用时再描述。

后面描述的结构字段采用了一些与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) 用一个字节表示的一个字符。
  • (zstr) 以0字节结束的字符串类型。
  • (lstr) 第一个字节表示字符串的长度,后跟字符串内容。
  • (dstr n) 总共占用n个字节的以0字节结束的字符串形式的正负浮点数。
  • (ipaddr) 用4个字节存储的ip地址,业务类型为java.net.InetAddress。
  • (datetime format) 日期时间类型,在后面专门描述。

如果nil是有效的业务值,则不能使用u、i两个类型,只能使用内部用bcd码表示的几个数值类型。这些类型用全0xff字节表示nil。

在packet.core空间下定义了一个raw类型,用法为(raw n),表示n个原始字节,这是最基本的类型,上面那些类型都是由它派生的。

基本类型须带名字空间使用,除非在当前名字空间中userefer它们

修饰符与修饰语

修饰语附加在字段定义中,为字段提供额外功能。修饰符就是引导修饰语的特殊符号。修饰符与它的参数一起成为修饰语。不要用下面介绍的各类修饰符作为字段的名称。一般用法为

(u 2 as a)

其中as就是一个修饰符,a是其参数,as a称为修饰语,这个修饰语是为当前字段命名。前面的基本部分(u 2)称为宿主字段。下面分别解释各类修饰符。

as

为宿主字段指定名称,有名称的字段可以反映到解析结果中,也可以被后续字段引用。

(u 2 as a)

将宿主字段命名为a。as修饰语可出现在任意位置,但通常作为第一个修饰语出现。后面介绍修饰符时不再写出宿主字段。

with

指定与业务值间的转换,可有几种形式:

  • with [:a :b :c] 表示报文中的数字值依次对应向量中提供的业务值。
  • with {17 :tcp 11 :udp} 表示报文中的数字值按照给定映射对应于业务值。提供的映射的键的类型必须与宿主字段的类型相符,但通常应该是整数。
  • with f1 ... 将报文中的值d用(f1 d ...)转换为业务值,业务值向报文值的转换是通过自动分析f1及其参数得到的逆函数来进行的。业务值可以是任何类型,不限于数值类型。
  • with f1 ... and f2 ... 如果f1不连续、不严格单调,则可能无法自动分析得到其逆函数,此时需要提供f2将业务值v用(f2 v ...)转换为数字值。

在通过向量或映射进行转换的情况下,如果没有为某数字值指定业务值,解析时会直接输出原始数字值。构建时只需为该字段提供业务值,如果提供的业务值没有对应的键值,也会直接使用提供的值构建报文,这可能会带来意想不到的结果,要谨慎对待。

如果通过向量或映射给定的业务值有重复,则在构建时无法保证选择哪个数字值,因此应避免这种情况。

向量或映射中不能有变量引用,否则无法通过编译。

default

为宿主字段指定默认值,

default 3

表示宿主字段的默认值为3。构建时如果没有指定该字段的值,则使用默认值。

可以用包含变量引用的表达式来指定默认值,这样默认值就是与其他字段的值有关的动态值。

该修饰语通常放在with修饰语之后,这样指定默认值时可使用业务值。

should

对字段值进行检验,用法如下:

should f ...

对字段值v应用(f v ...),如果结果为任何真值,则有效,否则判定该字段的值无效。如果该修饰语在with修饰语后,v是业务值,否则是原始值。通常将它防止with修饰语后,以便针对业务值进线检验。

如果函数f是=或zero?,则构建时可以不提供该字段的值,本系统会自动计算满足要求的值。

count-of

表示宿主字段的值是另外一个重复字段的重复次数。如

count-of a

表示a字段的重复次数,这里a字段必须是一个重复字段。如repeat和while字段。

length-of

表示宿主字段的值是另外一个字段的字节数,用法举例:

length-of a +4 ;表示宿主字段的值是a字段的长度加4

构建时该字段的值可以省略。

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指定了结束位置,则采用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指定了结束位置,则采用until指定的结束位置。

在packet.checksum空间下定义了一些常用的校验码计算函数,有cs16、lrc、crc8、crc16、crc32、crc16-modbus。 如果不指定校验函数,则使用cs16。

构建时该字段的值可以省略。

修饰语总结

带修饰语的字段的一般格式为

(u 2 ... as field-name prompt-string
      with f1 ... and f2 ...
      default ...
      should predicate ...
      count-of ...
      length ...
      length-of ...
      checksum ...)

除as修饰语外,其他修饰语均只用于基本类型。

根据逻辑语义,count-of、length、length-of、checksum最多只能出现其中一个。当它们出现的时候,with和default也是没有意义的,不应该出现。

除开以上修饰语,宿主字段中的紧挨各修饰语前的第一个字符串用来指定该字段的错误提示信息。如果宿主字段包含字符串类型的参数,为了避免该参数被误认为是错误提示信息,应该总是在as修饰语前或后设置错误提示信息。

除以上基本类型外,还有一些特殊类型和组合类型以及位类型,它们没有名字空间,可直接使用。

特殊类型

除上面的基本类型外,本库还定义了一些特殊类型,下面用举例来说明它们的用法。

raw

这是唯一定义在packet.core空间中的字段,用法举例:

  • (raw 4) 该类型将返回4个字节的原始内容
  • (raw as a) 由前面某个具有length-of a修饰语的字段的值决定字节数

第一种用法通常用于自定义字段类型,第二种用法通常用于嵌入规约。 对嵌入规约,通常用法如下: 底层规约中通常有一个字段指定嵌入规约的名称,如(u 1 as protocol with [:udp :tcp]),另一字段指定嵌入规约的长度,如(u 2 length-of a)。 解析时,先提取原始字节序列(长度由前面那个具有length-of a修饰语的字段的值决定),再根据protocol字段指定的规约类型进一步解析这些原始字节序列。 构建时,先构建嵌入规约的字节序列,再将该序列作为raw字段的值并相应设置protocol字段的值,以此来构建底层规约。

pattern

匹配一个模式,用法举例:

(pattern 0x68 0x10 a b ? ? a b 0x68)

数字(值必须在字节范围内)必须准确匹配,符号可以匹配任意字节,相同的符号匹配相同的字节,?可随意匹配任意字节。 符号匹配的字节可以在后面的字段中引用。

跳过一个或多个无关字节,用法举例:

(skip 3)

上例表示跳过3个字节。如果不提供参数,则跳过1个字节。构建时用0填充。

reserved

保留字段,用法举例:

(reserved 0x55 0x66)

上例表示解析时跳过两个字节,构建时用0x55和0x66填充这个字段。

padding

对齐字段,用法:

(padding n v)

使得下一个字段位于n字节的整数倍边界上,v是构建时用于填充的字节,如果不提供则用0填充

举例如下:

(defpacket foo
  (u 5)
  (padding 4) ;填充合适数量的(这里是3个)字节使得后面的a字段对齐4字节边界。
  (u 2 as a))

label

(label a)

定义一个没有任何内容的字段,主要目的就是定义一个名称共其他字段使用,如给checksum修饰语引用用于指定校验范围。

success

不消耗内容,总是成功的类型,通常只用在or、case、cond字段的最后,用于保证这些字段不会失败。用例如下:

(success)

fail

不消耗内容,总是失败的类型。这个类型极少使用,用例如下:

(fail "some reason")

组合类型

组合类型表示其他字段有条件地存在、或重复、或选择等。

下面出现的expr均表示一个普通clojure表达式,里面函数位置的符号与这里定义的类型无关,就是通常的clojure函数、宏、或特殊形式。 表达式里面可以使用与字段名相同的符号,但须前缀$,代表对应字段的值。当然这些字段必须在当前字段之前定义。 如果要引用的字段处于不同的层次,则用.分开各个层次。也可以引用def定义的全局变量。

[& fields]

将多个字段组合起来作为一个整体,视为一个字段。某些结构由固定数量(而不是任意数量)的字段组成,如if结构只能提供1到2个字段供条件满足和不满足时分别应用。case结构和cond结构中与值或条件对应的也只能是一个字段。这时可将多个字段放入[]中,将这些字段视为一个整体,作为一个组合字段。如

(if (> $a 100)
    [(u 1) (i 2)]
    (bcd 3))

(case $a
  1 [(u 1) (i 2)]
  2 (u 1)
  ...)

(group & fields)

除了用[]来组合字段外,也可显式地用group来组合字段。与前者相比,后者的优势在于可以为这个组起个名称,如

(group as a (u 1) (i 2))

你通常不必使用group,使用[]就好。

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字段的值。举例如下:

(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的计算结果为真值时,应用后续字段,否则忽略本字段。举例如下:

(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)

如果条件满足,在应用完fields后直接跳出当前字段组,忽略当前字段组内本字段后面的同级字段。

(or & fields)

依次应用各个字段,直到某一个成功,余下的字段不应用。如果均不成功,则总体失败。

(option & fields)

可选字段,即如果fields中任一字段无法正常解析就忽略本字段。

(option
  (u 1 should = 1)
  (bcd 1 as a))

在构建时,如果没有提供某个子字段的值且该值无法自动推断,则不构建整个option字段。

(repeat n as a & fields)

n是一个整数,表示重复次数。如果前面某个字段有count-of a修饰语,则可以省略n,由那个字段指定重复次数,这是通常的应用模式,因为一般来说事先并不知道重复次数。

该类型的字段必须有名称,因为解析结果是一个数组(即使只有一个值甚至没有值),必须有地方安放这个数组。

(repeat 5 as a
  ...)

(u 1 count-of b)
...
(repeat as b
  ...)

表示字段a重复5次,字段b重复的次数由前面那个具有count-of修饰语的字段的值决定。

表示重复字段b的重复次数由前面那个带有count-of修饰语的字段的值决定,那个字段不需要有名称,构建时也不需要给出那个字段的值。

(while expr as a & fields)

在expr计算结果为真时应用fields,如此重复,直到计算结果为假。

该类型的字段必须有名称,因为解析结果是一个数组(即使只有一个值甚至没有值),必须有地方安放这个数组。

while字段的expr通常会引用预定义的变量$=,这个特殊变量表示报文的当前位置,其值在每次重复后都会改变,因此每次计算expr才可能得到不同的结果,最终终止重复。

有时候报文中没有一个字段明确表示重复次数,但有一个与报文长度有关的字段, 并且这个字段计及的报文范围涵盖了重复字段。假设该字段名为len,并且假设涵盖范围之前的字段长度为outer-before, 涵盖范围之内处于重复字段之后的字段的长度为inner-after,则重复条件为:

(< (+ $= inner-after) (+ outer-before len))

其中inner-after和outer-before都可能是一个表达式,举例如下:


(defpacket aname
  (i 4 as p)
  (u 2 as len length-of data)
  (when (>= $p 0) as data
    ...
    (repeat as a (< (+ $= (if (zero? $p) 2 0)) ;if表达式是b字段的长度
                    (+ 6 $len))
       ...)
    (when (zero? $p) (u 2))))
  ...

(case expr v1 field1 v2 field2 ...)

首先计算expr,应用与该值对应的field,如果没有对应的值,并且最后有兜底的field,则应用该field,否则失败。如果要避免失败,可最后放一个(succeed)字段来兜底。

如果多个值对应同样的field,可以用这样的形式:

(case expr 
  (1 5 - 10 13) field1 
  18 field2 
  ...)

上面表示expr的值为1、或5到10、或13时应用field1。

如果某个值需要对应多个字段,可以把这些字段放在向量中。

如果某个值对应的字段是其他多个值对应的字段的叠加,则可用这样的形式:

(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)来兜底。

标志类型

有的字段是由一个或多个位段组成的,这时可用标志类型,如下所示:

(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)

来规定需要加扰的范围。对这样的报文,解析时会自动去扰。

注意,加扰范围不可嵌套和重叠,但可以有多个分离的区域

层次关系

一个字段可以包含多个子字段,如

(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字段的值以

{:a 1
 :b {:b1 ...
     :b2 ...}}

的形式存在。如果不需要这个多余的层次,则在定义报文结构时可省略b,如下所示:

(defpacket foo
  (u 1 as a)
  (when (= $a 1)
     (u 1 as b1)
     (u 2 as b2)))

此时解析结果为

{:a 1
 :b1 ...
 :b2 ...}

自定义类型

对于需要重复使用的字段组,用packet.types空间下的deftype定义,如下所示

(deftype name doc-string [& args] & fields)

doc-string为文档字符串,可以省略。如果不需要参数,也可以省略[& args],连空的[]都可以不要。

fields的格式同defpacket。定义之后,name可作为类型名使用

解析报文

调用形式为

(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。

大端小端(字节序)

多字节的整数按照网络传输的惯例默认高字节在前,如果要改为低字节在前,在定义规约时要加上:little-endian标志,如

(defpacket name :little-endian
  ...
  )

构建报文

调用形式为

(build name domain env)

其中

  • name 前面用defpacket定义的规约名,
  • domain 业务对象(一个映射),键是字段名对应的关键字。注意提供的值是业务值而不是报文中的数值。业务值可能是一个复杂的对象,由字段类型决定。
  • env 环境变量,就是一些预设的字段值或其他信息。如果提供报文缓冲区,则它放在:dest键下。

如果构建过程没有错误,则返回包含报文内容的字节向量,否则,返回一个映射,包含错误信息。

提供的业务对象中可以不提供某些字段的值,这些字段有以下几种情况:

  • 用default修饰语指定了默认值的字段以及指定了默认值的位字段
  • 表示其他字段的重复次数,即有count-of修饰语的字段
  • 表示其他字段的字节数,即有length或length-of修饰语的字段
  • 检查和字段,即有checksum修饰语的字段,这种字段将根据报文内容自动计算
  • 有should修饰语并使用=函数进行检验的字段,将自动提供满足检验要求的值

除了这些类型的字段以及固定内容的字段外,其他字段都应该有名称,否则,解析时无法返回其值,构建时无法指定其值。

如果提供的dest是暂态向量,必须从:value中读取构建的报文,不能从原始的暂态向量中读取。

自定义类型

如果要定义自己的类型,可用packet.types/deftype定义,如下所示:

(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

也可以从原始类型来定义自己的类型,如

;;下面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是电压的意思):

(deftype voltage (d 2 0.1))

有了上面的定义,则可以如下使用

(voltage)

时间类型

如果用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.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)

其他工具

为自定义类型可以使用以下函数在已有类型的基础上定义新的类型。

  • mapping
  • map-input
  • map-value
  • flatmap

示例

以解析ip报文为例

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

Can you improve this documentation?Edit at git repository

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close