当前位置:网站首页>C语言实现DNS请求器

C语言实现DNS请求器

2022-06-24 19:19:00 qq_42120843

C语言实现DNS请求器

项目介绍

本程序完成给定指定域名请求对应ip地址的功能,类似于win和linux下nslookup的功能

前置知识

DNS介绍

域名系统(英文:Domain Name System ,缩写 DNS )是互联网的一项服务。它作为将域名和 IP 地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。 DNS 使用 TCP 和 UDP 端口 53 。当前,对于每一级域名长度的限制是 63 个字符,域名总长度则不能超过 253 个字符。域名系统(英文:Domain Name System ,缩写 DNS 的作用是将人类可读的域名如, www.example.com) 转换为机器可读的 IP 地址 如, 192.0.2.44) 。

DNS的分层

域名系统是分层次的。
在域名系统的层次结构中,各种域名都隶属于域名系统根域的下级。域名的第一级是顶级域,它包括通用顶级域,例如 .com 、 .net 和 .org ;以及国家和地区顶级域,例如 .us 、 .cn 和 .tk 。顶级域名下一层是二级域名,一级一级地往下。这些域名向人们提供注册服务,人们可以用它创建公开的互联网资源或运行网站。顶级域名的管理服务由对应的 域名注册 管理机构(域名注册局)负责,注册服务通常由域名注册商负责。

在这里插入图片描述

域名解析

主机名到IP 地址的映射有两种方式:
静态映射 在本机上配置域名和 IP 的映射,旨在本机上使用。 Windows 和 Linux的 hosts 文件中的内容就属于静态映射。

动态映射 建立一套域名解析系统( DNS ),只在专门的 DNS 服务器上配置主机到IP 地址的映射,网络上需要使用主机名通信的设备,首先需要到 DNS 服务器查询主机所对应的 IP 地址。通过域名去查询域名服务器,得到IP 地址的过程叫做域名解析。在解析域名时,一般先静态域名解析,再动态解析域名。可以将一些常用的域名放入静态域名解析表中,这样可以大大提高域名解析效率。

递归查询和迭代查询

  • 递归查询:本机向本地域名服务器发出一次查询请求,就静待最终的结果。如果本地域名服务器无法解析,自己会以 DNS 客户机的身份向其它域名服务器查询,直到得到最终的 IP 地址告诉本机。
  • 迭代查询:本地域名服务器向根域名服务器查询,**根域名服务器告诉它下一步到哪里去查询,然后它再去查,**每次它都是以客户机的 身份去各个服务器查询

DNS协议报文格式

在这里插入图片描述

头部(Header)

在这里插入图片描述

Queries(查询问题区域)

在这里插入图片描述

域名( 2 字节或不定长): 它的格式和 Queries 区域的查询名字字段是一样的。有一点
不同就是,当报文中域名重复出现的时候,该字段使用 2 个字节的偏移指针来表示。比
如,在资源记录中,域名通常是查询问题部分的域名的重复,因此用 2 字节的指针来表
示,具体格式是最前面的两个高位是 11(0xC0),用于识别指针。其余的 14 位从 DNS 报文的开
始处计数(从 0 开始),指出该报文中的相应字节数。一个典型的例子,
C00C(11 00 000000001100, 12 (字节)正好是头部的长度,其正好指向 Queries 区域的查询名字字段 。
查询类型(Type): 表明资源纪录的类型.
查询类(Class): 对于 Internet 信息,总是 IN
生存时间( TTL 以秒为单位,表示的是资源记录的生命周期,一般用于当地址解析程
序取出资源记录后决定保存及使用缓存数据的时间,它同时也可以表明该资源记录的稳
定程度,极为稳定的信息会 被分配一个很大的值(比如 86400 ,这是一天的秒数)。
资源数据: 该字段是一个可变长字段,表示按照查询段的要求返回的相关资源记录的数
据。**可以是 Address (表明查询报文想要的回应是一个 IP 地址)**或者 CNAME (表明查询
**报文想要的回应是一个规范主机名)**等。

wireshark 分析DNS请求和响应报文

在这里插入图片描述

在这里插入图片描述

技术要点

  1. 使用传输层的UDP Socket进行编程
  2. 应用层的DNS报文格式

程序执行流程

在这里插入图片描述

程序设计流程

  1. DNS请求头以及正文部分的结构体定义
  2. DNS header 数据填充
  3. DNS question 数据填充
  4. 合并DNS header和question部分(build_requestion)
  5. 通过UDP Socket 发送请求报文和接受响应报文
  6. 解析出响应报文的相关数据

代码剖析

DNS header和 question部分结构体实现

对照DNS报文格式进行实现即可

//dns报文Header部分数据结构
struct dns_header{
    
    unsigned short id; //2字节(16位)
    unsigned short flags; 

    unsigned short questions; //问题数
    unsigned short answer; //回答数

    unsigned short authority;
    unsigned short additional;
};

//dns报文Queries部分的数据结构
struct dns_question{
    
    int length; //主机名的长度,自己添加的,便于操作
    unsigned short qtype;
    unsigned short qclass;
    //查询名是长度和域名组合
    //如www.0voice.com ==> 60voice3com0
    //之所以这么做是因为域名查询一般是树结构查询,com顶级域,0voice二级域
    unsigned char *name; // 主机名(长度不确定)
};

//dns响应报文中数据(域名和ip)的结构体
struct dns_item{
    
    char *domain; 
    char *ip;
};

DNS header数据填充

我们只填充3个字段:Transaction IDFlagsQuestions.其中会话标识随机生成,标志和问题数要进行主机字节序到网络字节序的转换:

//将header部分字段填充数据
int dns_create_header(struct dns_header *header)
{
    
    if(header == NULL)
        return -1;
    memset(header, 0x00, sizeof(struct dns_header));

    //id用随机数,种子用time(NULL),表明生成随机数的范围
    srandom(time(NULL)); // 线程不安全
    header->id = random();
    
    //网络字节序(大端):地址低位存数据高位;主机字节序则与之相反
    //主机(host)字节序转网络(net)字节序
    header->flags = htons(0x0100);
    header->questions = htons(1);
    return 0;
}

DNS Queries部分数据的填充

对照报文中Queries的部分进行填充

int dns_create_question(struct dns_question *question, const char *hostname)
{
    
    if(question == NULL || hostname == NULL)
        return -1;
    memset(question, 0x00, sizeof(struct dns_question));

    //内存空间长度:hostname长度 + 结尾\0 再多给一个空间
    question->name = (char *)malloc(strlen(hostname) + 2);
    if(question->name == NULL)
    {
    
        return -2;
    }

    question->length = strlen(hostname) + 2;

    //查询类型1表示获得IPv4地址
    question->qtype = htons(1);
    //查询类1表示Internet数据
    question->qclass = htons(1);

    //【重要步骤】
    //名字存储:www.0voice.com -> 3www60voice3com 
    const char delim[2] = ".";
    char *qname = question->name; //用于填充内容用的指针

    //strdup先开辟大小与hostname同的内存,然后将hostname的字符拷贝到开辟的内存上
    char *hostname_dup = strdup(hostname); //复制字符串,调用malloc
    //将按照delim分割出字符串数组,返回第一个字符串
    char *token = strtok(hostname_dup, delim);

    while(token != NULL)
    {
    
        //strlen返回字符串长度不含'\0'
        size_t len = strlen(token);

        *qname = len;//长度的ASCII码
        qname++;

        //将token所指的字符串复制到qname所指的内存上,最多复制len + 1长度 
        //len+1使得最后一个字符串把\0复制进去
        strncpy(qname, token, len + 1);
        qname += len;

        //固定写法,此时内部会获得下一个分割出的字符串返回(strtok会依赖上一次运行的结果)
        token = strtok(NULL, delim); //依赖上一次的结果,线程不安全
    }

    free(hostname_dup);       
}

合并DNS header和question部分

将header部分数据和question部分数据合并到字符数组中去。

//将header和question合并到request中
//header [in]
//question [in]
//request [out]
//rlen:代表request的大小
int dns_build_requestion(struct dns_header *header, struct dns_question *question, char *request, int rlen)
{
    
    if (header == NULL || question == NULL || request == NULL)
        return -1;

    memset(request, 0, rlen);

    //header -> request
    memcpy(request, header, sizeof(struct dns_header));
    int offset = sizeof(struct dns_header);

    //Queries部分字段写入到request中,question->length是question->name的长度
    memcpy(request + offset, question->name, question->length);
    offset += question->length;

    memcpy(request + offset, &question->qclass, sizeof(question->qclass));
    offset += sizeof(question->qclass);

    memcpy(request + offset, &question->qtype, sizeof(question->qtype));
    offset += sizeof(question->qtype);

    return offset; //返回request数据的实际长度
}

UDP Socket编程发送DNS请求和接收DNS响应

UDP Socket编程的基本流程和函数调用顺序基本相同,

代码流程:

  1. 创建UDP Socket
  2. 填充服务器地址结构体的数据struct sockaddr_in
  3. 连接以保证尽可能的可靠(connect
  4. DNS报文数据填充 (dns_create_header、dns_build_requestion)
  5. 通过socket发送DNS请求报文(sendto)
  6. 接受DNS响应报文(recvfrom)
  7. 解析响应报文
int dns_client_commit(const char *domain)
{
    
    //下方流程是基本定死的套路
    //1.创建UDP socket
    //网络层ipv4, 传输层用udp
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
    
        return -1;
    }

    //2.结构体填充数据
    struct sockaddr_in servaddr;
    bzero(&servaddr, sizeof(servaddr)); //将结构体数组清空
    servaddr.sin_family = AF_INET; 
    servaddr.sin_port = htons(DNS_SERVER_PORT);
    //点分十进制地址转为网络所用的二进制数 替换inet_pton
    //servaddr.sin_addr.s_addr = inet_addr(DNS_SERVER_IP);
    inet_pton(AF_INET, DNS_SERVER_IP, &servaddr.sin_addr.s_addr);

    //UDP不一定要connect,只是这样提高成功发送请求的可能性
    connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));


    //3.dns报文的数据填充
    struct dns_header header = {
    0};
    dns_create_header(&header);

    struct dns_question question = {
    0};

    dns_create_question(&question, domain);

    char request[1024] = {
    0};
    int len = dns_build_requestion(&header, &question, request, 1024);

    //4.通过sockfd发送DNS请求报文
    int slen = sendto(sockfd, request, len, 0, (struct sockaddr *)&servaddr, sizeof(struct sockaddr));

    char response[1024] = {
    0};
    struct sockaddr_in addr;
    size_t addr_len = sizeof(struct sockaddr_in);

    //5.接受DNS服务器的响应报文
    //addr和addr_len是输出参数
    int n = recvfrom(sockfd, response, sizeof(response), 0, (struct sockaddr *)&addr, (socklen_t *)&addr_len);

    struct dns_item *dns_domain = NULL;
    //6.解析响应
    dns_parse_response(response, &dns_domain);

    free(dns_domain);
    
    return n; //返回接受到的响应报文的长度
}

解析出响应报文的相关数据

解析响应的函数

//struct dns_item** 是因为当struct dns_item *为NULL时需要改变其值
static int dns_parse_response(char *buffer, struct dns_item **domains)
{
    
    int i = 0;
    unsigned char *ptr = buffer;

    ptr += 4; // ptr向前4字节,指向Questions(问题数)字段开头
    int querys = ntohs(*(unsigned short *)ptr);

    ptr += 2; //ptr向前2字节,指向Answer RR回答数开头
    int answers = ntohs(*(unsigned short *)ptr); //一个域名可能对应多个ip

    ptr += 6; //ptr向前6字节,指向Queries(查询问题区域)Name字段的开头

    for(i = 0;i < querys; i++)
    {
    
        //如查询的网址为www.0voice,则Name = 3www60voice3com0 
        while(1)
        {
    
            //flag就是随后字符串的长度
            int flag = (int)ptr[0];
            ptr += (flag + 1); 

            if(flag == 0)   break;
        }
        ptr += 4; //指向下一个查询名Name字段的开头
        //最后一次循环是跳过Type和Class字段
    }

    char cname[128], aname[128], ip[20], netip[4];
    int len, type, ttl, datalen;

    int cnt = 0;
    //动态分配内存的数组
    //分配answers个dns_item的内存,并全部置为0,返回指向一个位置的指针
    struct dns_item *list = calloc(answers, sizeof(struct dns_item));
    if(list == NULL)
    {
    
        return -1;
    }

    //解析出Answers(回答区域)的内容
    for(int i = 0;i < answers;++i)
    {
    
        bzero(aname, sizeof(aname));
        len = 0;
		
        //从buffer中ptr指向的位置解析出域名到aname中,并将长度写入len中
        dns_parse_name(buffer, ptr, aname, &len);
        ptr += 2; //???

        type = htons(*(unsigned short *)ptr);
        ptr += 4;

        ttl = htons(*(unsigned short *)ptr);
        ptr += 4;

        datalen = ntohs(*(unsigned short *)ptr);
        ptr += 2;

        if(type == DNS_CNAME)
        {
    
            bzero(cname, sizeof(cname));
            len = 0;
            从buffer的ptr位置开始解析出内容到cname中,len用来接受解析出的内容长度
            dns_parse_name(buffer, ptr, cname, &len);
            ptr += datalen;
        }
        else if(type == DNS_HOST)
        {
    
            bzero(ip, sizeof(ip));
            
            //ipv4为4字节
            if(datalen == 4)
            {
    
                memcpy(netip, ptr, datalen);
                //二进制网络字节序netip转为点分十进制地址存到ip
                inet_ntop(AF_INET, netip, ip, sizeof(struct sockaddr));
                
                printf("%s has address %s\n", aname, ip);
                printf("\t Time to live : %d minutes, %d seconds\n",ttl / 60, ttl % 60);

                list[cnt].domain = (char *)calloc(strlen(aname) + 1, 1);
                memcpy(list[cnt].domain, aname, strlen(aname));

                list[cnt].ip = (char *)calloc(strlen(ip) + 1, 1);
                memcpy(list[cnt].ip, ip, strlen(ip));

                cnt++;
            }

            ptr += datalen;
        }
    }
    *domains = list;
    ptr += 2; //????

    return cnt;
}

解析出域名的函数

域名( 2 字节或不定长): 它的格式和 Queries 区域的查询名字字段是一样的。有一点
不同就是,当报文中域名重复出现的时候,该字段使用 2 个字节的偏移指针来表示。比
如,在资源记录中,域名通常是查询问题部分的域名的重复,因此用 2 字节的指针来表
示,具体格式是最前面的两个高位是 11(0xC0),用于识别指针。其余的 14 位从 DNS 报文的开
始处计数(从 0 开始),指出该报文中的相应字节数。一个典型的例子,
C00C(11 00 000000001100, 12 (字节)正好是头部的长度,其正好指向 Queries 区域的查询名字字段 。

第一次出现某个Name(域名)

在这里插入图片描述

第二次出现相同的域名,只占两个字节(C0 0C = 1100 0000 0000 1100),高2位表示是指针,低14位表示这个域名第一次出现的偏移(12字节)

在这里插入图片描述

//从chunk的ptr指向的位置开始解析名字,长度写入len
static void dns_parse_name(unsigned char* chunk, unsigned char *ptr, char *out, int *len)
{
    
    int flag = 0, n = 0, alen = 0;
    //pos指向的内存用于存储解析得到的结果
    char *pos = out + (*len); // 传入的 *len = 0

    while(1)
    {
    
        flag = (int)ptr[0]; 
        if(flag == 0) break;

        //如果为指针表明该Name重复出现过,这一字段只占2字节
        if(is_pointer(flag))
        {
    
            n = (int)ptr[1]; //获取第一次Name出现的偏移
            ptr = chunk + n;
            dns_parse_name(chunk, ptr, out, len);
            break;
        }
        else //不是指针,表明是第一次出现Name的地方,此时flag表示随后字符串长度
        {
    
            ptr++;
            memcpy(pos, ptr, flag);
            pos += flag;
            ptr += flag;

            *len += flag;
            if((int)ptr[0] != 0)
            {
    
                memcpy(pos, ".", 1);
                pos += 1;
                (*len) += 1;
            }
        }
    }
    
}

static int is_pointer(int in)
{
    
    //0xC0 : 1100 0000
    return ((in & 0xC0) == 0xC0);    
}

完整源代码

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

#define DNS_SERVER_PORT 53
#define DNS_SERVER_IP "114.114.114.114"

#define DNS_HOST 0x01
#define DNS_CNAME 0x05


//dns报文Header部分数据结构
struct dns_header{
    
    unsigned short id; //2字节(16位)
    unsigned short flags; 

    unsigned short questions; //问题数
    unsigned short answer; //回答数

    unsigned short authority;
    unsigned short additional;
};

//dns报文Queries部分的数据结构
struct dns_question{
    
    int length; //主机名的长度,自己添加的,便于操作
    unsigned short qtype;
    unsigned short qclass;
    //查询名是长度和域名组合
    //如www.0voice.com ==> 60voice3com0
    //之所以这么做是因为域名查询一般是树结构查询,com顶级域,0voice二级域
    unsigned char *name; // 主机名(长度不确定)
};

//dns响应报文中数据(域名和ip)的结构体
struct dns_item{
    
    char *domain; 
    char *ip;
};



//将header部分字段填充数据
int dns_create_header(struct dns_header *header)
{
    
    if(header == NULL)
        return -1;
    memset(header, 0x00, sizeof(struct dns_header));

    //id用随机数,种子用time(NULL),表明生成随机数的范围
    srandom(time(NULL)); // 线程不安全
    header->id = random();
    
    //网络字节序(大端)地址低位存数据高位
    //主机(host)字节序转网络(net)字节序
    header->flags = htons(0x0100);
    header->questions = htons(1);
    return 0;
}


//将Queries部分的字段填充数据
int dns_create_question(struct dns_question *question, const char *hostname)
{
    
    if(question == NULL || hostname == NULL)
        return -1;
    memset(question, 0x00, sizeof(struct dns_question));

    //内存空间长度:hostname长度 + 结尾\0 再多给一个空间
    question->name = (char *)malloc(strlen(hostname) + 2);
    if(question->name == NULL)
    {
    
        return -2;
    }

    question->length = strlen(hostname) + 2;

    //查询类型1表示获得IPv4地址
    question->qtype = htons(1);
    //查询类1表示Internet数据
    question->qclass = htons(1);

    //【重要步骤】
    //名字存储:www.0voice.com -> 3www60voice3com 
    const char delim[2] = ".";
    char *qname = question->name; //用于填充内容用的指针

    //strdup先开辟大小与hostname同的内存,然后将hostname的字符拷贝到开辟的内存上
    char *hostname_dup = strdup(hostname); //复制字符串,调用malloc
    //将按照delim分割出字符串数组,返回第一个字符串
    char *token = strtok(hostname_dup, delim);

    while(token != NULL)
    {
    
        //strlen返回字符串长度不含'\0'
        size_t len = strlen(token);

        *qname = len;//长度的ASCII码
        qname++;

        //将token所指的字符串复制到qname所指的内存上,最多复制len + 1长度 
        //len+1使得最后一个字符串把\0复制进去
        strncpy(qname, token, len + 1);
        qname += len;

        //固定写法,此时内部会获得下一个分割出的字符串返回(strtok会依赖上一次运行的结果)
        token = strtok(NULL, delim); //依赖上一次的结果,线程不安全
    }

    free(hostname_dup);       
}


//将header和question合并到request中
//request是传入传出参数
int dns_build_requestion(struct dns_header *header, struct dns_question *question, char *request, int rlen)
{
    
    if (header == NULL || question == NULL || request == NULL)
        return -1;

    memset(request, 0, rlen);

    //header -> request
    memcpy(request, header, sizeof(struct dns_header));
    int offset = sizeof(struct dns_header);

    //Queries部分字段写入到request中,question->length是question->name的长度
    memcpy(request + offset, question->name, question->length);
    offset += question->length;

    memcpy(request + offset, &question->qclass, sizeof(question->qclass));
    offset += sizeof(question->qclass);

    memcpy(request + offset, &question->qtype, sizeof(question->qtype));
    offset += sizeof(question->qtype);

    return offset; //返回request数据的实际长度
}

static int is_pointer(int in)
{
    
    //0xC0 : 1100 0000
    return ((in & 0xC0) == 0xC0);    
}

//从chunk的ptr指向的位置开始解析名字,长度写入len
static void dns_parse_name(unsigned char* chunk, unsigned char *ptr, char *out, int *len)
{
    
    int flag = 0, n = 0, alen = 0;
    //pos指向的内存用于存储解析得到的结果
    char *pos = out + (*len); // 传入的 *len = 0

    //???
    while(1)
    {
    
        flag = (int)ptr[0]; // ???
        if(flag == 0) break;

        if(is_pointer(flag))
        {
    
            n = (int)ptr[1];
            ptr = chunk + n;
            dns_parse_name(chunk, ptr, out, len);
            break;
        }
        else //不是指针,表明是第一次出现Name的地方
        {
    
            ptr++;
            memcpy(pos, ptr, flag);
            pos += flag;
            ptr += flag;

            *len += flag;
            if((int)ptr[0] != 0)
            {
    
                memcpy(pos, ".", 1);
                pos += 1;
                (*len) += 1;
            }
        }
    }
    
}

//解析响应
//struct dns_item** 是因为当struct dns_item *为NULL时需要改变其值
static int dns_parse_response(char *buffer, struct dns_item **domains)
{
    
    int i = 0;
    unsigned char *ptr = buffer;

    ptr += 4; // ptr向前4字节,指向Questions(问题数)字段开头
    int querys = ntohs(*(unsigned short *)ptr);

    ptr += 2; //ptr向前2字节,指向Answer RR回答数开头
    int answers = ntohs(*(unsigned short *)ptr); //一个域名可能对应多个ip

    ptr += 6; //ptr向前6字节,指向Queries(查询问题区域)Name字段的开头

    for(i = 0;i < querys; i++)
    {
    
        //如查询的网址为www.0voice,则Name = 3www60voice3com0 
        while(1)
        {
    
            int flag = (int)ptr[0];
            ptr += (flag + 1); //???

            if(flag == 0)   break;
        }
        ptr += 4; //指向下一个查询名Name字段的开头或跳过Type和Class字段
    }

    char cname[128], aname[128], ip[20], netip[4];
    int len, type, ttl, datalen;

    int cnt = 0;
    //动态分配内存的数组
    //分配answers个dns_item的内存,并全部置为0,返回指向一个位置的指针
    struct dns_item *list = calloc(answers, sizeof(struct dns_item));
    if(list == NULL)
    {
    
        return -1;
    }

    for(int i = 0;i < answers;++i)
    {
    
        bzero(aname, sizeof(aname));
        len = 0;

        //解析出域名
        dns_parse_name(buffer, ptr, aname, &len);
        ptr += 2;

        type = htons(*(unsigned short *)ptr);
        ptr += 4;

        ttl = htons(*(unsigned short *)ptr);
        ptr += 4;

        datalen = ntohs(*(unsigned short *)ptr);
        ptr += 2;

        if(type == DNS_CNAME)
        {
    
            bzero(cname, sizeof(cname));
            len = 0;
            //猜:从buffer的ptr位置开始解析出内容到cname中,len用来接受解析出的内容长度
            dns_parse_name(buffer, ptr, cname, &len);
            ptr += datalen;
        }
        else if(type == DNS_HOST)
        {
    
            bzero(ip, sizeof(ip));
            
            //ipv4为4字节
            if(datalen == 4)
            {
    
                memcpy(netip, ptr, datalen);
                //二进制网络字节序netip转为点分十进制地址存到ip
                inet_ntop(AF_INET, netip, ip, sizeof(struct sockaddr));
                
                printf("%s has address %s\n", aname, ip);
                printf("\t Time to live : %d minutes, %d seconds\n",ttl / 60, ttl % 60);

                list[cnt].domain = (char *)calloc(strlen(aname) + 1, 1);
                memcpy(list[cnt].domain, aname, strlen(aname));

                list[cnt].ip = (char *)calloc(strlen(ip) + 1, 1);
                memcpy(list[cnt].ip, ip, strlen(ip));

                cnt++;
            }

            ptr += datalen;
        }
    }
    *domains = list;
    ptr += 2; // 经测试这行不加也行

    return cnt;
}

//客户端向dns服务器发送请求
int dns_client_commit(const char *domain)
{
    
    //下方流程是基本定死的套路
    //1.创建UDP socket
    //网络层ipv4, 传输层用udp
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
    
        return -1;
    }

    //2.结构体填充数据
    struct sockaddr_in servaddr;
    bzero(&servaddr, sizeof(servaddr)); //将结构体数组清空
    servaddr.sin_family = AF_INET; 
    servaddr.sin_port = htons(DNS_SERVER_PORT);
    //点分十进制地址转为网络所用的二进制数 替换inet_pton
    //servaddr.sin_addr.s_addr = inet_addr(DNS_SERVER_IP);
    inet_pton(AF_INET, DNS_SERVER_IP, &servaddr.sin_addr.s_addr);

    //UDP不一定要connect,只是这样提高成功发送请求的可能性
    connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));


    //3.dns报文的数据填充
    struct dns_header header = {
    0};
    dns_create_header(&header);

    struct dns_question question = {
    0};

    dns_create_question(&question, domain);

    char request[1024] = {
    0};
    int len = dns_build_requestion(&header, &question, request, 1024);

    //4.通过sockfd发送DNS请求报文
    int slen = sendto(sockfd, request, len, 0, (struct sockaddr *)&servaddr, sizeof(struct sockaddr));

    char response[1024] = {
    0};
    struct sockaddr_in addr;
    size_t addr_len = sizeof(struct sockaddr_in);

    //5.接受DNS服务器的响应报文
    //addr和addr_len是输出参数
    int n = recvfrom(sockfd, response, sizeof(response), 0, (struct sockaddr *)&addr, (socklen_t *)&addr_len);

    struct dns_item *dns_domain = NULL;
    //6.解析响应
    dns_parse_response(response, &dns_domain);

    free(dns_domain);
    
    return n; //返回接受到的响应报文的长度
}

int main(int argc, char *argv[])
{
    
    if(argc < 2) return -1;
    dns_client_commit(argv[1]);
    return 0;
}

编译和执行

在这里插入图片描述

问题

  1. dns_parse_response函数中下面部分

    for(int i = 0;i < answers;++i)
        {
          
            bzero(aname, sizeof(aname));
            len = 0;
    
            //解析出域名
            dns_parse_name(buffer, ptr, aname, &len);
            ptr += 2;// ???????为什么ptr指针只要移动2个字节,而不是len,aname难道不是可变的吗,只有跳过aname实际长度才会到Type字段。
    
原网站

版权声明
本文为[qq_42120843]所创,转载请带上原文链接,感谢
https://blog.csdn.net/qq_42120843/article/details/125416951