dns.c
dns_makequery(),第 412-462 行)4.2 非阻塞状态机(DNS_run(),第 502-581 行)4.3 超时判定(check_DNS_timeout(),第 473-489 行)4.4 应答解析4.5 与上层(ethernet.c)的时序配合5. 对外接口6. 依赖关系7. 备注与风险点源文件路径:
midware/ethernt/lan/dns.c所属模块:midware/ethernt/lan(W5500 以太网协议栈应用层 — DHCP/DNS/Socket) 代码行数:591
1. 文件职责
本文件是 WIZnet 官方 DNS 客户端库(W5500 版)的实现,负责把云端服务器域名(如 cloudiot.alphaess.com)翻译成 IPv4 地址:构造 DNS 查询报文、通过 W5500 的 UDP socket 发往 53 端口、接收并解析应答,最终把 A 记录写回调用者缓冲区。
在 EMS 的数据流里它处在"DHCP 已拿到 IP,但 TCP 客户端还不能建链"的中间环节:ethernet.c 的 dns_pro_fun() 用它的输出 g_stNetSysInfo.Domain_IP 去 connect();解析失败时 dns_pro_fun() 会依次轮换 8 个公共 DNS 服务器重试。
与官方原版最大的差别是本工程把 DNS_run() 改造成了分次调用的非阻塞状态机(新增 static u8 s_u8_flag,注释为中文"先是发送查询指令"/"转到数据接收状态"),因为原版是"发送后在函数内忙等应答"的阻塞实现,会阻塞 task_ethernet。文件由 task_ethernet(优先级 17,0.5 秒一次)驱动收发包,超时用的秒计数由 task_system_aux(优先级 8)每秒递增。
2. 关键数据结构与宏
| 名称 | 类型 | 说明 |
|---|---|---|
INITRTT | 宏 | 初始平滑往返时间 2000L(第 62 行);来自经典 BIND resolver 代码,本文件实际未使用 |
MAXCNAME | 宏 | CNAME 递归展开缓冲上限,值 MAX_DOMAIN_NAME + (MAX_DOMAIN_NAME>>1) = 168+84 = 252(第 63 行);parse_name()/dns_question()/dns_answer()/dns_makequery() 都在栈上开这么大的数组 |
TYPE_A … TYPE_TXT / TYPE_ANY | 宏 | DNS 资源记录类型:1 A、2 NS、5 CNAME、6 SOA、12 PTR、13 HINFO、15 MX、16 TXT、255 ANY 等(第 65-81 行) |
CLASS_IN | 宏 | ARPA Internet 类 1(第 83 行);组帧时写死为 1,未用到该宏 |
AGAIN / LAGAIN / DGAIN / LDGAIN | 宏 | RTT 平滑增益参数 8 / 3 / 4 / 2(第 86-89 行);随 INITRTT 一起是 BIND 残留,本文件未使用 |
dhdr | 结构体 | DNS 报文头(第 92-115 行)。字段:id、qr、opcode、aa、tc、rd、ra、rcode(各自带 #define 取值)、qdcount、ancount、nscount、arcount。注意该结构体只是"解析结果的容器",其内存布局与线上 12 字节报文头并不一致(qs/rr 按位拆成了多个字节字段),所以代码用 get16(&msg[0]) 逐字段手工解析,而不是把报文直接映射到结构体上 |
QUERY / RESPONSE | 宏 | dhdr.qr 取值 0/1(结构体内定义,第 96-97 行) |
IQUERY | 宏 | dhdr.opcode 取值 1(第 99 行) |
NO_ERROR / FORMAT_ERROR / SERVER_FAIL / NAME_ERROR / NOT_IMPL / REFUSED | 宏 | dhdr.rcode 取值 0~5(第 105-110 行) |
全局变量(第 118-122 行):
| 名称 | 类型 | 说明 |
|---|---|---|
pDNSMSG | uint8_t* | 指向外部传入的收发共用报文缓冲区(本工程为 g_u8DNSBuf,512 字节) |
DNS_SOCKET | uint8_t | DNS 使用的 W5500 socket 号,本工程为 SOCK_DNS=0 |
DNS_MSGID | uint16_t | DNS 事务 ID,每次组帧自增 |
dns_1s_tick | uint32_t | 超时用秒计数,由 DNS_time_handler() 递增 |
3. 函数清单
| 函数名 | 作用域 | 说明(一句话) |
|---|---|---|
get16 | 全局(第 125 行) | 从网络字节序缓冲读取一个 uint16_t |
put16 | 全局(第 134 行) | 把一个 uint16_t 以网络字节序写入缓冲,返回写后指针 |
parse_name | 全局(第 152 行) | 解析(含 0xC0 压缩指针间接寻址)的域名,展开为点分字符串并返回压缩字段总长度 |
dns_question | 全局(第 213 行) | 跳过 Question 段一条记录(域名 + type + class),返回下一条记录指针 |
dns_answer | 全局(第 239 行) | 跳过 Answer 段一条记录;若为 TYPE_A 则把 4 字节地址写入 ip_from_dns |
parseDNSMSG | 全局(第 334 行) | 解析完整 DNS 应答:还原 12 字节报文头到 dhdr,遍历 Question/Answer 段,按 rcode 返回 1/0/-1 |
dns_makequery | 全局(第 412 行) | 组 DNS 查询报文(报文头 + 域名点分段 + type/class),返回报文长度 |
check_DNS_timeout | 全局(第 473 行) | 基于 dns_1s_tick 与内部静态 retry_count 判断是否超时,返回 -1/0/1 |
DNS_init | 全局(第 494 行) | 绑定 socket、缓冲区与初始报文 ID |
DNS_run | 全局(第 502 行) | 非阻塞 DNS 状态机:状态 1 发查询、状态 2 收应答并解析 |
DNS_time_handler | 全局(第 585 行) | 秒计数 dns_1s_tick++ |
文件内没有 static 函数;check_DNS_timeout() 内含一个 static uint8_t retry_count,DNS_run() 内含一个 static u8 s_u8_flag。
4. 核心逻辑解析
4.1 报文格式与组帧(dns_makequery(),第 412-462 行)
输出报文(全部字段按网络字节序,put16 写入):
偏移 0-1 id = ++DNS_MSGID(初值 DNS_MSG_ID=0x1122)
偏移 2-3 标志 = (op << 11) | 0x0100 // op=0 → 0x0100,QR=0 查询、RD=1 期望递归
偏移 4-5 qdcount = 1
偏移 6-7 ancount = 0
偏移 8-9 nscount = 0
偏移 10-11 arcount = 0
偏移 12.. QNAME = 域名按 '.' 拆成 [len][label]... 形式,末尾 0x00
之后 2 字节 QTYPE = 0x0001 (TYPE_A)
之后 2 字节 QCLASS = 0x0001 (CLASS_IN)
域名的拆分段逻辑(第 435-456 行):先把 name 用 strcpy 拷到栈上的 sname[MAXCNAME],然后用 strchr 逐段找点,每段写"长度 + 内容",最后补 0x00 结束。返回值为 cp - buf,即整包长度。
注意函数参数 len(调用方传入 MAX_DNS_BUF_SIZE=512)在第 440 行被重新赋值为当前段的长度,原始上限从此丢失:整个函数既没有校验 strlen(name) < MAXCNAME,也没有校验输出是否超出 512,是典型的 BIND 老代码直译风格。
4.2 非阻塞状态机(DNS_run(),第 502-581 行)
static u8 s_u8_flag 只有两个取值,跨调用保持:
- case 1(发查询):
socket(DNS_SOCKET, Sn_MR_UDP, 0, 0)打开 UDP socket(本地端口 0 → 由socket.c的sock_any_port自动分配);dns_makequery()组包;sendto(..., IPPORT_DOMAIN);dns_1s_tick = 0;s_u8_flag = 2;返回2(处理中)。 - case 2(收应答):
getSn_RX_RSR()有数据时,把长度限制在MAX_DNS_BUF_SIZE内后recvfrom(),调用parseDNSMSG()解析(返回值直接作为DNS_run的返回值),close(DNS_SOCKET),s_u8_flag复位为 1。无数据时调用check_DNS_timeout():- 返回
< 0(重试次数用尽)→close()、s_u8_flag = 1、返回0(失败); - 返回
0(秒计时到但未到上限)→ 返回2(继续等); - 返回
1(计时未到)→ 返回2。
- 返回
- default:返回
0。
因此 DNS_run() 需要被反复调用才能完成一次解析,调用方每 0.5 秒调一次(ethernet.c:424 的 > 500 tick 闸门),单次解析的实际耗时约 DNS_WAIT_TIME(3 秒) ~ 3.5 秒。
4.3 超时判定(check_DNS_timeout(),第 473-489 行)
static uint8_t retry_count; // 函数内静态,跨调用保持
if (dns_1s_tick >= DNS_WAIT_TIME) { // 3 秒到
if (retry_count >= MAX_DNS_RETRY) { // 已重试 1 次
retry_count = 0;
return -1; // 判定超时
}
retry_count++;
return 0; // 计时到,但不算超时
}
return 1; // 计时未到
关键点:dns_1s_tick 只在 case 1 发查询时清零(第 528 行),超时分支不重置也不重发——MAX_DNS_RETRY = 1,而 retry_count 初值 0,语义上"允许 1 次额外等待 3 秒",但并没有真正重发一次查询。等效行为是:发包后大约 3 秒内没收到应答,下一次调用即判定失败(总等待时间约 3~3.5 秒)。这与头文件注释"max time = MAX_DNS_RETRY * DNS_WAIT_TIME"以及"Requery Count"的描述不一致。
4.4 应答解析
报文头还原(parseDNSMSG(),第 334-399 行):先把 dhdr 整体 memset 清零,再逐字段填:
id = get16(&msg[0])(注意:没有与DNS_MSGID比对,不校验应答是否属于本次查询);tmp = get16(&msg[2])后按位拆:bit15 →qr,(tmp>>11)&0xf→opcode,bit10/9/8/7 →aa/tc/rd/ra,低 4 位 →rcode;qdcount/ancount/nscount/arcount取偏移 4/6/8/10。
Question 段:循环 qdcount 次调用 dns_question(),每次 parse_name() 展开域名并 +2(type)+2(class)推进;parse_name() 返回 -1 则整体返回 -1。
Answer 段:循环 ancount 次调用 dns_answer():
TYPE_A(1):把紧随其后的 4 字节写入ip_from_dns(这就是最终结果);TYPE_CNAME/MB/MG/MR/NS/PTR:再次parse_name()展开并跳过;TYPE_HINFO(13):跳两段"长度+内容";TYPE_MX(15):跳过 2 字节 preference 后展开域名;TYPE_SOA(6):跳过两个域名 + 5 个 4 字节字段;TYPE_TXT/default:不再推进(default分支不前进指针,理论上可能造成相邻记录错位)。
最后按 rcode 返回:NO_ERROR → 1,其它 → 0。nscount 与 arcount 两个循环体内是空语句(第 386-395 行),只做计数占位,未真正解析授权段与附加段。
域名展开(parse_name(),第 152-203 行):处理 0xC0 压缩指针((slen & 0xc0) == 0xc0 时用 ((slen & 0x3f)<<8) + 下一字节 作偏移回跳,且只把首个指针算进 clen),逐段写出并以 . 连接,最后补 '\0';根名(nseg == 0)输出单个 .。缓冲区不足时(len < 0)返回 -1。
4.5 与上层(ethernet.c)的时序配合
ethernet.c 的 dns_pro_fun()(第 417-540 行)是唯一调用方:
- 每 500 tick(0.5 秒)执行一次;
- 只在
g_stNetSysInfo.u8DNSOk == FALSE时工作; - 用
s_u8_index % 8依次挑选 DNS 服务器:0 用 DHCP 下发的 DNS,1~7 依次为 Google 8.8.8.8、CloudFlare 1.1.1.1、Quad9 9.9.9.9、DNS.SB 185.222.222.222、阿里 223.5.5.5、DNSPod 119.29.29.29、Yandex 77.88.8.8(利用switchcase 的贯穿逐级填充); - 调
DNS_run(l_u8_dns, get_ethernet_server_point(), g_stNetSysInfo.Domain_IP):ret == 2直接返回继续等,ret == 1且结果 IP 非 0.0.0.0 时置u8DNSOk = TRUE,否则s_u8_index++换下一个 DNS; - 无线通道场景下不走本函数,直接把
Domain_IP定为 192.168.137.1(ethernet.c:523-527)。
秒计数 dns_1s_tick 由 ethernet.c:1087 在 task_system_aux 中每秒递增,与运行 DNS_run() 的 task_ethernet 不是同一任务,dns_1s_tick 无 volatile 修饰也无保护。
5. 对外接口
本文件导出的函数(声明见 dns.h):DNS_init、DNS_run、DNS_time_handler。
另外以下函数虽未在头文件声明,但因未加 static 而具有全局链接属性:get16、put16、parse_name、dns_question、dns_answer、parseDNSMSG、dns_makequery、check_DNS_timeout。
本文件导出的全局变量:pDNSMSG、DNS_SOCKET、DNS_MSGID、dns_1s_tick。
本文件调用的外部函数/变量:
- 来自
socket.c(经socket.h):socket()、close()、sendto()、recvfrom()、getSn_RX_RSR(); - 来自
w5500.h/wizchip_conf.h:Sn_MR_UDP等宏定义; - 来自 ANSI C 库:
<string.h>的strcpy、strchr、strncpy、strlen、memset(第 52 行 include),<stdlib.h>(第 53 行 include,本文件未实际使用其函数); - 类型
u8(第 509 行)来自includes.h(经w5500.h间接引入的 uCOS-II 环境); printf(仅_DNS_DEBUG_打开时)。
6. 依赖关系
- 依赖(include):
<string.h>、<stdlib.h>(第 52-53 行),socket.h、dns.h(第 55-56 行);打开_DNS_DEBUG_时还要<stdio.h>(第 59 行)。 - 依赖(间接):
socket.h→wizchip_conf.h→w5500.h→includes.h(提供u8与 uCOS-II 环境)。 - 被依赖:
midware/ethernt/ethernet.c(第 17 行 includedns.h,第 296/497/1087 行调用)。本工程内无其它调用者。
7. 备注与风险点
dns_makequery()的域名长度与越界风险(第 432 行strcpy(sname, name)):sname[MAXCNAME]只有 252 字节,name来自可配置的服务器域名,长度完全不受控;同时组帧输出也未与MAX_DNS_BUF_SIZE(512) 比较,超长域名会造成栈溢出或缓冲区溢出。头文件对此的说明是"调用者应保证域名 +1 ≤ 168",但没有代码强制。dns_makequery()的len形参被覆盖(第 440 行):头注释声明len是"缓冲区最大尺寸",实现中第一次循环就把它改成当前段长度,原语义丢失,属官方遗留写法,极易误用。parseDNSMSG()不校验报文的id与源 IP:任何到达 socket 0 的 UDP 报文(recvfrom取回的ip/port从未比对)都会被当作 DNS 应答解析,存在被伪造应答篡改解析结果的可能;对 EMS 这种长期在线的设备值得关注。parseDNSMSG()没有长度边界检查:dns_question()/dns_answer()依据报文内的qdcount/ancount与各记录的长度字段推进指针,若应答被截断(例如len > MAX_DNS_BUF_SIZE被截到 512,第 537-538 行)或ancount被伪造,可能越界读取缓冲区之后的内存。default分支不推进指针也会导致后续记录解析错位。- 超时重试的语义与文档不符:
MAX_DNS_RETRY = 1名义上是"重查询 1 次",实际实现中不会重发查询(dns_1s_tick不清零、s_u8_flag不回退到 1),只是多给 3 秒等待;dns.h的"max time = MAX_DNS_RETRY * DNS_WAIT_TIME"注释不准确。 check_DNS_timeout()的静态retry_count不会在解析成功时复位:成功路径(case 2 收到包)没有清它,但由于只在超时分支递增、在判定超时时清零,实践中影响有限;不过它与DNS_run()的s_u8_flag共同构成了两处跨调用隐藏状态,DNS_run()因此不可重入,同时只能有一个域名查询在途(本工程只有一条查询,暂无问题)。dns_1s_tick无volatile也无临界区:由task_system_aux每秒写、由task_ethernet读,编译器优化下存在读到陈旧值的理论风险(对比dhcp.c中同作用的dhcp_tick_1s已加volatile)。nscount/arcount循环体为空(第 386-395 行):授权段与附加段完全不解析,也在返回值判断里不起作用;这是官方为省代码量的裁剪,但会让阅读者误以为已处理。- 死代码与 BIND 残留:
INITRTT、AGAIN、LAGAIN、DGAIN、LDGAIN、CLASS_IN、TYPE_MD/MF/MB/MG/MR/NULL/WKS/PTR(部分仅在 switch 中被列举)以及<stdlib.h>的 include 都是未使用的残留;#ifdef _DNS_DEUBG_(第 369、379 行)拼写错误,且其块内的printf缺少右括号与分号,一旦该宏被定义将无法编译。 DNS_run()中socket()每次都执行(第 520 行,case 1):socket()内部会先close()再打开(socket.c:141),意味着每次重新发起解析都会重建 socket 0;socket 0 同时被约定为 DNS 专用,若其它模块误用会互相踢掉。getSn_RX_RSR()截断:if(len > MAX_DNS_BUF_SIZE) len = MAX_DNS_BUF_SIZE;之后recvfrom()只收 512 字节,而 W5500 的 RX 缓冲区里剩余数据会滞留在 socket 中;好在此后立即close(),影响被限制在单次查询内。