框架_C语言_数据包解析代码框架
数据解析
何时会用到数据解析:
在数据传输中,我们经常会需要规定数据的一些格式,这就需要我们对数据进行组包、拆分操作,因此,只要有数据报文的传输,那么就一定有数据解析的存在。下面以一个具体的示例来详细的描述数据解析的过程以及代码框架。
逻辑上的拆包:
逻辑上的拆包主要是搞懂"帧头是什么"、"数据段传递了什么"、"校验位如何规定的"这三个问题。
首先我们需要在相应的协议文档找到数据的格式是什么样子的,比如在下图,数据格式就是由帧起始位、帧长度域等组成的,这一部分往往是报文的控制内容。
接下来我们需要找到数据域是如何组成的,这代表这一个报文数据所要传递的信息,比如在下图,数据域就要传递采集时间、表底值等信息。
最后我们需要搞清楚校验位是怎么计算的,这关系着接收时如何去判断数据包的正确性。比如在当前举例的协议中,校验位是1位,它是校验和的计算模式,文档中的描述如下:
报文拆包示例:
下面举一个例子来应用上述的拆包流程。真实的0x31报文如下:
68002e03333832363030303100000000000000000001310dc6044c00000000000000000000000000000000000000000000ec
这是一段由十六进制展示的0x31报文,68代表0x68。帧结构如下:
根据这个结构,我们可以清晰的得知:
内容含义 | 内容数据 |
帧起始位 | 0x68 |
帧长度域 | 0x00 0x2e |
序列号域 | 03 |
设备地址 | 33 38 32 36 30 30 30 31 00 00 00 00 00 00 00 00 |
加密方式 | 00 |
数据对象 | 01 |
帧类型码 | 31 |
数据域 | 0dc6044c00000000000000000000000000000000000000000000 |
校验和 | ec |
0x31数据域的结构如下:(部分内容)
根据这个结构,我们可以知道0dc6044c代表的是采集时间,这样我们在逻辑上就已经解析完成了该报文。
代码解析
代码框架:
1、使用结构体对帧结构、数据域进行管理,需要注意字节对齐
2、编写拆包函数和分析函数,并编写相应的接收处理函数
3、各个报文的发送处理函数,并编写组包函数
1、结构体声明
帧结构结构体:
根据报文结构,定义相应的结构体声明,具体代码实现如下:
//帧结构
#pragma pack(1) //开始字节对齐,以单字节对齐
#define BUF_SIZE 1024
#define ADDRESS_SIZE 16
typedef struct frame{
//帧起始域
unsigned char StartChar;
//帧长度域:从序列号域到帧数据域的所有字节数,高字节在前,低字节在后;
unsigned char FrameLength_H;
unsigned char FrameLength_L;
//序列号域
unsigned char SerialNo;
//设备地址
unsigned char Address[ADDRESS_SIZE];
//加密方式
unsigned char ProtectFlag;
//数据对象
unsigned char DataObject;
//类型标志
//注意:这里的类型不一定都能用枚举实现,需要注意帧结构中该位的大小
//enum eFrameType FrameType; //4字节
unsigned char FrameType; //帧结构中要求1字节
//数据域
unsigned char payload[BUF_SIZE];
//校验和,从序列号域到数据域的和校验
unsigned char checksum;
}frame_t;
#pragma pack() //结束字节对齐,以默认形式进行对齐
数据包结构体:
下面将以0x01和0x02报文为例,编写代码解析例程,0x01、0x02报文格式如下:
具体的结构体声明如下:
//数据包结构体
#define STATION_NUM_SIZE 16
#pragma pack(1)
typedef struct login_01{
unsigned char StationNum[STATION_NUM_SIZE]; //装编码
unsigned char StationMode; //桩类型
unsigned char GunNum; //充电枪数量
unsigned char ProtocolVersion; //通信协议版本
unsigned char ProgramVersion[16]; //程序版本
unsigned int longitude; //充电桩经度
unsigned int latitude; //充电桩纬度
char DeviceFingerprint[32]; //设备指纹
}login_01_t;
#pragma pack()
#pragma pack(1)
typedef struct login_02{
unsigned char result; //登录结果
unsigned char StationNum[STATION_NUM_SIZE]; //桩编码
unsigned char EncryptionKey[32]; //加密秘钥
unsigned char EncryptionVector[32]; //加密向量
unsigned int time; //当前时间
}login_02_t;
#pragma pack()
2、发送相关函数
2.1 函数实现
概述:
当我们要发送数据时,我们需要先调用具体的报文发送函数将数据存入到帧结构的数据段中,之后调用组包函数添加数据包的帧头以及校验位,最终调用底层发送函数发送出去。
发送函数的实现:
/*
* send_req_01 :发送0x01报文
* param fd :socket的fd
* param pReport:要发送的数据
*/
void send_req_01(int fd,frame_t* pReport){
login_01_t* p = (login_01_t*)pReport->payload;
unsigned int len = 0;
//接收到后,根据需求去赋值使用,这里只打印调试一下结果
//1.桩编码
strcpy((char*)p->StationNum,"this is client");
len += 16;
//2.桩类型
p->StationMode = 0x01;
len += 1;
//3.充电枪数量
p->GunNum = 1;
len += 1;
//4.通信协议版本
p->ProtocolVersion = 0x87;
len += 1;
//5.程序版本
strcpy((char*)p->ProgramVersion,"MY_VERSION");
len += 16;
//6.充电桩经度
p->longitude = 0x1234;
len += 4;
//7.充电桩纬度
p->longitude = 0x5678;
len += 4;
//8.设备指纹
memset(p->DeviceFingerprint,0,sizeof(p->DeviceFingerprint));
len += 32;
PackPacket(fd,pReport,len,0x01);
}
组包函数的实现:
/*
* PackPacket:组包函数,添加数据包的帧头以及校验位
* param fd :socket的fd
* param pReport :要发送的数据
* param payloadLength:数据段长度
* param FrameType :报文类型
*/
void PackPacket(int fd,frame_t* pReport,unsigned char payloadLength,unsigned char FrameType){
//1.帧起始域
pReport->StartChar = 0x68;
//2.帧长度域
unsigned int FrameLen = 1+16+1+1+1+payloadLength;
pReport->FrameLength_H = FrameLen >> 8;
pReport->FrameLength_L = FrameLen & 0xff;
//3.序列号域
//这里不根据具体的逻辑处理序列号域,写0填充
pReport->SerialNo = 0;
//4.设备地址
//这里不根据具体的逻辑处理设备地址,写abc填充
strcpy((char*)pReport->Address,"abc");
//5.数据对象
//这里不根据具体的逻辑处理数据对象,写固定的交流,1枪
pReport->DataObject = (0x01 << 6)| 0x01;
//6.帧类型码
pReport->FrameType = FrameType;
//7.数据域
//该部分已经设置
//8.校验和
pReport->checksum = sumBCH(&pReport->SerialNo,FrameLen);
unsigned char SendNum = 1+2+1+16+1+1+1+1;//除了数据域以外的全部字节数
if(payloadLength < FRAME_MIN_SIZE){
//当有效载荷小于规定的最小载荷时,发送的有效载荷长度应该为规定的最小载荷
SendNum += FRAME_MIN_SIZE;
}else{
SendNum += payloadLength;
}
Socket_Write(fd,pReport,SendNum);
}
2、接收相关函数
2.1 函数实现
概述:
当接收到数据后,我们需要对其进行拆包操作,该操作主要是将报文的各个字段解析并获取含义,同时对整个数据进行校验,判断数据包无误。之后调用分析函数将报文类型进行解析,并传递到各个具体的报文接收函数。
拆包函数的实现:
/*
* CheckPacket:检测数据包正确性
* param buf:接收到的数据包
* */
void CheckPacket(unsigned char* buf){
//以帧结构形式访问接收到的数据
frame_t* pReport = (frame_t*)buf;
printf("Debug:GET CheckPacket\n");
//1.判断帧起始位是否正确,应为0x68
if(pReport->StartChar != 0x68){
return;
}
//2.获取序列号域到帧数据域的数据长度
//有时需要判断该数据包是否过长,但这里不需要判断
unsigned short FrameLength =pReport->FrameLength_H*256 +pReport->FrameLength_L;
//3.检验校验和,计算值应等于接收值
if(sumBCH(&pReport->SerialNo,FrameLength) != *(&pReport->SerialNo + FrameLength)){
return;
}
//4.解析数据包
AnalysisPacket(pReport);
}
分析函数的实现:
/*
* AnalysisPacket:解析数据包
* param pReport:已经检验过的正确的数据包
* */
void AnalysisPacket(frame_t* pReport){
printf("Debug:GET AnalysisPacket\n");
//1.根据所需从报文各个字段获取数据,这里只进行打印,如果有需要可以存储到相应的变量
//1.1 设备地址,ASCII形式
char address[ADDRESS_SIZE];
strcpy(address,(char*)pReport->Address);
printf("设备地址 = %s\n",address);
//1.2 桩类型,高 2 位表示桩类型(0:直流,1:交流,2:交直流)
char StationMode = pReport->DataObject >> 6;
switch(StationMode){
case 0:
printf("桩类型 = 直流\n");
break;
case 1:
printf("桩类型 = 交流\n");
break;
case 2:
printf("桩类型 = 交直流\n");
break;
default:
printf("桩类型错误\n");
return;
}
//1.3 枪号,低 6 位表示枪号(0:充电机主体, 1:1 枪,2:2 枪)
unsigned char GunNum = pReport->DataObject & 0x3f;
switch(GunNum){
case 0:
printf("枪号 = 充电机主体\n");
break;
case 1:
printf("枪号 = 1枪\n");
break;
case 2:
printf("枪号 = 2枪\n");
break;
default:
printf("枪号错误\n");
return;
}
//1.4 帧类型码
//根据不同的类型码调用不同的接收处理函数
//这里只实现了一个为例子,之后可以添加相应的case和相应的接收处理函数
switch(pReport-> FrameType){
case 0x01:
rec_req_01(pReport);
break;
default:
printf("帧类型码未定义\n");
return;
}
}
接收函数实现:
void rec_req_01(frame_t* pReport){
login_01_t* p = (login_01_t*)pReport->payload;
//接收到后,根据需求去赋值使用,这里只打印调试一下结果
//1.桩编码
printf("桩编码 = %s\n",p->StationNum);
//2.桩类型
switch(p->StationMode){
case 0:
printf("桩类型 = 直流\n");
break;
case 1:
printf("桩类型 = 交流\n");
break;
case 2:
printf("桩类型 = 交直流\n");
break;
default:
printf("桩类型错误\n");
return;
}
//3.充电枪数量
printf("充电枪数量 = %d\n",p->GunNum);
//4.通信协议版本
printf("通信协议版本 = %d\n",p->ProtocolVersion);
//5.程序版本
printf("程序版本 = %s\n",p->ProgramVersion);
//6.充电桩经度
printf("充电桩经度 = %d\n",p->longitude);
//7.充电桩纬度
printf("充电桩纬度 = %d\n",p->latitude);
//8.设备指纹
printf("设备指纹 = %s\n",p->DeviceFingerprint);
}
3、测试main函数
客户端的main:
#include <stdio.h>
#include <string.h>
#include "net.h"
#include "frame_client.h"
int main(int argc,char** argv){
int fd = Socket_TcpClientInit(argc,argv);
frame_t Report;
memset(&Report,0,sizeof(Report));
send_req_01(fd,&Report);//发送0x01报文
return 0;
}
服务器的main:
#include <stdio.h>
#include "net.h"
#include "frame_server.h"
int main(int argc,char** argv){
int fd = Socket_TcpServerInit(argc,argv);//建立服务器
int newfd = Socket_TcpServerAccept(fd); //等待客户端接入
unsigned char buf[1024] = {0};
int len;
len = Socket_Read(newfd,buf,sizeof(buf)); //从网络上读取数据
for(int i=0;i<len;i++){
printf("%x ",buf[i]);
}
puts("");
CheckPacket(buf);
return 0;
}