全部例程

DNS

W5500 其他标签

2025/02/17 更新

本篇文章,我们将详细介绍如何在W5500芯片上面实现DNS域名解析功能。并通过实战例程,为大家讲解如何将wiznet.io的域名解析为实际IP地址,供大家参考。

该例程用到的其他网络协议,例如DHCP,请参考相关章节。有关初始化过程,请参考Network Install章节,这里将不再赘述。

DNS协议简介

在学习DNS协议之前,我们先区分一下IP地址和域名这两个概念:

  • IP地址:一长串能够唯一地标记网络上地计算机的数字。
  • 域名:又称网域,是由一串用点分隔的名字组成的Internet上某一台计算机或计算机组的名称,用于在数据传输时对计算机的定位标识,例如:wiznet.io。

如何理解域名和网址的概念,可以这么理解,网址里面包含域名。举个例子:https://wiznet.io/Products就是一个网址,而wiznet.io就是域名。 因为 IP 地址具有不方便记忆并且不能显示地址组织的名称和性质等缺点,所以设计出了域名,并通过域名解析协议(DNS,Domain Name System)来将域名和 IP 地址相互映射,使人能够更方便地访问互联网,而不用去记住能够被机器直接读取的 IP 地址数串。将域名映射成 IP 地址称为DNS正向解析,将 IP 地址映射成域名称为DNS反向解析。

DNS协议可以使用UDP或者TCP进行传输,使用的端口号都为53,但大多数情况下DNS都是用UDP进行传输。

以上是DNS协议的简介,如想深入了解该协议,请参考mozilla网站上的介绍: DNS - MDN Web 文档术语表:Web 相关术语的定义 | MDN

DNS域名介绍

DNS域名通常分为以下几类:

  • 根域名服务器:根域名服务器是DNS系统的顶层,负责管理整个DNS命名空间的根区(Root Zone)。它主要用于引导查询,指向顶级域(TLD)的权威服务器。
  • 顶级域名服务器:负责特定顶级域(如.com、.org、.net)或国家/地区代码顶级域(ccTLD,如.cn、.uk)的解析。
  • 权威DNS服务器:负责存储并提供特定域名的DNS记录信息。
  • 本地DNS服务器:本地域名服务器是电脑解析时的默认域名服务器,即电脑中设置的首选 DNS 服务器和备选 DNS 服务器。常见的有电信、联通、谷歌、阿里等的本地 DNS 服务。

DNS查询方式

DNS查询方式分为以下两种:

  • 递归查询指由DNS客户端(如用户设备或本地域名服务器)向DNS服务器发起的查询请求,DNS服务器负责全程完成查询过程,并将最终的解析结果返回给客户端。
  • 迭代查询指DNS服务器返回给客户端或请求者的下一步建议,而不是直接返回最终结果,由客户端自行完成多次查询,逐步获取解析结果。

下面两张图则是递归查询和迭代查询的工作流程图。

Blog Image
Blog Image

DNS协议的基本工作流程

  1. 首先搜索「浏览器的 DNS 缓存」,缓存中维护一张域名与 IP 地址的对应表;
  2. 若没有命中,则继续搜索「操作系统的 DNS 缓存」
  3. 若仍然没有命中,则操作系统将域名发送至「本地域名服务器」,本地域名服务器查询自己的 DNS缓存,查找成功则返回结果;

    请注意:

    主机和本地域名服务器之间的查询方式是「递归查询」

  4. 若本地域名服务器的 DNS缓存没有命中,则本地域名服务器向上级域名服务器进行查询,通过以下方式进行「迭代查询」

    请注意:

    本地域名服务器和其他域名服务器之间的查询方式是「迭代查询」,防止根域名服务器压力过大

    • 首先本地域名服务器向「根域名服务器」发起请求,根域名服务器是最高层次的,它并不会直接指明这个域名对应的 IP 地址,而是返回顶级域名服务器的地址,也就是说给本地域名服务器指明一条道路,让他去这里寻找答案。
    • 本地域名服务器拿到这个「顶级域名服务器」的地址后,就向其发起请求,获取「权限域名服务器」的地址
    • 本地域名服务器根据权限域名服务器的地址向其发起请求,最终得到该域名对应的 IP 地址
    • 嵌入式Web服务器:一些嵌入式设备内置Web服务器(例如路由器、网关、传感器设备等),通过TCP协议提供网页接口给用户进行配置和监控。
  5. 本地域名服务器将得到的 IP 地址返回给操作系统,同时自己将 IP 地址缓存起来;
  6. 操作系统将 IP 地址返回给浏览器,同时自己也将 IP 地址缓存起来;
  7. 至此,浏览器就得到了域名对应的 IP 地址,并将 IP 地址缓存起来;

上文配合下图更直观理解DNS工作过程。

Blog Image

在W5500上使用DNS正向解析wiznet.io域名时,我们只需要向本地域名服务器发送DNS请求报文,然后解析DNS响应报文即可。

DNS报文

DNS报文分为以下五个部分:

  • 报文头部:定义了请求或响应的元信息(如标志、条目数等)。
  • 问题区域:描述了查询的域名和查询类型。
  • 回答区域:包含查询的最终结果(如域名对应的IP地址)。
  • 权威区域:提供权威DNS服务器的信息。
  • 附加区域:包含附加的相关信息(如域名的A记录)。


DNS请求报文主要由报文头部和问题区域组成,回答区域、权威区域和附加区域为空。

  1. 报文头部
    • Transaction ID:固定长度为16bit,唯一标识符,用于匹配请求和响应。
    • Flags:固定长度为16bit,标志位(例如查询类型、递归期望等)。
    • Questions:固定长度为16bit,问题区域的条目数,通常为1。
    • Answer RRs:固定长度为16bit,回答区域的条目数,查询报文中为0。
    • Authority RRs:固定长度为16bit,权威区域的条目数,查询报文中为0。
    • Additional RRs:固定长度为16bit,附加区域的条目数,查询报文中为0。
  2. 问题区域
    • QName:查询的域名(以点分形式存储)。
    • QType:查询的记录类型(如A记录、AAAA记录、MX记录等)。
    • QClass:查询的记录类别,通常为IN(互联网)。

DNS响应报文包含与请求报文类似的头部和问题区域,并附加回答、权威和附加区域信息。

  1. 报文头部:同请求报文,但Flags内容有所变化。
    • QR:1表示响应(查询报文中为0)
    • RCODE:返回码,表示响应状态(如0表示无错误,3表示域名不存在)。
    • AA:权威回答标志(1表示这是权威服务器返回的响应)。
  2. 问题区域:与请求报文一致,用于描述客户端的查询。
  3. 回答区域:包含查询结果,如域名对应的IP地址。每条回答包含以下字段
    • Name对应的域名
    • Type:记录类型(如A、AAAA、CNAME等)。
    • Class:记录类别(通常为IN)。
    • TTL:记录的生存时间(秒)。
    • Rdata:记录的具体值(如IP地址)。
  4. 权威区域:提供权威服务器的信息,通常包含NS记录。
  5. 附加区域:包含额外的解析信息,如权威服务器的A记录和AAAA记录。


请求报文实例:请求解析域名wiznet.io的A记录

| 报文原文 |
8D 12 01 00 00 01 00 00 00 00 00 00
06 77 69 7A 6E 65 74 02 69 64 00 00 01 00 01

| 报文解析 |
Transaction ID: 0x8D12
Flags: 0x0100 (标准查询、期望递归)
Questions: 1
Answer RRs: 0
Authority RRs: 0
Additional RRs: 0
| 问题区域 |
QName:wiznet.io
QType: A
QClass: IN

响应报文实例:DNS服务器返回wiznet.io的A记录解析结果(IP为183.111.138.249)

| 报文原文 |
8D 12 81 80 00 01 00 01 00 00 00 00
06 77 69 7A 6E 65 74 02 69 6F 00 00 01 00 01
C0 0C 00 01 00 01 00 00 00 9C 00 04 B7 6F 8A F9

| 报文解析 |
Transaction ID: 0x8D12
Flags: 0x8180 (响应、无错误)
Questions: 1
Answer RRs: 1
Authority RRs: 0
Additional RRs: 0
| 问题区域 |
QName:wiznet.io
QType: A
QClass: IN
| 回答区域 |
Name:wiznet.io
Type: A
Class: IN
TTL: 156
RData: 183.111.138.249

实现过程

接下来,我们看看如何在W5500上实现DNS正向解析。

步骤1:注册DNS定时器中断到1s定时器中


void TIM2_IRQHandler(void)
{
    static uint32_t wiz_timer_1ms_count = 0;
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
    {
        wiz_timer_1ms_count++;
        if (wiz_timer_1ms_count >= 1000)
        {
            DHCP_time_handler();
            DNS_time_handler();
            wiz_timer_1ms_count = 0;
        }
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    }
}

注册DNS定时器中断主要为了DNS超时处理。

在dns.h文件中,定义了DNS超时时间、重试次数、端口号和消息ID等内容:


#define	MAX_DNS_BUF_SIZE	256		///< maximum size of DNS buffer. */
/*
  * @brief Maxium length of your queried Domain name 
  * @todo SHOULD BE defined it equal as or greater than your Domain name lenght + null character(1)
  * @note SHOULD BE careful to stack overflow because it is allocated 1.5 times as MAX_DOMAIN_NAME in stack.
  */
#define  MAX_DOMAIN_NAME   128       // for example "www.google.com"

#define	MAX_DNS_RETRY     2        ///< Requery Count
#define	DNS_WAIT_TIME     3        ///< Wait response time. unit 1s.

#define	IPPORT_DOMAIN     53       ///< DNS server port number

#define DNS_MSG_ID         0x1122   ///< ID for DNS message. You can be modifyed it any number

步骤2:进行DNS正向解析处理

在do_dns()函数中,我们实现了dns正向解析的过程。


do_dns(ethernet_buf, dns_name, ip_fromdns);

这个函数的三个传参分别为DNS解析所需缓存,带解析域名,解析后的IP地址。

do_dns()函数的内容如下:

/**
* @brief   DNS domain name resolution
* @param   ethernet_buff: ethernet buffer
* @param   domain_name:Domain name to be resolved
* @param   domain_ip:Resolved Internet Protocol Address
* @return  0:success;-1:failed
*/
int do_dns(uint8_t *buf, uint8_t *domain_name, uint8_t *domain_ip)
{
    int         dns_ok_flag  = 0;
    int         dns_run_flag = 1;
    wiz_NetInfo net_info;
    uint8_t     dns_retry_cnt = 0;
    DNS_init(0, buf); // DNS client init
    wizchip_getnetinfo(&net_info);
    while (1)
    {
        switch (DNS_run(net_info.dns, domain_name, domain_ip)) // Read the DNS_run return value
        {
        case DNS_RET_FAIL:                                     // The DNS domain name is successfully resolved
        {
            if (dns_retry_cnt < DNS_RETRY)                     // Determine whether the parsing is successful or whether the parsing exceeds the number of times
            {
                dns_retry_cnt++;
            }
            else
            {
                printf("> DNS Failed\r\n");
                dns_ok_flag  = -1;
                dns_run_flag = 0;
            }
            break;
        }
        case DNS_RET_SUCCESS: {
            printf("> Translated %s to %d.%d.%d.%d\r\n", domain_name, domain_ip[0], domain_ip[1], domain_ip[2], domain_ip[3]);
            dns_ok_flag  = 0;
            dns_run_flag = 0;
            break;
        }
        }
        if (dns_run_flag != 1)
        {
            return dns_ok_flag;
        }
    }
}

首先会调用DNS_init()函数初始化DNS配置:


/* DNS CLIENT INIT */
void DNS_init(uint8_t s, uint8_t * buf)
{
	DNS_SOCKET = s; // SOCK_DNS
	pDNSMSG = buf; // User's shared buffer
	DNS_MSGID = DNS_MSG_ID;
}

然后是在DNS主循环中运行DNS执行函数DNS_run,它的主要作用是进行DNS组包,发送请求,响应内容解析以及超时处理,这里只需要根据DNS_run()函数的返回值进行相应处理即可。

DNS_run()函数内容如下:

 /* DNS CLIENT RUN */
int8_t DNS_run(uint8_t * dns_ip, uint8_t * name, uint8_t * ip_from_dns)
{
  int8_t ret;
  struct dhdr dhp;
  uint8_t ip[4];
  uint16_t len, port;
  int8_t ret_check_timeout;

  retry_count = 0;
  dns_1s_tick = 0;

  // Socket open
  socket(DNS_SOCKET, Sn_MR_UDP, 0, 0);

#ifdef _DNS_DEBUG_
  printf("> DNS Query to DNS Server : %d.%d.%d.%d\r\n", dns_ip[0], dns_ip[1], dns_ip[2], dns_ip[3]);
#endif

  len = dns_makequery(0, (char *)name, pDNSMSG, MAX_DNS_BUF_SIZE);
  sendto(DNS_SOCKET, pDNSMSG, len, dns_ip, IPPORT_DOMAIN);

  while (1)
  {
    if ((len = getSn_RX_RSR(DNS_SOCKET)) > 0)
    {
      if (len > MAX_DNS_BUF_SIZE) len = MAX_DNS_BUF_SIZE;
      len = recvfrom(DNS_SOCKET, pDNSMSG, len, ip, &port);
      #ifdef _DNS_DEBUG_
        printf("> Receive DNS message from %d.%d.%d.%d(%d). len = %d\r\n", ip[0], ip[1], ip[2], 
        ip[3],port,len);
      #endif
        ret = parseDNSMSG(&dhp, pDNSMSG, ip_from_dns);
      break;
    }
    // Check Timeout
    ret_check_timeout = check_DNS_timeout();
    if (ret_check_timeout < 0) {

#ifdef _DNS_DEBUG_
  printf("> DNS Server is not responding : %d.%d.%d.%d\r\n", dns_ip[0], dns_ip[1], dns_ip[2], dns_ip[3]);
#endif
      
close(DNS_SOCKET);
      return 0; // timeout occurred
    }
    else if (ret_check_timeout == 0) {

#ifdef _DNS_DEBUG_
      printf("> DNS Timeout\r\n");
#endif
      sendto(DNS_SOCKET, pDNSMSG, len, dns_ip, IPPORT_DOMAIN);
    }
  }
  close(DNS_SOCKET);
  // Return value
  // 0 > :  failed / 1 - success
  return ret;
}

运行结果

请注意:

因为本示例需要访问互联网,请确保W5500的配置能够访问互联网。

烧录例程运行后,首先进行了PHY链路检测,然后是DHCP获取网络地址结果,最后是DNS成功解析出wiznet.io的IP地址为183.111.138.249,如下图所示:

Blog Image

总结

本文介绍在 W5500 芯片上实现 DNS 域名解析功能的方法,讲解如何将 wiznet.io 域名解析为实际 IP 地址。阐述了 DNS 协议概念、域名分类、查询方式和工作流程,介绍了 DNS 报文结构及请求、响应报文实例等。展示在W5500上的实现过程。

下一篇将讲解在该芯片上实现 HTTP Client 功能,介绍向指定网站提交数据的原理和实现步骤。敬请期待!

下载本章例程

我们提供完整的工程文件以及配套开发板,方便你随时测试,快速完成产品开发:

开发环境: Keil MDK5 配套开发板