tble:lipdp格式

在收发数据时,像蓝牙,数据往往封装成某种格式的消息。lipdp是用于封装消息的一种格式。这格式名称来自“Leagor IP Discovery Protocol(LIPDP)”。

图1 lipdp格式的消息示例

图1是数个lipdp格式的消息示例。

  • header。头部有4个字节:一个固定值的前导字节、消息码(cmd)、payload字节数。
  • payload。负载可以任意长度,包括0。header中两个字节指示了payload块的字节数。负载由若干个单元组成。

payload单元的第一个字节指示单元类型,后面字节是什么内容依赖单元类型,目前支持7种类型。

类型名称字节数 描述
lipdp_tn8(0)1字节整数1 int8_t
lipdp_tn32(1)4字节整数4int32_t,小端序
lipdp_tn64(2)8字节整数8int64_t,小端序
lipdp_tstring(3)不含'\0'的字符串可变串中不能含有'\0',末尾会加'\0'
lipdp_thexstring(4)hex二进制数据可变 以十六进制字符串表示的二进制数据(注1)
lipdp_tbinary(5)二进制数据可变先二字节表示长度,然后二进制数据
lipdp_tip(6)IP地址 1/5/17先1字节类型,然后数值。没有可用网络/ipv4/ipv6

注1:真到发送时不会出现lipdp_thexstring,存在它只是方便编写代码。举个例子,要表示“4b010100cf”这5字节的二进制数据,c语言可用一个uint8_t数组,一些脚本语言就不那么方便了,这时可让允许书写为字符串“4b010100cf”,然后送给lipdp解释器。解释器发现它是lipdp_thexstring,最终会把它转换为lipdp_tbinary。为什么不是lipdp_tstring?——lipdp_tstring判断长度的方法是遇到“\0”,即内容不能出现“\0”,而lipdp_thexstring成为二进制后,是有可能出现“\0”的。

local len, wrote = aplt.vdata_:write_lipdp(1, {
	{rose.lipdp_tn8, 1},
	{rose.lipdp_tn32, 0xfe123456},
	{rose.lipdp_tstring, "5a01010001"},
	{rose.lipdp_tstring, ""},
	{rose.lipdp_thexstring, "4b010100cf"},
	{rose.lipdp_thexstring, ""},
	{rose.lipdp_tip, rose.LEAGOR_BLE_AF_INET, 0x7301a8c0},
});

-- 4: LEAGOR_BLE_MTU_HEADER_SIZE
local lipdp = aplt.vdata_:read_lipdp(len - wrote + 4);

以上lua脚本用于生成一个lipdp消息,cmd是1,负载含有7个单元。以下是这个消息在内存中的字节流。

图2 一个lipdp消息的字节流实例
  • 5a 01 26 00。header。cmd是1,负载长度38字节。
  • 00 01。第一单元。类型tn8,数值1。
  • 01 56 34 12 fe。第二单元。类型tn32,数值0xfe123456。
  • 03 35 61 30 31 30 31 30 30 30 31 00。第三单元。类型tstring,字符串“5a01010001”。
  • 03 00。第四单元。类型tstring,空字符串。
  • 05 05 00 4b 01 01 01 00 cf。第五单元。代码写的是thexstring,翻译成tbinary,二进制数值{0x4b, 0x01, 0x01, 0x00, 0xcf}。“05 00”指示了该单元中字节数。
  • 05 00 00。第六单元。代码写的是thexstring,翻译成tbinary,0字节数据。
  • 06 02 c0 a8 01 73。第七单元。类型tip,02表示该IP类型是ipv4(LEAGOR_BLE_AF_INET),后面4字节是ipv4地址。

vdata提供了read_lipdp方法,功能是把lipdp格式的一块内存转换为一个lua变量。

图3 lua中的lipdp变量

图3是上面代码中,紧接调用read_lipdp(len - wrote + 4)生成的lipdp变量。lipdp自身是一个数组表,然后每个单元是一个数组表。

 

一、tlipdp_packer、tlipdp_parser

tlipdp_packer、tlipdp_parser是rose为处理lipdp格式,提供的两个c++类。

tlipdp_packer用于将cmd、若干单元打包成消息。tlipdp_parser则是逆方向,从消息解析出cmd、若干单元。

不论打包还是解析,都用了中间格式:tlipdp_item。使用tlipdp_item一分为三步。

  1. 定义items、tlipdp_items_lock、index、payload_len。
  2. 使用LIPDP_PUSH_ITEM_xxx向负载增加单元。这些单元会放在items数组。
  3. 调用packer.items_2_data,由items数组生成负载、加上cmd,从而生成一个表示该消息内存块。返回值是该消息字节数。
void pack_sample(tuint8data& result)
{
	tlipdp_packer packer;
	tlipdp_item* items = nullptr;
	int item_count = 7;
	tlipdp_items_lock lock(item_count, &items);
	int index = 0;
	int payload_len = 0;

	LIPDP_PUSH_ITEM_n8(1);
	LIPDP_PUSH_ITEM_n32(0xfe123456);
	LIPDP_PUSH_ITEM_string("5a01010001");
	LIPDP_PUSH_ITEM_string("");
	LIPDP_PUSH_ITEM_hexstring("4b010100cf");
	LIPDP_PUSH_ITEM_hexstring("");
	LIPDP_PUSH_ITEM_ipv4(0x7301a8c0);

	VALIDATE(index == item_count, null_str);
	int packet_len = packer.items_2_data(msg_queryip_resp, payload_len, items, item_count);

	result.ptr = packer.packet_data_;
	result.len = packet_len;
}

这个用C写的函数能打包出一条和上面lua代码一样的消息。在内部,vdata::write_lipdp(...)就是靠着这样的C代码实现打包。

LIPDP_PUSH_ITEM_xxx有着差不多逻辑,以下是LIPDP_PUSH_ITEM_string。

#define LIPDP_PUSH_ITEM_string(_data, _len)	\
	items[index].type = lipdp_tstring;	\
	items[index].data = (const uint8_t*)(_data);	\
	items[index].int32 = _len;	\
	payload_len += 1 + items[index].int32 + 1;	\
	index ++;

把单元内容存储到items[index]后,它们会连带着修改index、payload_len。后绪items_2_data须要提供payload_len参数,4+payload_len就是该消息字节数。

tlipdp_item不会复制字符串、二进制数据,所以必须在字符串、二进制数据还有效前,调用items_2_data。

 

二、tlipdp_receiver::enqueue

tipdp_receiver缓存、消费数据都是在一个叫enqueue的函数内进行。

void tlipdp_receiver::enqueue(const uint8_t* data, int len);
std::function<void (int cmd, const uint8_t* data, int len)> did_read_;
  1. app收到数据了,调用enqueue(data, len)。
  2. 在enqueue(data, len),根据规则不断提取消息。提取到一个消息后,调用app提供的did_read_。
  3. did_read_是app提供的,执行着如何消费消息。有三个参数,cmd是藏在头中的命令,data是payload开始地址,len是payload字节数。
图4 enqueue流程图

enqueue一个任务是提取消息,要做到不出错,又快,提取用了两重判断。1)消息必须以LEAGOR_BLE_PREFIX_BYTE开始。2)命令码必须是允许的命令码。——或许吧,为更准确,可能还得增加在注册允许的命令码时,增加指明该命令的payload字节数。

void tblebuf::enqueue(const uint8_t* data, int len)
{
	VALIDATE(data != nullptr && len > 0, null_str);
	const int min_size = posix_align_ceil(recv_data_vsize_ + len, 4096);

	if (min_size > recv_data_size_) {
		uint8_t* tmp = (uint8_t*)malloc(min_size);
		if (recv_data_) {
			if (recv_data_vsize_) {
				memcpy(tmp, recv_data_, recv_data_vsize_);
			}
			free(recv_data_);
		}
		recv_data_ = tmp;
		recv_data_size_ = min_size;
	}

(recv_data_, recv_data_size_)表示接收缓存。根据收到的数据长度自动扩大缓存。

	memcpy(recv_data_ + recv_data_vsize_, data, len);
	recv_data_vsize_ += len;

	while (true) {
		while (true) {
			// 1. must prefix with LEAGOR_BLE_PREFIX_BYTE
			int skip = 0;
			for (int i = 0; i < recv_data_vsize_; i ++) {
				if (recv_data_[i] == LEAGOR_BLE_PREFIX_BYTE) {
					break;
				}
				skip ++;
			}
			if (skip && skip != recv_data_vsize_) {
				memcpy(recv_data_, recv_data_ + skip, recv_data_vsize_ - skip);
			}
			recv_data_vsize_ -= skip;
			if (recv_data_vsize_ < LEAGOR_BLE_MTU_HEADER_SIZE) { // payload len maybe is 0.
				return;
			}

包头第一个字节必须是LEAGOR_BLE_PREFIX_BYTE。缓存中至少有包头字节数(LEAGOR_BLE_MTU_HEADER_SIZE)。

			// 2. len
			const int cmd = recv_data_[1];
			if (recv_cmds_.count(cmd) == 0) {
				// skip first. then again.
				memcpy(recv_data_, recv_data_ + 1, recv_data_vsize_ - 1);
				recv_data_vsize_ --;
				continue;
			}
			const int len = posix_mku16(recv_data_[2], recv_data_[3]);
			if (recv_data_vsize_ < LEAGOR_BLE_MTU_HEADER_SIZE + len) {
				return;
			}

根据包头中命令进行判断。这些命令必须是已注册的recv_cmds_中的一个。

由包头提取出payload字节数后,缓存中至少有“包头+payload”字节数。

			break;
		}

		const int cmd = recv_data_[1];
		const int len = posix_mku16(recv_data_[2], recv_data_[3]);

		const int len2 = LEAGOR_BLE_MTU_HEADER_SIZE + len;
		VALIDATE(recv_data_vsize_ >= len2, null_str);

		if (did_read_) {
			did_read_(cmd, recv_data_ + LEAGOR_BLE_MTU_HEADER_SIZE, len);

调用app提供的did_read_。这个len不是参数len,是从头中取出的payload字节数。len2则是“header+payload”的消息长度。

图5 did_read_消费第N个消息前缓存(recv_data_)
		}

		if (recv_data_vsize_ > len2) {
			memcpy(recv_data_, recv_data_ + len2, recv_data_vsize_ - len2);
		}
		recv_data_vsize_ -= len2;

既然已处理len2字节,把后面recv_data_vsize_ - len2字节向前挪到开始。

图6 did_read_消费完第N个消息、调整后缓存(recv_data_)
		if (recv_data_vsize_ == 0) {
			SDL_Log("tblebuf::enqueue(1.2): there is no extra data");
		} else {
			std::string str = rtc::hex_encode((const char*)recv_data_, recv_data_vsize_);
			SDL_Log("tblebuf::enqueue(1.2): extra data: %s, recv_data_vsize_: %i", str.c_str(), recv_data_vsize_);
		}
	}
}

 

 

全部评论: 0

    写评论: