发新帖本帖赏金 120.00元(功能说明)我要提问
返回列表
打印
[APM32F4]

基于LWIP2.2:DNS的配置与使用(动态DHCP)

[复制链接]
1216|2
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
DKENNY|  楼主 | 2024-8-27 19:04 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 DKENNY 于 2024-8-27 18:59 编辑

#申请原创# @21小跑堂
前言:
    当我们在浏览器里输入一个网址(比如www.example.com)的时候,我们的电脑其实并不知道这个网址代表的是什么。计算机之间并不直接使用这些网址(域名)来互相交流,而是通过IP地址,这些地址是由一串数字组成的,比如 192.168.1.1 或者更复杂的 2607:f8b0:4005:805::200e 这样的数字和字母组合。
    但是,记住这些数字对我们人类来说很困难,因此我们更喜欢使用简单易记的域名。这时,DNS 就登场了。

1. DNS介绍
    DNS,全称是域名系统(Domain Name System)。简单来说,DNS 就像是互联网的电话簿。

1.1 DNS的工作原理
    当我们访问一个网站时,DNS 的工作大致可以分为以下几个步骤:
    1. 输入网址:在浏览器里输入www.example.com。
    2. 寻找IP地址:我们的电脑会向DNS服务器(这个服务器专门用来处理域名与IP地址之间的转换)发送一个请求,询问www.example.com的IP地址是什么。
    3. DNS服务器查询:DNS服务器会在自己的数据库中查找www.example.com对应的IP地址。如果找不到,它会向其他DNS服务器求助,最终找到并返回这个IP地址给你的电脑。
    4. 访问网站:一旦电脑拿到了IP地址,它就能用这个地址去找到并连接到www.example.com所在的服务器,并将网页内容显示在你的浏览器中。

1.2 DNS的功能
    总结一下,DNS 主要有以下几个功能:
    1. 域名解析:将我们输入的域名翻译成计算机能理解的IP地址。
    2. 分布式管理:全球有很多DNS服务器,它们分布在世界各地,分工协作来处理域名与IP地址的对应关系。这种分布式管理使得整个系统非常稳定和高效。
    3. 缓存:DNS服务器会缓存(暂时存储)常用域名和IP地址的对应关系,这样在下次查询时就不需要再从头开始查找,可以更快地返回结果。
    4. 负载均衡:有些大网站会有多个服务器来处理访问请求,DNS可以根据访问者的位置或服务器的负载情况,将用户引导到最合适的服务器上。

2. 关键API介绍:gethostbyname
err_t
dns_gethostbyname(const char *hostname, ip_addr_t *addr, dns_found_callback found,
                  void *callback_arg)
{
  return dns_gethostbyname_addrtype(hostname, addr, found, callback_arg, LWIP_DNS_ADDRTYPE_DEFAULT);
}

err_t
dns_gethostbyname_addrtype(const char *hostname, ip_addr_t *addr, dns_found_callback found,
                           void *callback_arg, u8_t dns_addrtype)
{
  size_t hostnamelen;
#if LWIP_DNS_SUPPORT_MDNS_QUERIES
  u8_t is_mdns;
#endif
  /* not initialized or no valid server yet, or invalid addr pointer
   * or invalid hostname or invalid hostname length */
  if ((addr == NULL) ||
      (!hostname) || (!hostname[0])) {
    return ERR_ARG;
  }
#if ((LWIP_DNS_SECURE & LWIP_DNS_SECURE_RAND_SRC_PORT) == 0)
  if (dns_pcbs[0] == NULL) {
    return ERR_ARG;
  }
#endif
  hostnamelen = strlen(hostname);
  if (hostname[hostnamelen - 1] == '.') {
    hostnamelen--;
  }
  if (hostnamelen >= DNS_MAX_NAME_LENGTH) {
    LWIP_DEBUGF(DNS_DEBUG, ("dns_gethostbyname: name too long to resolve\n"));
    return ERR_ARG;
  }


#if LWIP_HAVE_LOOPIF
  if (strcmp(hostname, "localhost") == 0) {
    ip_addr_set_loopback(LWIP_DNS_ADDRTYPE_IS_IPV6(dns_addrtype), addr);
    return ERR_OK;
  }
#endif /* LWIP_HAVE_LOOPIF */

  /* host name already in octet notation? set ip addr and return ERR_OK */
  if (ipaddr_aton(hostname, addr)) {
#if LWIP_IPV4 && LWIP_IPV6
    if ((IP_IS_V6(addr) && (dns_addrtype != LWIP_DNS_ADDRTYPE_IPV4)) ||
        (IP_IS_V4(addr) && (dns_addrtype != LWIP_DNS_ADDRTYPE_IPV6)))
#endif /* LWIP_IPV4 && LWIP_IPV6 */
    {
      return ERR_OK;
    }
  }
  /* already have this address cached? */
  if (dns_lookup(hostname, hostnamelen, addr LWIP_DNS_ADDRTYPE_ARG(dns_addrtype)) == ERR_OK) {
    return ERR_OK;
  }
#if LWIP_IPV4 && LWIP_IPV6
  if ((dns_addrtype == LWIP_DNS_ADDRTYPE_IPV4_IPV6) || (dns_addrtype == LWIP_DNS_ADDRTYPE_IPV6_IPV4)) {
    /* fallback to 2nd IP type and try again to lookup */
    u8_t fallback;
    if (dns_addrtype == LWIP_DNS_ADDRTYPE_IPV4_IPV6) {
      fallback = LWIP_DNS_ADDRTYPE_IPV6;
    } else {
      fallback = LWIP_DNS_ADDRTYPE_IPV4;
    }
    if (dns_lookup(hostname, hostnamelen, addr LWIP_DNS_ADDRTYPE_ARG(fallback)) == ERR_OK) {
      return ERR_OK;
    }
  }
#else /* LWIP_IPV4 && LWIP_IPV6 */
  LWIP_UNUSED_ARG(dns_addrtype);
#endif /* LWIP_IPV4 && LWIP_IPV6 */

#if LWIP_DNS_SUPPORT_MDNS_QUERIES
  if (strstr(hostname, ".local") == &hostname[hostnamelen] - 6) {
    is_mdns = 1;
  } else {
    is_mdns = 0;
  }

  if (!is_mdns)
#endif /* LWIP_DNS_SUPPORT_MDNS_QUERIES */
  {
    /* prevent calling found callback if no server is set, return error instead */
    if (ip_addr_isany_val(dns_servers[0])) {
      return ERR_VAL;
    }
  }

  /* queue query with specified callback */
  return dns_enqueue(hostname, hostnamelen, found, callback_arg LWIP_DNS_ADDRTYPE_ARG(dns_addrtype)
                     LWIP_DNS_ISMDNS_ARG(is_mdns));
}

    dns_gethostbyname和 dns_gethostbyname_addrtype是来自 lwIP(轻量级 IP)协议栈的函数。这些函数用于 DNS(域名系统)解析,将域名(如 "www.example.com")转换为 IP 地址。

2.1 函数概览
    1.dns_gethostbyname(const char *hostname, ip_addr_t *addr, dns_found_callback found, void *callback_arg)
    - 这个函数是 dns_gethostbyname_addrtype的封装器。它通过使用默认地址类型(LWIP_DNS_ADDRTYPE_DEFAULT)简化了 DNS 查找。
    2. dns_gethostbyname_addrtype(const char *hostname, ip_addr_t *addr, dns_found_callback found, void *callback_arg, u8_t dns_addrtype)
    - 这是处理 DNS 查询的核心函数。
    - 它允许用户指定要解析的地址类型(IPv4、IPv6 或两者之间的优先选择)。

2.2 函数逻辑分解
    1. 输入验证
    - 函数首先检查输入是否有效:
        - addr 不能为空。
        - hostname 必须非空且不能是空字符串。
        - 如果这些检查失败,函数返回 ERR_ARG。

    2. 主机名长度和格式验证
    - 函数计算 hostname 的长度。
    - 它还检查主机名是否以点(.)结尾,这通常表示完全限定域名(FQDN)。并相应调整长度。
    - 如果主机名长度超过允许的最大值(DNS_MAX_NAME_LENGTH),函数返回 ERR_ARG。

    3. 本地主机处理
    - 如果主机名是localhost,函数直接设置环回地址(对于 IPv4 为 127.0.0.1,对于 IPv6 为 ::1)并返回 ERR_OK。

    4. 直接 IP 地址处理
    - 函数检查主机名是否已经是字符串格式的 IP 地址,使用 ipaddr_aton 进行检查。
    - 如果它是有效的 IP 地址并且与请求的地址类型匹配,函数返回 ERR_OK。

    5. DNS 缓存查找
    - 然后,函数检查请求的主机名是否已被缓存,调用 dns_lookup进行查找。
    - 如果找到缓存地址,函数返回 ERR_OK。

    6. 双栈 IPv4/IPv6 处理
    - 如果函数被要求同时解析 IPv4 和 IPv6(使用 LWIP_DNS_ADDRTYPE_IPV4_IPV6 或 LWIP_DNS_ADDRTYPE_IPV6_IPV4),且第一次查找失败,它会尝试解析另一种地址类型。

    7. 多播 DNS (mDNS) 处理
    - 函数检查主机名是否以 ".local" 结尾,这是 mDNS 的典型标志。
    - 如果是 mDNS,函数会设置一个标志来适当处理它。

    8. DNS 服务器可用性
    - 在排队 DNS 查询之前,函数检查是否配置了任何 DNS 服务器。
    - 如果没有可用的服务器,函数返回 ERR_VAL。

    9. DNS 查询入队
    - 最后,函数通过调用 dns_enqueue 将 DNS 查询入队,该函数将处理发送 DNS 请求并在收到响应后调用回调函数。

    总结
    - dns_gethostbyname 是一个简化的 DNS 查找接口。
    - dns_gethostbyname_addrtype 提供了对 DNS 查找过程的更多控制,包括指定首选地址类型(IPv4 或 IPv6)的能力。
    - 两个函数都处理各种场景,包括本地地址解析(localhost)、直接 IP 地址、DNS 缓存、mDNS 和 IPv4 与 IPv6 之间的callback。
    - 这些函数设计用于在 lwIP 协议栈的约束条件下工作,该协议栈通常用于资源受限的环境,如嵌入式系统。

2.3 gethostbyname() 在 lwIP 中的原理描述:
    1. 输入主机名:
    - 函数接收一个要查询的主机名(hostname)。

    2. 检查缓存:
    - 检查是否在本地缓存中已存在该主机名的对应 IP 地址。
    - 如果找到,则直接返回缓存中的 IP 地址。

    3. 初始化 DNS 查询:
    - 如果缓存中没有找到,则初始化一个 DNS 查询。
    - 生成唯一的查询 ID。

    4. 发送 DNS 请求:
    - 构建 DNS 查询包并通过 UDP 协议发送到 DNS 服务器。

    5. 等待响应:
    - 等待来自 DNS 服务器的响应。

    6. 接收 DNS 响应:
    - 接收 DNS 服务器的响应包。

    7. 解析响应包:
    - 解析响应包中的数据,提取出对应的 IP 地址。

    8. 更新缓存:
    - 将新获取到的 IP 地址及其对应的主机名存入本地缓存,以便后续查询。

    9. 返回 IP 地址:
    - 将解析出的 IP 地址返回给调用者。

    10. 错误处理:
    - 如果在某个步骤中出现错误(如超时、无效响应等),则返回错误状态。

    可参考以下工作流程图。


3. 准备:
    1.APM32F407IG-Tiny。
    2.已经移植好的DHCP的例程(可去这篇帖子获取:https://bbs.21ic.com/icview-3362052-1-1.html)。
    3.一根TYPE-C的USB数据线,网口线。

4.  配置流程:
    打开lwipopts.h添加以下宏,开启DNS服务。


    关键代码:
void dns_test(void) 
{
    ip_addr_t addr;
    err_t err;
    uint8_t data[1000];

    if (usart_rx_flag_ok == 1)
    {
        memcpy(data, USART_RXBUF, USART_RXLEN);

        // 解析域名
        err = dns_gethostbyname((const char*)data, &addr, dns_found_callback_test, NULL);
        if (err == ERR_INPROGRESS)
        {
            // 正在处理
            printf("DNS resolution in progress...\r\n");
        }
        else if (err == ERR_OK)
        {
            // DNS解析成功
            printf("DNS resolved to: %s\r\n", ipaddr_ntoa(&addr));
        }
        else
        {
            printf("DNS resolution failed: %d\r\n", err);
        }

        usart_rx_flag_ok = 0;
        
        for (int i = 0; i < 1000; i++)
        {
            USART_RXBUF[i] = '\0';
        }
        USART_RXLEN = 0;
    }
   

}

void dns_found_callback_test(const char* name, const ip_addr_t* addr, void* arg)
{
    if (addr)
    {
        printf("DNS resolved: %s -> %s\r\n", name, ipaddr_ntoa(addr));
    }
    else
    {
        printf("DNS resolution failed for: %s\r\n", name);
    }
}

说明:
    1.dns_test()
    目的:
    dns_test()函数的设计目的是基于通过 USART(通用同步异步收发器)接收到的数据发起一个 DNS(域名系统)解析请求。该函数检查是否接收到有效的域名,如果是,它会尝试将该域名解析为 IP 地址。

    函数流程:
        - USART 数据处理:
            - 函数首先检查usart_rx_flag_ok是否设置为 1,表示已通过 USART 接收到一整行数据。
            - 如果标志已设置,则将接收到的数据(假设为域名)从 USART_RXBUF 复制到局部缓冲区 data 中。

        - DNS 请求:
            - 然后,函数调用 dns_gethostbyname()将存储在 `data` 中的域名解析为 IP 地址。这是一个异步函数,意味着它可能不会立即解析域名。
            - 函数检查 dns_gethostbyname() 的返回值(err):
            - 如果 err == ERR_INPROGRESS,表示 DNS 解析正在进行中,函数会打印一条消息指示这一点。
            - 如果 err == ERR_OK,表示 DNS 解析成功,函数会打印解析出的 IP 地址。
            - 如果出现其他错误(err != ERR_OK),则会打印失败消息。

        - 清理:
            - 在尝试解析域名后,函数会重置 USART 接收标志(usart_rx_flag_ok = 0)并清空 USART_RXBUF 缓冲区以接收新的数据。

    总结:
    dns_test() 充当根据通过 USART 接收到的用户输入触发 DNS 解析的处理程序。它尝试异步解析域名,并根据 DNS 系统的响应处理结果。

    2. dns_found_callback_test()
    目的:
    dns_found_callback_test() 函数是一个回调函数,在由 dns_test()启动的 DNS 解析过程完成时被调用。它处理 DNS 解析的最终结果,无论是成功还是失败。

    函数流程:
    - 参数检查:
        - 函数接收三个参数:
            - name:被解析的域名。
            - addr:解析出的 IP 地址。
            - arg:一个额外的用户参数,在此上下文中为 NULL。

   - DNS 解析结果:
        - 函数检查 addr 指针是否为 NULL。如果 addr 有效,则表示 DNS 解析成功。
            - 如果成功,函数会打印解析出的 IP 地址。
            - 如果 addr 为 NULL,则表示 DNS 解析失败,并打印相应的失败消息。

    总结:
    dns_found_callback_test()提供了有关 DNS 解析是否成功的反馈,处理域名解析过程中的最后一步。当 DNS 查询完成时,DNS 子系统会自动调用它。
总体交互:
    - dns_test() 在通过 USART 接收到域名时发起 DNS 查询。DNS 系统异步处理该查询。
    - 一旦 DNS 解析完成(无论是成功还是出错),dns_found_callback_test() 会被调用以处理并打印结果。

    通过这些函数,程序能够根据通过 USART 接收到的输入动态解析域名,从而为系统提供实时的 DNS 解析能力。

5. 实验现象:
    第一种现象:使用程序解析的ip地址与本机ping得到的ip地址一致。
    在串口输入要解析的域名地址,串口打印域名解析的ip地址。例如www.baidu.com,得到的ip地址为157.148.69.74。


    打开cmd,直接ping www.baidu.com,返回的ip地址同样为157.148.69.74。符合我们的最终效果。


    第二种现象:使用程序解析的ip地址与本机ping得到的ip地址不一样。
    例如,输入www.badu.com,程序得到的地址为157.148.69.80,而电脑直接ping这个域名,得到的是157.148.69.74。



    这时,我们如果用电脑直接ping 157.148.69.80,发现也是能ping通的。


    为什么同一个域名对应的ip地址不一样呢?
    一般出现这种情况,有以下4种原因。
    1. 负载均衡:大型网站通常使用负载均衡技术,将流量分散到多个服务器上。它们会将同一个域名解析到多个不同的IP地址,这样可以提高网站的性能和可靠性。假设我们访问一个大型购物网站,比如Amazon。Amazon可能会将其域名解析到多个不同的IP地址,比如203.0.113.1、203.0.113.2和203.0.113.3。这样,当我们访问Amazon时,我们可能会连接到不同的服务器,以分散流量并提高整体性能。

    2. DNS轮询:有些DNS服务器会按顺序返回多个关联到同一个域名的IP地址。每次查询时,它可能会返回不同的地址,实现轮询效果。这里以Netflix为例,当DNS服务器按顺序返回多个IP地址时,假设Netflix的域名解析到了三个IP地址:198.51.100.1、198.51.100.2和198.51.100.3。每次查询时,DNS服务器可能会按顺序返回这些IP地址,这样不同的用户可能会连接到不同的服务器。

    3. 地理位置:一些DNS服务商或内容分发网络(CDN)会根据用户的地理位置返回不同的IP地址,以便让用户连接到离他们更近的服务器,提高访问速度和减少延迟。关于地理位置,假设我们访问了一个某一个网站,根据我们的地理位置,比如在美国、欧洲或亚洲,DNS服务器可能会返回不同的IP地址,以便我们连接到距离最近的服务器,提高视频加载速度和减少缓冲等待时间。

    4. TTL(生存时间)设置:DNS记录中设置了TTL值,当记录过期后,DNS服务器会重新查询并返回不同的IP地址。这在负载均衡环境中比较常见。在TTL设置的情况下,假设某域名使用了TTL设置,将其域名解析到IP地址 192.0.2.1,但TTL值为60秒。一旦这个TTL过期了,DNS服务器会重新查询,并且可能会返回不同的IP地址,比如192.0.2.2,以便更新连接信息。
    所以,当我们在不同的时间查询同一个域名时,得到不同的IP地址是正常的。

6. 结语:
    总结来说,LWIP的DNS配置与使用在嵌入式系统中具有重要意义。通过正确配置DNS,可以实现设备与互联网的无缝连接,简化网络应用的开发与调试。
    以上就是本篇文章的全部内容,欢迎各位讨论交流。

附件:LWIP DNS例程
LWIP_DNS.zip (9.59 MB)



使用特权

评论回复

打赏榜单

21小跑堂 打赏了 120.00 元 2024-08-28
理由:恭喜通过原创审核!期待您更多的原创作品~

评论
forgot 2024-8-31 10:19 回复TA
赞 
21小跑堂 2024-8-28 10:17 回复TA
从巧解DNS的原理介绍、API的详细解读到APM32F407IG上实现DNS服务。整套流程紧密衔接,段落配置合理,文章排版舒适,实现效果较佳。 
发新帖 本帖赏金 120.00元(功能说明)我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

28

主题

51

帖子

5

粉丝