NB-IoT使用笔记(2)实现UDP访问DNS服务获取IP地址(2)

5 篇文章 1 订阅
1 篇文章 0 订阅

背景

书接上回,第一版采用了python方法实现了UDP方式请求DNS服务,然而在使用单片机时是用C语言实现的,并没有python语言那么高的灵活性,考虑到此,今天使用C语言重新实现这个功能。本次实验是在Ubuntu64位机运行的,如果是windows的童鞋可以改一下对应的包含库文件。

预备知识

在编程过程中,对于DNS服务中数据包的格式有了一个更加深刻的理解。
从网上的一些博客以及相关资料中(主要参考深入理解DNS报文格式 - 夜苍山 - CSDN博客的部分内容),查到了其种一些需要注意的点,一些重要参数我会用粗体标识:

公共头报文

在请求和应答报文中有12个字节的固定长度,称为公共头报文,其内容如下
这里写图片描述
- 标识:由请求端设置的16位ID。唯一确定一组发送数据包和接收数据包,可以避免多次发送不同数据包之间混淆,可以由rand()%256 分两次得到。
- 标志:同样由请求端按位设置的报文特性,不同的位代表不同功能:
这里写图片描述
- QR 1个比特位用来区分是请求(0)还是应答(1)
- **OPCODE**E4个比特位用来设置查询的种类,应答的时候会带相同值,可用的值如下:
- 0 标准查询 (QUERY);
- 1 反向查询 (IQUERY);
- 3-15 保留值,暂时未使用;
- AA 授权应答(Authoritative Answer) - 这个比特位在应答的时候才有意义,指出给出应答的服务器是查询域名的授权解析服务器(注意因为别名的存在,应答可能存在多个主域名,这个AA位对应请求名,或者应答中的第一个主域名)。
- RA 支持递归(Recursion Available) - 这个比特位在应答中设置或取消,用来代表服务器是否支持递归查询。
- Z 保留值,暂时未使用。在所有的请求和应答报文中必须置为0。
- RCODE 应答码(Response code) - 这4个比特位在应答报文中设置,代表的含义如下:
- 0 没有错误;
- 1 报文格式错误(Format error) - 服务器不能理解请求的报文;
- 2 服务器失败(Server failure) - 因为服务器的原因导致没办法处理这个请求;
- 3 名字错误(Name Error) - 只有对授权域名解析服务器有意义,指出解析的域名不存在;
- 4 没有实现(Not Implemented) - 域名服务器不支持查询类型;
- 5 拒绝(Refused) - 服务器由于设置的策略拒绝给出应答。比如,服务器不希望对某些请求者给出应答,或者服务器不希望进行某些操作(比如区域传送zone transfer);
- 6-15 保留值,暂时未使用;
- 问题数:无符号16位整数表示报文请求段中的问题记录数,这里取1只请求一个主机的IP。
- 资源记录数:无符号16位整数,应答报文中该项内容与请求报文(一般为0)不同,在本次实验中可以通过这一项判断IP地址个数。
- 授权资源记录数:无符号16位整数。
- 额外资源记录数:无符号16位整数。


请求报文

先上图
这里写图片描述

  • 查询码名:顾名思义,就是主机名称的ASCII了,这里需要注意的是,结尾除了用0以外,还要将主机名中的字符串根据 ‘.’ 切片,切片后的子字符串每个前面加上该字符串长度,举一个栗子
    baidu.com 就是 5 98 97 105 100 117 3 99 111 109 0 ,其种的5和3表示长度。
  • 查询类型:两个字节表示查询类型,取值可以为任何可用的类型值,以及通配码来表示所有的资源记录。
名字数值描述
A1期望获得查询名的IP地址
NS2一个授权的域名服务器
CNAME5规范名称
PTR12指针记录
HINFO13主机信息
MX15邮件交换记录
AXFR252对区域转换的请求
ANY255对所有记录的请求

还有一些查询类型,可以在上面的博客中看到英文描述,这里没用到就不提了。

  • 查询类:通常为1,表明是Internet数据。

    这里给出一个完整的发送报文,总共长度28,请求的是 baidu.com
    194 186 1 0 0 1 0 0 0 0 0 0 5 98 97 105 100 117 3 99 111 109 0 0 1 0 1


应答报文

老规矩,先上结构图,图穷匕首见嘛~
这里写图片描述
前面的三项中除了域名部分都相同,与请求报文相同。有一点不同就是,当报文中主机名重复出现的时候,该字段使用2个字节的偏移指针来表示。比如,在资源记录中,域名通常是查询问题部分的域名的重复,因此用2字节的指针来表示,具体格式是最前面的两个高位是 11,用于识别指针。其余的14位从DNS报文的开始处计数(从0开始),指出该报文中的相应字节数。一个典型的例子,C00C(1100000000001100,12正好是头部的长度,其正好指向查询名字字段)。

  • 生存时间(TTL):以秒为单位,表示的是资源记录的生命周期,一般用于当地址解析程序取出资源记录后决定保存及使用缓存数据的时间,它同时也可以表明该资源记录的稳定程度,极为稳定的信息会被分配一个很大的值(比如86400,这是一天的秒数)。
  • 资源数据长度:在DNS请求IP地址时,这里都是4,后面紧跟IP地址。
  • 资源数据:该字段是一个可变长字段,表示按照查询段的要求返回的相关资源记录的数据。可以是Address(表明查询报文想要的回应是一个IP地址)或者CNAME(表明查询报文想要的回应是一个规范主机名)等。

同样,给出一个栗子(对应上面的请求报文):
194 186 129 128 0 1 0 3 0 0 0 0 5 98 97 105 100 117 3 99 111 109 0 0 1 0 1 192 12 0 1 0 1 0 0 1 206 0 4 111 13 101 208 192 12 0 1 0 1 0 0 1 206 0 4 123 125 114 144 192 12 0 1 0 1 0 0 1 206 0 4 220 181 57 217
这里的加粗的部分,第一个3表示的是baidu.com对应了三个IP地址,后面三个加粗的分别是他们的值;斜体部分可以和发送报文进行对比。


预备知识就到这里,关于上面参数的具体细节可以参考网上一些专业的资料或者专业书籍等,这里只用到了实现一个主机名请求DNS服务涉及到的内容。

代码

简单粗暴先上代码:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include<time.h>

#define MYPORT 53
#define SHOW_DATA
char* SERVERIP = "114.114.114.114";

#define ERR_EXIT(m)\
do \
{\
    perror(m); \
    exit(EXIT_FAILURE); \
} while(0)

void ask_ip(int sock,char * name,char show)
{
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(MYPORT);
    servaddr.sin_addr.s_addr = inet_addr(SERVERIP);

    int ret;
    int i,j,k;
    char * s = name;
    unsigned char sendbuf[1024] = {0};
    unsigned char host[50] = {0};
    unsigned char nd1[10] = {1,0,0,1,0,0,0,0,0,0};  //数据标志,期望递归RD置位,一个问题
    unsigned char nd2[5] = {0,0,1,0,1};                  //主机名结尾0,查询类型1,查询类1
    unsigned char recvbuf[1024] = {0};

    //产生数据包标识
    sendbuf[0] = rand() % 256;
    sendbuf[1] = rand() % 256;
    //主机名处理
    i = k = 0;
    j = 1;
    while(*name != 0)
    {
        if(*name == '.')
        {
            host[i] = k;
            k = 0;
            i = j;
            j++;
            name++;
        }else
        {
            host[j] = *name;
            j++;
            k++;
            name++;
        }
    }
    host[i] = k;
    k = j ;
    j = 0;
    i = 2;
    for(j=0;j<10;i++,j++)
    {
        sendbuf[i] = nd1[j];
    }
    for(j=0;j<k;i++,j++)
    {
        sendbuf[i] = host[j];
    }
    for(j=0;j<5;i++,j++)
    {
        sendbuf[i] = nd2[j];
    }
    if(show)
    {
        printf("发送数据包:\n");
        for(j=0;j<i;j++)
        {
            printf("%4d",sendbuf[j]);
        }
        printf("\n长度:%d\n",i);
    }
    sendto(sock, sendbuf, i, 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
    ret = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, NULL, NULL);
    if (ret == -1)
    {
        if (errno == EINTR)
        {
            printf("失败\n");
            close(sock);
            return;
        }
        ERR_EXIT("recvfrom");
    }
    //recvbuf[7]保存资源记录数,即IP个数
    j = recvbuf[7];
    k = j * 16 + i;
    printf("主机  %s  对应的%2d个IP地址如下:\n",s,recvbuf[7]);
    i += 12;
    while(j--)
    {
        printf("%3d.%3d.%3d.%3d\n",recvbuf[i],recvbuf[i+1],recvbuf[i+2],recvbuf[i+3]);
        i += 16;
    }
    if(show)
    {
        printf("接收数据包:\n");
        for(j=0;j<k;j++)
        {
            printf("%4d",recvbuf[j]);
        }
        printf("\n长度:%d\n",k);
    }
    close(sock);
}

int main(int argc,char* argv[])
{
    int sock;
    char *inputbuf;
    char show;
    //随机函数种子
    srand((int)time(0));
    if(argc != 2 && argc != 3)
    {
        printf("请使用 \"./udp_dns.cpp -s (hostname)\" 或者 \"./udp_dns.cpp -u (hostname)\" 方式运行,主机名可写可不写\n");
        return 0;
    }else{
        if(strcmp("-s",argv[1]) == 0){
            show = 1;
        }else if(strcmp("-u",argv[1]) == 0){
            show = 0;
        }else
        {
            printf("请使用 \"./udp_dns.cpp -s \" 或者 \"./udp_dns.cpp -u \" 方式运行\n");
            return 0;
        }
        if(argc == 3){
            inputbuf = argv[2];
        }else{
            inputbuf = (char *)malloc(40 * sizeof(char));
            printf("请输入要解析的主机名(eg:baidu.com):\n");
            scanf("%s",inputbuf);
        }
    }

    if ((sock = socket(PF_INET, SOCK_DGRAM, 0)) < 0){
        ERR_EXIT("socket");
    }
    ask_ip(sock,inputbuf,show);
    return 0;
}

程序实现了基本的DNS请求获得IP地址功能,同时加入-s(显示数据) -d(屏蔽数据)选项,可以方便地查看数据包。第三个主机名参数可有可无,没有的话会在程序运行时从键盘读入。
再最后,终于可以图穷匕首见了 xx—>
这里写图片描述
最后,欢迎大家在留言区评论,交流~


转载请注明出处

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
NB-IoT现阶段访问一个服务器只能通过IP地址加端口的方式,省去了DNS解析,如果产品以后的IP变化或者改变了服务端的IP地址,就需要一个DNS解析的功能。 通过DNS解析某个域名的IP地址获取IP地址后再进行通信。 如果NB-IoT模块只能绑定一个IP,则可以通过这个IP先获取其他IP列表,然后统一经过这个IP进行转发。 参考: 1.1.2 NB卡准备 模组使用的SIM卡为中国电信物联网专用NB卡,如下图正面(留意NB字样,无此字样的均为不合法的NB卡): 背面(留意其ICCID号,在让运营商开放IP白名单时可能需要此号码,相当于手机卡的手机号): 重要:收到卡后,需要致电背面的客服电话, 使其将您自己的IP地址加入访问白名单,此一步完成后,方能进行下面的步骤,切记切记 。 如何判定服务器IP已被加入访问白名单,使用如下两种方式: 1. AT+NPING 指令,通过PING自己的服务器地址,如返回ERROR,则多半(尚需继续排除防火墙因素)未就绪; 2. UDP通讯,如能与自己的服务器直接连接UDP通讯,那可以证明一定就绪; 1.1.3 服务器准备  首先您需要有一个固定IP的公网服务器,由于目前BC95暂不支持域名解析,故必须使用IP地址配置方式。  服务器可以使用阿里云服务器,目前本CoAP端暂未开源,有Windows 32位、Windows 64位、Linux CentOS 6、CentOS 7的可执行文件,请暂时选择以上指定系统;  CoAP标准协议使用 UDP 5683 端口,当然您也可以自定义此端口,必须让防火墙放通UDP 的指定端口;  CoAP网关需要使用WEB方式进行设备管理、用户管理等,默认使用 TCP 8080 端口,同 理,防火墙必须放通此端口; 1.1.4 模组准备 推荐使用 串口调试助手 sscom 来调试NB模组,如下,首先将您的NB模组上电使其启动,使用AT指令能收到OK的回复,证明已启动完毕,按如下步骤进行: 基础配置 1. 配置 NCDP 服务器,使用的指令序列为 AT+CFUN=0 +NCDP=103.37.149.19,5683 AT+NRB 留意 IP 地址必须为您自己指定的IP地址,如果暂时没有,也可以用 如上 地址临时使用(但 WEB 设备管理地址也需换成这个IP),完成后重启了设备; 2. 等待设备附着网络后,使用 AT+NPING=103.37.149.19 尝试PING自己的服务器,当返回ERROR时,极有可能是 IP 白名单未成功配置的缘故; 3. 使用 AT+CGSN=1 查询设备 IMEI 号,并将设备的 IMEI 注册到 WEB 平台,如果在上面操作 过,可忽略。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值