1.项目背景
我需要通过UDP接收GPS设备的位置信息,厂家定义的数据包结构大致如下:
数据包头:
描述 | 字节数 |
---|---|
命令标志 | 2 |
版本号 | 2 |
数据体大小 | 4 |
数据体:
描述 | 字段类型 | 数据长度 |
---|---|---|
设备编号 | unsigned char | 10 |
设备类型 | unsigned char | 1 |
经度 | double | 8 |
纬度 | doube | 8 |
设备编号:不足20位数字,在数字前补零,每两个数字共用一个字节
2.初始设计
按照以前的经验,我很自然地先定义了一个结构体:
typedef struct dataHeader
{
unsigned short Flag;
unsigned short Ver;
unsigned int Size;
}Header;
typedef struct dataLocation
{
unsigned char DeviceName[10];
unsigned char DeviceType;
double Longitude;
double Latitude;
}Location;
typedef struct Data
{
Header header;
Location location;
}GPSData;
然后就是一段简单的接收程序:
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sstream>
#include "tUtil.h"
int main(int argc, char *argv[])
{
int ret;
char* PORT="9302";
//定义udp套接字
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(PORT));
addr.sin_addr.s_addr = htonl(INADDR_ANY);
int sock;
if ( (sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("Socket init error;");
exit(1);
}
//绑定端口
if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0)
{
perror("Socket bind error;");
exit(1);
}
//发送端地址
struct sockaddr_in clientAddr;
memset(&clientAddr,0,sizeof(clientAddr));
size_t n;
socklen_t len = sizeof(clientAddr);
//声明接收数据结构体
GPSData gpsLoc;
char buff[sizeof(gpsLoc)];
memset(buff,0x00,sizeof(buff));
while (1)
{
n = recvfrom(sock, buff, sizeof(buff), 0, (struct sockaddr*)&clientAddr, &len);
if (n > 0)
{
memcpy(&gpsLoc,buff,sizeof(gpsLoc));
//打印发送端信息
printf("From address: %s port: %u \n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
//处理接收到的信息
//先打印个编号吧
char id[20]="";
for(int i =0;i<10;i++)
{
char ts[2]="";
sprintf(ts,"%02x",gpsLoc.location.DeviceName[i]);
sprintf(id,"%s%s",id,ts);
}
printf("GPS Info: DeviceID: %s\n",id);)
}
}
return 0;
}
一切看起来都那么美好,开始测试啦!
3.测试过程
3.1 筛选数据
当我收到第一条消息后:程序卒;
抓个包看下,来了一条跟上面结构完全不一样的数据,好吧,原来有其他格式的消息发过来了。这时候我要做个数据筛选,于是我改成了这样:
if (n>0 && buff[1] == 0xcc)
{
...
}
再跑一下试试,等了好久,没有一条符合要求的,是不是没有消息推过来啊,再抓个包分析下,有数据的啊,为什么不符合判断条件呢?
打印一下buff[1]发现,它不是0xcc,是0xffffffcc,这跟我想像的不一样啊!然后我看到了这篇博客,简单来讲就是:printf()函数的%x(X)输出的是Int型别的16进制格式,所以char型别的c变量会被转换成Int型别,而char类型是有符号的。再看看上面别人数据接口的定义:
unsigned char DeviceName[10];
恍然大悟,我的buff数组是char类型的,而别人发过来的是unsigned char的,所以将判断条件改成下面这样:
if (n>0 && (unsigned char)buff[1] == 0xcc)
{
...
}
终于收到数据啦,打印出来一串设备编号,很开心啊!
3.2 结构体大小
接下来解析经纬度了,double数据嘛,很容易的:
//把上面那句打印编号的代码改成这样
printf("GPS Info: DeviceID: %s, Longitude: %f Latitude: %f \n",id, gpsLoc.location.Longitude,gpsLoc.location.Latitude);
收到的结果是经纬度都是0,刚开始想想,正常嘛,也许没有收到信号呢,再等等吧…
依旧是0,偶尔还有非常长的一串数字…
直觉告诉我,解析出错了,错在哪呢?还是分析抓到的数据包。
收到的数据长度是35,我之前还特意算了一下自己定义的结构体的size应该是40,显然对方发过来的数据,没有按默认的字节对齐方式,而是按照1字节对齐了,应该是为了节省发送的数据量;
那么就需要按1字节对齐,在结构体定义的最前面加上#pragma pack(1),这里提醒一下,结构体定义完成后一定要养成#pragma pack()恢复默认的对齐方式,因为很可能影响到你用的第三方库,比如我这个项目刚好用了tinyxml生成xml,刚开始没有加#pragma pack(),结果生成xml结构就一直出错。
好了,那么下面应该就没问题了吧。
3.3 大小端、网络字节序
事实证明,并没有好,输出的现象跟上面一样,一定是哪里不对。
这里要说一下,别人的接口中说明了用的是网络字节序,在UDP/TCP/IP协议中,规定网络字节序用的是大端模式,所以,我立刻检查了下我系统使用的是大端还是小端模式,一段代码验证一下,结果是小端模式,那么接下来的事情就清晰明了了,我得转换一下字节序,
所以我把解析经纬度的代码改成了这样:
typedef union cTod{
char a[8];
double f;
}CTOD;
//处理接收到的double数据
CTOD lonUnion,latUnion;
memcpy(lonUnion.a,buff+19,8);
memcpy(latUnion.a,buff+27,8);
//测试系统大小端
union check
{
int i;
char ch;
} c;
c.i = 1;
if(c.ch == 1)//小端模式
{
for(int n =0;n<4;n++)
{
char tmp = lonUnion.a[n];
lonUnion.a[n] = lonUnion.a[7-n];
lonUnion.a[7-n] = tmp;
tmp = latUnion.a[n];
latUnion.a[n] = latUnion.a[7-n];
latUnion.a[7-n] = tmp;
}
printf("*************Little endian Union result: Longitude: %f Latitude: %f **********\n", lonUnion.f,latUnion.f);
}
else if(c.ch == 0)//大端模式,与网络字节序一致
{
printf("*************Big endian Union result:, Longitude: %f Latitude: %f **********\n", lonUnion.f,latUnion.f);
}
printf("GPS Info: DeviceID: %s, Longitude: %f Latitude: %f \n",id, lonUnion.f,latUnion.f);
这里用到了联合体,对,就是那个我平时都不知道用来干嘛的玩意儿。
我用union的性质,实现double类型和char数组之间的数据内存共享,顺便用它判断一下本机的字节序是大端还是小端。
将收到的经纬度数据保存到union的char数组中,对于小端模式的系统,再对char数组做一下逆序操作,这样union中的double数据就是我要的经纬度了。
4.总结
跑了一下,终于可以正常运行了,也看到了熟悉的经纬度数据。最终的代码我传到CSDN了,有兴趣的同学可以在我的资源主页找到。
学习C++的道路漫长而又曲折啊…