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

上一篇:利用哈希算法找两个文件代码行不同的C语言代码


下一篇:ARC 与非 ARC 之间那些的'*'