一个TCP长连接设备管理后台工程(四)---jtt808协议解析

2023-10-31

协议解析

从前面内容我们可以发现,808协议是一个很典型的协议格式:

固定字段+变长字段

其中固定字段用来检测一个帧格式的完整性和有效性,所以一般会包含一下内容:帧头+变长字段对应的长度+校验。由于这一段的数据格式固定,目的单一,所以处理起来比较简单。

变长字段的长度是由固定字段终端某一个子字段的值决定的,而且这部分的格式比较多变,需要灵活处理。这一字段我们通常称为Body或者Apdu。

我们首先说明变长字段的处理流程。

Body处理

正因为Body字段格式灵活,所以为了提高代码的复用性和拓展性,我们需要对Body的处理机制进行抽象,提取出一个相对通用的接口出来。

有经验的工程师都知道,一个协议格式处理,无非就是编码和解码。编码我们称之为Marshal,解码我们称之为Unmarshal。对于不同的格式,我们只需要提供不同的Marshal和Unmarshal实现即可。

从前面分析可以知道,我们现在面对的一种格式是类似于Plain的格式,这种格式没有基本的分割符,下面我们就对这种编码来实现Marshal和Unmarshal。我们将这部分逻辑定义为一个codec包

package codec

func Unmarshal(data []byte, v interface{
   }) (int, error){
   }
func Marshal(v interface{
   }) ([]byte, error){
   }

参考官方库解析json的流程,很快我们就想到了用反射来实现这两个功能。

首先我们来分析Unmarshal,我们需要按照v的类型,将data数据按照对应的长度和类型赋值。举个最简单的例子:

func TestSimple(t *testing.T) {
   
	type Body struct {
   
		Age1 int8
		Age2 int16
	}

	data := []byte{
   0x01, 0x02, 0x03}
	pack := Body{
   }
	i, err := Unmarshal(data, &pack)
	if err != nil {
   
		t.Errorf("err:%s", err.Error())
	}

	t.Log("len:", i)
	t.Log("pack:", pack)
}
$ go test -v server/codec -run TestSimple
=== RUN   TestSimple
--- PASS: TestSimple (0.00s)
    codec_test.go:20: len: 3
    codec_test.go:21: pack: {
   1 515}
PASS
ok      server/codec    0.002s

对于Body结构体,第一个字段是int8,占用一个字节,所以分配的值是0x01。第二个字段是int16,占用两个字节,分配的值是0x02,0x03,然后把这两个字节按照大端格式组合成一个int16就行了。所以结果就是Age1字段为1(0x01),Age2字段为515(0x0203)

所以处理的关键是,我们要识别出v interface{}的类型,然后计算该类型对应的大小,再将data中对应大小的数据段组合成对应类型值复制给v中的对应字段。

v interface{}的类型多变,可能会涉及到结构体嵌套等,所以会存在递归处理,当然第一步我们需要获取到v的类型:

rv := reflect.ValueOf(v)
switch rv.Kind() {
   
    case reflect.Int8:
		//
	case reflect.Uint8:
		//
	case reflect.Int16:
		//
	case reflect.Uint16:
		//
	case reflect.Int32:
		//
	case reflect.Uint32:
		//
	case reflect.Int64:
		//
	case reflect.Uint64:
		//
	case reflect.Float32:
    	//
    case reflect.Float64:
		//
	case reflect.String:
		//
	case reflect.Slice:
		//
	case reflect.Struct:
    	//需要对struct中的每个元素进行解析
}

其他的类型都比较好处理,需要说明的是struct类型,首先我们要能够遍历struct中的各个元素,于是我们找到了:

fieldCount := v.NumField()
v.Field(i)

NumField()能够获取结构体内部元素个数,然后Field(i)通过指定index就可以获取到指定的元素了。获取到了元素后,我们就需要最这个元素进行再次的Unmarshal,也就是递归。但是此时我们通过v.Field(i)获取到的是reflect.Value类型,而不是interface{}类型了,所以递归的入参我们使用reflect.Value。另外还需要考虑的一个问题是data数据的索引问题,一次调用Unmarshal就会消耗掉一定字节的data数据,消耗的长度应该能够被获取到,以方便下一次调用Unmarshal时,能够对入参的data数据索引做正确的设定。因此,Unmarshal函数需要返回一个当前当用后所占用的字节长度。比如int8就是一个字节,struct就是各个字段字节之和。

func Unmarshal(data []byte, v interface{
   })  (int,error) {
   
	rv := reflect.ValueOf(v)
	if rv.Kind() != reflect.Ptr || rv.IsNil() {
   
		return 0,fmt.Errorf("error")
	}

	return refUnmarshal(data, reflect.ValueOf(v))
}

func refUnmarshal(data []byte, v reflect.Value)  (int,error) {
   
	var usedLen int = 0
	if v.Kind() == reflect.Ptr {
   
		v = v.Elem()
	}
	switch v.Kind() {
   
	case reflect.Int8:
		usedLen = usedLen + 1
	case reflect.Uint8:
		usedLen = usedLen + 1
	case reflect.Int16:
		if len(data) < 2 {
   
			return 0, fmt.Errorf("data to short")
		}
		usedLen = usedLen + 2
	case reflect.Uint16:
		if len(data) < 2 {
   
			return 0, fmt.Errorf("data to short")
		}
		usedLen = usedLen + 2
	case reflect.Int32:
		if len(data) < 4 {
   
			return 0, fmt.Errorf("data to short")
		}
		usedLen = usedLen + 4
	case reflect.Uint32:
		if len(data) < 4 {
   
			return 0, fmt.Errorf("data to short")
		}
		usedLen = usedLen + 4
	case reflect.Int64:
		usedLen = usedLen + 8
	case reflect.Uint64:
		usedLen = usedLen + 8
	case reflect.Float32:
		usedLen = usedLen + 4
	case reflect.Float64:
		usedLen = usedLen + 8
	case reflect.String:
		//待处理
	case reflect.Slice:
		//待处理
	case reflect.Struct:
		fieldCount := v.NumField()

		for i := 0; i < fieldCount; i++ {
   
			l, err := refUnmarshal(data[usedLen:], v.Field(i), v.Type().Field(i), streLen)
			if err != nil {
   
				return 0, err
			}

			usedLen = usedLen + l
		}
	}
	return usedLen, nil
}

解析到这个地方我们发现,我们又遇到了另外的一个问题:我们没有办法单纯的通过类型来获取到string和struct的长度,而且我们还必须处理这两个类型,因为这两个类型在协议处理中是很常见的。既然单纯的通过类型无法判断长度,我们就要借助tag了。我们尝试着在string和slice上设定tag来解决这个问题。但是tag是属于结构体的,只有结构体内部元素才能拥有tag,而且我们不能通过元素本身获取tag,必须通过上层的struct的type才能获取到,所以此时我们入参还要加入一个通过结构体type获取到的对应字段reflect.StructField:

func refUnmarshal(data []byte, v reflect.Value, tag reflect.StructField) (int, error) {
   
	var usedLen int = 0
	if v.Kind() == reflect.Ptr {
   
		v = v.Elem()
	}
	switch v.Kind() {
   
	case reflect.Int8:
		usedLen = usedLen + 1
	case reflect.Uint8:
		usedLen = usedLen + 1
	case reflect.Int16:
		usedLen = usedLen + 2
	case reflect.Uint16:
		usedLen = usedLen + 2
	case reflect.Int32:
		usedLen = usedLen + 4
	case reflect.Uint32:
		usedLen = usedLen + 4
	case reflect.Int64:
		usedLen = usedLen + 8
	case reflect.Uint64:
		usedLen = usedLen + 8
	case reflect.Float32:
		usedLen = usedLen + 4
	case reflect.Float64:
		usedLen = usedLen + 8
	case reflect.String:
		strLen := tag.Tag.Get("len")
		var lens int = 0
		if strLen == "" {
   
			//
		} else {
   
			lens64, err := strconv.ParseInt(strLen, 10, 0)
			if err != nil {
   
				return 0, err
			}

			lens = int(lens64)
		}
		usedLen = usedLen + int(lens)
	case reflect.Slice:
		strLen := tag.Tag.Get("len")
		var lens int = 0
		if strLen == "" {
   
			//
		} 
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

一个TCP长连接设备管理后台工程(四)---jtt808协议解析 的相关文章

随机推荐

  • ISP图像处理流程

    文章目录 前言 ISP图像处理流程 总结 参考 前言 因工作需要 今天看了ISP图像处理的基本流程 为了检验自己的理解情况 这里根据自己的理解写下这篇文章 如有错误 敬请原谅 ISP图像处理流程 ISP Image Sensor Proce
  • 后台管理系统项目

    1 项目名称 后台管理 2 技术栈 vue全家桶 element ui axios less eachers 3 项目亮点 性能优化 百万级项目 新旧系统更迭 权限把控 项目开发流程 1 安装vue脚手架 2 vue create 项目名称
  • MySQL-索引

    一 介绍 索引是数据库对象之一 用于提高字段检索效率 使用者只需要对哪个表中哪些字段建立索引即可 其余什么都不做 数据库会自行处理 索引提供指向存储在表的指定列中的数据值的指针 如同图书的目录 能够加快表的查询速度 但同时也增加了插入 更新
  • SpringBoot 二维码生成

    来源 https www cnblogs com songweipeng p 16623793 html 一 基于Google开发工具包ZXing生成二维码
  • 算法与数据结构之带头结点的单链表

    单链表优缺点 链表是非随机存取的存储结构 和顺序表相比 链表存储结构在实现插入 删除的操作时 不需要移动大量数据元素 但不容易实现随机存取线性表的第 i 个数据元素的操作 所以 链表适用于经常需要进行插入和删除操作的线性表 如飞机航班的乘客
  • 【机器学习 - 5】:多元线性回归

    文章目录 多元线性回归 多元线性回归公式推导 举例 波士顿房价 取特征值RM为例 取所有特证为例 多元线性回归 多元线性回归方程 特征值为两个或两个以上 以下是多元线性回归的模型 我们需要求出theta 使得真实值和预测值的差值最小 多元线
  • 前端系列19集-vue3引入高德地图,响应式,自适应

    npm i amap amap jsapi loader save import AMapLoader from amap amap jsapi loader 使用加载器加载JSAPI 可以避免异步加载 重复加载等常见错误加载错误 为地图注
  • 四种推荐系统原理介绍(基于内容过滤/协同过滤/关联规则/序列模式)

    在推荐系统中常用的技术可大致分为四类 基于内容的过滤 协同过滤 基于规则的方法和混合方法 一 基于内容过滤 基于内容过滤推荐系统思路如下 1 通过在抓取每个商品的一系列特征来构建商品档案 2 通过用户购买的商品特征来构建基于内容的用户档案
  • Jmeter怎么添加token?

    不需要token的场景 随便进入一个购物app 都是可以看里面的商品的 这时不需要token 但是当你要买它的时候就需要你登录自己的账号 登录状态需要token 什么时候需要用到token 比如 京东商城里面 我需要查看我的消息列表 这时候
  • Jenkins自动化构建网站与流水线构建Maven项目实战

    1 gitlab Jenkins自动化构建网站实战 基本架构 通过gitlab jenkins构建一个常规网站的原理图 Jenkins插件和环境配置 配置全局变量 要配置的全局变量有Git JDK和Maven 指定JDK的路径 设置Git可
  • vue2.0 vue3.0 input组件封装

    vue2
  • tar -xf node-v12.16.1-linux-armv7l.tar.xz 错误

    1 tar解压tar xz能会出错 因为你的系统里没有对应的解压工具 你安装上xz和tar对应工具包就行 apt get install xz utils 2 chmod无法访问 没有那个文件或目录 方案之一 修改 etc selinux
  • UEFI模式安装下Ubuntu 18.04 系统分区参考(win10+Ubuntu双系统)

    由于这几天给自己的电脑成功装了双系统 所以在此记录下当时在装Ubuntu系统中最难懂的部分 系统分区 以供大家以及自己今后参考 具体安装步骤我就不一一细说了 可以参考其他博主的文章 小米笔记本安装Win10 Ubuntu16 04 LTS
  • 金山卫士开源软件之旅(二) 简单教程:如何创建一个基于金山卫士界面库的工程

    完整解决方案代码压缩包 test full zip 267 59 KB 参考 http bbs code ijinshan com thread 1391 1 1 html 为了让更多的朋友能够让自己的程序使用上金山卫士的界面库 现将创建界
  • oracle case when的使用方法

    大家都知道Case when的用法 一旦满足了某一个WHEN 则这一条数据就会退出CASE WHEN 而不再考虑其他CASE 文章来详细的介绍了case when的用法并举例说明了 Case when 的用法 简单Case函数 简单CASE
  • C++ 接口(抽象类)

    C 接口是使用抽象类来实现的 接口描述了类的行为和功能 而不需要完成类的特定实现 且抽象类与数据抽象互不混淆 如果类中至少有一个函数被声明为纯虚函数 则这个类就是抽象类 数据抽象则是一个把实现细节与相关的接口分离开的概念 如果类中至少有一个
  • Ubuntu18.04安装ROS教程bug解决办法

    Ubuntu18 04安装ROS教程bug解决办法 写在前面 一 配置源文件bug 二 rosdep update 报错 三 安装ROS中出现bash opt ros melodic setup bash 没有那个文件或目录或者bash o
  • linux设置时间为24小时制,设置时区

    1 查看系统时间 root localhost localdomain date Thu Feb 4 14 24 18 CST 2010 时区是CST 为了彻底弄明白GMT UTC CST 我查阅了下网上的相关教程 进行整理 一般来说 UT
  • Android 应用内部存储之应用文件缓存

    前言 Android 应用内部存储之应用文件缓存的重点在最后总结 如果想快速学习 直接查看最后总结 在向手机上保存数据 一般是把数据保存在sdcard中的 大部分应用是直接在sdcard的根目录下创建一个文件夹 然后把数据保存在该文件夹中
  • 一个TCP长连接设备管理后台工程(四)---jtt808协议解析

    协议解析 从前面内容我们可以发现 808协议是一个很典型的协议格式 固定字段 变长字段 其中固定字段用来检测一个帧格式的完整性和有效性 所以一般会包含一下内容 帧头 变长字段对应的长度 校验 由于这一段的数据格式固定 目的单一 所以处理起来