前言:
本人从事于Linux应用开发(音视频应用方向),现在主要是负者AI摄像头的开发,在学音视频的途中,虽然是个小白,但是更愿意把自己所学音视频的一些知识分享给大家,以后每周都会更新哦!
本期介绍的是用c语言对Jt808协议解析,要对协议进行解析,首先我们要知道jt808的协议基础以及数据传输过程的信息组成。
一、数据类型:
数据类型 | 描述及要求 |
BYTE | 无符号单字节整型(字节,8 位) |
WORD | 无符号双字节整型(字,16 位) |
DWORD | 无符号四字节整型(双字,32 位) |
BYTE[n] | n 字节 |
BCD[n] | 8421 码,n 字节 |
STRING | GBK 编码,若无数据,置空 |
传输规则协议采用大端模式(bir-endian)的网络字节序来传递字和双字。
约定如下:
----------字节(BYTE)的传输约定:按照字节流的方式传输;
----------字(WORD)的传输约定:先传递高八位,再传递低八位;
----------双字节(DWORD)的传输约定:先传递高 24 位,然后传递高 16 位,在 传递高八位,最后传递低八位。
这里要区分一下字节序的大小端模式:
Little endian:将低序字节存储在起始地址。
Big endian:将高序字节存储在起始地址。
举个例子:int a = 0x12345678;
0x78属于低地址,而0x12属于高地址
如果是大端模式,那输出方式是0x12 0x34 0x56 0x78,如果是小端模式,那么就是0x78 0x56 0x34 0x12
这个很容易理解,人类读写数据的习惯是大端字节序,而小端字节序是反着人类来的。
而我们网络字节序是大端模式,因此要转为大端模式。
注:
**大端小端是对于高于一个字节的数据类型来说的,比如说int,short等。char 类型的话就不存在大小端的问题。**
- 服务器一般是大端的(因为网络字节序是大端的,服务器为大端的话就不用修改了) 个人的电脑一般是小端的。
- 主机向网络上传输的数据,只要大于两个字节,都需要将主机字节序转换成网络字节序。
大端转小端的一些api。
#include <arpa/inet.h> uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);
二、消息的组成
1.每条消息由标位头、消息头、消息体和校验码组成,消息结构如图所示:
标识位 | 消息头 | 消息体 | 检验码 | 标识位 |
2.标识位
标识位采用 0x7e 表示,若校验码、消息头以及消息体中出现 0x7e,则要进行转义处理,转 义规则定义如下:
0x7e ←→0x7d 后紧跟一个 0x02; 0x7d ←→0x7d 后紧跟一个 0x01
转义处理过程如下:
发送消息时:消息封装→计算机并填充校验码→转义;
接收消息时:转移还原→验证校验码→解析消息。
示例:
发送一包内容为 0x30 0x7e 0x08 0x7d 0x55 的数据包,则经过封装如下:0x7e 0x30 0x7d 0x02 0x08 0x7d 0x01 0x55 0x7e。
三、消息头
消息头内容
起始字节 | 字段 | 数据类型 | 说明 |
0 | 消息 ID | WORD | |
2 | 消息体属性 | WORD | 消息体属性格式结构见图 2 |
4 | 终端手机号 | BCD[6] | 根据安装终端自身的手机号转换。手机号 不足 12 位,则在前补充数字,大陆手机 号补充数字 0,港澳台则根据其区号进行 位数补充 |
10 | 消息流水号 | WORD | 按发送顺序从 0 开始循环累加 |
12 | 消息包封装项 | 如果消息体属性中相关标识位确定消息分 包处理,则该项有内容,否则无该项 |
消息体内容:
实时音视频传输请求
消息 ID:0x9101
报文类型:信令数据报文。平台向终端设备请求实时音视频传输,包括实时视频传输、主动发起双向语音对讲、单向监听、向所有终端广播语音和特定透传等。消息体数据格式见如下表。 终端在收到此消息后回复视频终端通用应答,然后通过对应的服务器IP地址和端口号建立传输链路,然后按照音视频流传输协议传输相应的音视频流数据。
实时音视频传输请求数据格式
起 始 字 节 | 字 段 | 数据类型 | 描述及要求 |
0 | 服务器 IP 地址长度 | BYTE | 长度 n |
1 | 服务器 IP 地址 | STRING | 实时视频服务器 IP 地址 |
1 + n | 服务器视频通道监听端口号 (TCP) | WORD | 实时视频服务器 TCP 端口号 |
3 + n | 服务器视频通道监听端口号 (UDP) | WORD | 实时视频服务器 UDP 端口号 |
5 + n | 逻辑通道号 | BYTE | |
6 + n | 数据类型 | BYTE | 0:音视频,1:视频,2:双向对讲,3:监听, 4:中心广播,5:透传 |
7 + n | 码流类型 | BYTE | 0:主码流,1:子码流 |
平台收到视频终端的特殊报警后,应无须等待人工确认即主动下发本条指令,启动实时音视频传输。
通过阅读以上信息得知,我们解析先要从消息头跟消息体的内容出发,可以分别定义消息头跟消息体的结构体,里面的数据类型要跟以上的信息一致哦!
要解析这个协议,首先我将这些字符都放进数组里,然后封装了一个Jt808app函数,定义一个char *p指向这个数组,然后对指针p进行遍历,就能达到快速获得某个字符的地址跟值的效果,与数组相比,用for循环遍历显得比较麻烦。因此用指针指向数组的方法是最简便的。然后用memcpy(不能用strcpy)将地址赋给刚刚结构体定义的地址。然后用*取内容符号把值取出来,接着打印出来每个数据类型。
strcpy和memcpy的区别:
复制的内容不同,strcpy只能复制字符串,而memcpy可以复制任意内容,例如:字符数组、整型、结构体、类等
复制的方法不同,strcpy不需要指定长度,它遇到被复制字符的串结束符“\0”才结束,所以容易溢出。memcpy是根据其第三个参数决定复制的长度
用途不同,通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy
void *memcpy(void *dest, const void *src, size_t n);
功能:从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中
char* strcpy(char* dest, const char* src)
这个是需要解析的数据:
7e 91 01 00 16 01 33 04 70 54 35 42 cb 0e 31 32
30 2e 37 38 2e 32 30 35 2e 31 37 38 1e 77 00 00
01 00 01 54 7e
以下是我写的代码读不懂看我的注释哦!
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <assert.h>
#include <arpa/inet.h>struct msgHead
{unsigned char msgHeader; //消息头unsigned short msgId; //消息Idunsigned short msgAddr; //消息属性unsigned char BCD[6]; //终端手机号unsigned short serialNum; //消息流水号unsigned short msgPacket; //消息包封装项unsigned char checkBit; //检验位unsigned char msgTail; //消息尾
};struct msgBody
{unsigned char serverIpLen; //服务IP长度char serverIp[12]; //IP地址unsigned short msgTcpPort; //TCP端口号unsigned short msgUdpPort; //UPD端口号unsigned char channelNum; //逻辑通道号 unsigned char dataType; //数据类型unsigned char dataRate; //码流类型
};char charSave[128] = {'\0'};//fgetc每次读一个十六进制的字符,hexHandler功能是将两个十六进制的字符合成一个,并放到数组里
int hexHandler(FILE *fp)
{uint32_t cnt = 0;int out;int i = 0;uint32_t a = 0;unsigned char data[3];while (1){out = fgetc(fp);//printf("out = 0x%02x\n",out);if (out == EOF){if (a == 1){fputc(data[0], fp);cnt++;}break;}else if (out == ' ' || out == '\n' || out == '\r' || out == '\t' ){//out = 0xff;continue;}else if (out >= '0' && out <= '9'){out = out - '0';}else if (out >= 'a' && out <= 'f'){out = out - 'a' + 10;}else if (out >= 'A' && out <= 'F'){out = out - 'A' + 10;}else {printf("存在其他文件!\n");fclose(fp);return -1;}data[a] = out;a++;if (a == 2){a = 0;charSave[i] = (data[0] *16) + data[1];i++;cnt++;}}fclose(fp);return cnt;}
//JT808协议解析
void Jt808app()
{char data[128];int Len;int i;char *p = charSave;struct msgHead saveMsgHeader;struct msgBody saveMsgBody;printf("======================================================================================\n");printf("消息头解析begin:\n");//一、消息头//1.消息头 saveMsgHeader.msgHeader = *p; printf("消息头:0x%02x\n",saveMsgHeader.msgHeader);p=p+1;//2.消息idmemcpy(&saveMsgHeader.msgId,p,2);printf("消息Id:0x%04x\n",htons(saveMsgHeader.msgId));p = p+2;//3.消息属性 memcpy(&saveMsgHeader.msgAddr,p,2);printf("消息属性:0x%04x\n",htons(saveMsgHeader.msgAddr));p = p+2;//4.终端手机号memcpy(&saveMsgHeader.BCD,p,6);printf("终端手机号:");for(i = 0;i<6;i++){printf("%02x",saveMsgHeader.BCD[i]);}p = p+6;printf("\n");//5.流水号memcpy(&saveMsgHeader.serialNum,p,2); printf("流水号:0x%4x\n",htons(saveMsgHeader.serialNum));p = p+2;printf("======================================================================================\n");printf("消息体解析begin:\n");
//二、消息体//1.服务IP长度:memcpy(&saveMsgBody.serverIpLen,p,1);Len = (int)*p;//printf("len:%d\n",Len);printf("服务IP长度:%d\n",(int)saveMsgBody.serverIpLen);p=p+1;//2.服务IP:memcpy(&saveMsgBody.serverIp,p,Len); printf("服务IP:%s\n",saveMsgBody.serverIp);p = p+Len;//3.TCP端口号memcpy(&saveMsgBody.msgTcpPort,p,2); printf("TCP端口号:%d\n",htons(saveMsgBody.msgTcpPort));p = p+2;
//4.UDP端口号memcpy(&saveMsgBody.msgUdpPort,p,2); printf("UDP端口号:%d\n",htons(saveMsgBody.msgUdpPort));p = p+2;//5.逻辑通道号memcpy(&saveMsgBody.channelNum,p,1); printf("逻辑通道号:%02x\n",saveMsgBody.channelNum);p = p+1;//6.数据类型saveMsgBody.dataType= (int)*p;if(saveMsgBody.dataType == 0){printf("数据类型:音视频\n");}else if(saveMsgBody.dataType == 1){printf("码流类型:视频\n");}else if(saveMsgBody.dataType == 2){printf("码流类型:双向对讲\n");}else if(saveMsgBody.dataType == 3){printf("码流类型:监听\n");}else if(saveMsgBody.dataType == 4){printf("码流类型:中心广播\n");}else if(saveMsgBody.dataType == 5){printf("码流类型:透传\n");}p = p+1;
//7.码流类型saveMsgBody.dataType = (int)*p;if(saveMsgBody.dataType == 0){printf("码流类型:主码流\n");}else if(saveMsgBody.dataType == 1){printf("码流类型:子码流\n");} p = p+1;//检验位memcpy(&saveMsgHeader.checkBit,p,1);printf("检验位:0x%02x\n",saveMsgHeader.checkBit);p = p+1;//消息尾saveMsgHeader.msgTail = *p; printf("消息尾:0x%02x\n",saveMsgHeader.msgTail);printf("======================================================================================\n");printf("解析完毕!\n");
}
//打印数组,我是用来测试hexHandler这个函数有没将十六进制的数据放到数组里。
void printArry(int cnt)
{int i;printf("printArry begin\n");for(i = 0;i < cnt;i++){printf("%d = 0x%02x\n",i,(unsigned char)charSave[i]);}printf("printArry %d byte end\n",cnt);}int main()
{FILE* fp;int num;char fileName[12] = "9101.txt";fp = fopen(fileName, "r");if (fp ==NULL){printf("9101.txt cannot be open!\n");return -1;}num = hexHandler(fp); Jt808app();//printArry(num);return 0;
}
实现效果如下图: