Linux C++網絡編程實例分享——有關結構體、字節對齊、大小端字節序

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章