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++的道路漫長而又曲折啊…