前言:
本人从事于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; //消息Id
unsigned 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.消息id
memcpy(&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;
}
实现效果如下图: