资讯动态

BUPT计网实验:从零实现DNS服务器与报文解析工程包详解

发布时间:2026/10/6 14:49:03 来源:尧图企业网站定制
简介面向北邮大二下学期计网课程设计一份DNS服务器实验压缩包覆盖域名系统层次结构、记录类型、查询过程与服务器实现可作为计算机网络课程学生课设参考或实验入门。包内共4个文件包括两个txt文本说明、一个C语言头文件和一个C语言源文件整体仅9KB结构精简便于快速阅读和改动。已有166人学习下载。源码集中反映基于套接字编程的DNS查询或中继思路txt文件可分别对照A记录映射与请求转发配置递归/迭代查询、权威与缓存服务器等知识点在调试验证中都能得到具体印证。整体是一份小巧而全面的实践素材既能帮助读者把域名解析原理落到代码层面也适合作为复习网络编程和网络服务管理的备查资料为复现域名解析服务提供了清晰路径。1. 课程设计实战BUPT 计网实验里的 DNS 服务器与它的完整工程包如果你也在做计网课程设计大概率会拿到一个和自己搭建 DNS 服务器有关的任务——不是让你配 BIND而是让你从零实现一个能解析域名的服务器。BUPT 大二下这个 DNS 服务器实验压缩包里面就是整套可直接运行的工程源码、Makefile、测试脚本外加一份写好的实验报告。拿到手第一件事不是看代码而是先弄清楚这份 zip 解压后应该长什么样、怎么编译、怎么验证否则很容易在环境上翻车。这份资源适合两类人一是正在做计网课设、需要参考一份能跑通的 DNS 服务器实现的学生二是想弄清 DNS 报文解析与递归查询完整链路的从业者。它不像教科书那样只讲概念而是直接给你一份能编译、能响应 dig 查询的工程踩坑记录和参数调优都能在里面找到对应答案。2. 从报文到链路DNS 服务器实验的两块硬骨头2.1 报文头只有 12 个字节解析与构造先过这一关DNS 协议本身不复杂复杂的是二进制格式的解析。所有查询和应答都基于同一个报文结构头部固定 12 字节后面跟问题段、回答段、权威段和附加段。实验中 90% 的 bug 都出在这 12 个字节上。typedef struct { uint16_t id; // 事务ID用来匹配请求和响应 uint16_t flags; // 标志位QR/Opcode/AA/TC/RD/RA/Z/RCODE uint16_t qdcount; // 问题数通常为 1 uint16_t ancount; // 回答数解析成功后为 1 uint16_t nscount; // 权威记录数 uint16_t arcount; // 附加记录数 } dns_header_t;这段结构体定义了 DNS 报文头部的固定部分。四个uint16_t加起来正好 12 字节解析时直接把这个结构体指针强转到收到的缓冲区首地址即可。注意 id 字段没有校验的话很容易被 DNS 欺骗攻击但课程设计一般不要求防御评分更看重字段是否构造正确。flags 字段是整个报文最核心的部分。QR 位标识是查询还是响应1 表示响应Opcode 占 4 位标准查询为 0AA 表示权威应答如果缓存命中后返回需要置 0RCODE 是响应码0 表示无错误3 表示 NXDOMAIN域名不存在。很多同学在构造响应时只填了 ID 和回答记录把 RCODE 忘了置 0导致 dig 拿到响应后报 SERVFAIL。问题段的解析比头部绕一点。QNAME 是一连串长度前缀的标签序列比如www.example.com会被编码成03 77 77 77 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00最后一个00表示域名结束。解析时不能用 strcpy 或 strlen必须逐字节读取长度前缀然后跳过对应长度的字节。提示解析 QNAME 时最容易踩的是指针压缩compression pointer。DNS 响应中域名会以0xC0开头表示指针低 14 位是偏移。如果只做递归查询不做压缩解析第一次跑通能过但一旦测试用例里出现压缩指针就会翻车。2.2 递归查询链路拿到一个域名后代码到底在做什么课程设计里最常见的实现方式是递归查询也就是客户端发来一个域名你的服务器替它去完整走一遍全球 DNS 树。完整链路是这样的客户端把域名发到你的服务器监听 UDP 53 端口你的服务器先查本地缓存没有就向根域名服务器198.41.0.4发起迭代查询根服务器返回.com顶级域的地址再向顶级域查拿到example.com权威服务器的地址最后向权威服务器拿到具体记录返回给客户端。#define ROOT_SERVER 198.41.0.4 // 根服务器A地址 #define DNS_PORT 53 // 标准DNS端口 #define BUFFER_SIZE 1024 // 单包缓冲区实际响应可能更大 #define TIMEOUT_SEC 3 // 超时重传阈值这段参数定义决定了实验的基础行为。端口可以改成非标端口方便本机测试但注意如果不加-p参数dig 默认只会查 53 端口。缓冲区建议直接设成 2048 或 4096不要省——UDP 下 DNS 最大报文可以达到 4096 字节EDNS0默认 512 字节的老实现遇到 TXT 记录会直接截断。迭代查询的每一步都是一次独立的 UDP 通信构造查询报文发往当前层 DNS 服务器等待响应并解析从回答段或权威段中取出下一跳地址重复这个过程。你要维护一个循环当前查询的域名 当前要发往的服务器地址 重试计数。最多走 30 条链如果超过这个深度还没结果基本就是配置了诡异的 CNAME 链。缓存的实现也比较直接用哈希表把域名记录类型映射到{回答数据, TTL, 时间戳}。每次收到查询先查缓存命中就直接构造应答返回没命中才走递归。TTL 在响应报文的回答段里有只有 60 秒的可以缓存一天的要慎重。课程设计里缓存是加分项不是必选项——如果你时间不够先把递归链路跑通再谈缓存。实验的评分通常是这样的本地解析www.bupt.edu.cn能返回正确 IP、解析不存在的域名返回 NXDOMAIN、连续查询同一个域名第二次走缓存返回且时间显著变短。这三条对应的是协议解析、递归查询和缓存三个模块缺一不可。3. 把源码跑起来工程结构、编译步骤与验证命令3.1 压缩包里的文件清单与代码组织这个项目压缩包解压后典型的结构是源码、构建脚本、测试用例和实验报告四类文件。拿到手先别急着编译把文件结构过一遍确认哪些是必须的、哪些是参考用的。文件路径作用是否必读src/dns_server.c主程序UDP socket 监听与事件循环是src/dns_packet.c报文解析与构造QNAME 编码/解码是src/dns_cache.c缓存表实现TTL 过期处理视评分要求src/dns_resolve.c递归查询链路根服务器配置是Makefile编译脚本是test/dig_domains.txt测试域名列表覆盖 A/CNAME/NXDOMAIN建议docs/实验报告.md实验原理、流程、测试截图参考编译之前先打开Makefile看一眼编译参数。一般会用-g -Wall如果项目里隐藏了-Werror把所有警告当错误那代码里任何未使用的变量都会直接编译失败。遇到这种情况别慌挨个把警告处理掉或者干脆把-Werror从 Makefile 里注释掉——课程设计不会因为你删了这个就扣分。3.2 编译与启动三步让 DNS 服务在 53 端口工作# 第一步编译整个工程 make clean make # 第二步以 root 权限启动因为 53 端口是特权端口 # 如果你用的是非 root 用户需要先 sudo sudo ./dns_server -p 53 -t 3 # 第三步另开终端用 dig 发起本地查询 dig 127.0.0.1 www.bupt.edu.cn noall answer第一行的make clean是个好习惯避免旧的目标文件残留导致链接奇怪的符号错误。第二行启动参数的-p指定监听端口-t是超时秒数。如果你只想快速测试不想动系统 53 端口把端口改成 5353但 dig 要记得加-p 5353。最后一步的noall answer是 dig 的参数意思是只输出回答段的记录内容不打印查询统计和授权信息。能看到www.bupt.edu.cn对应的 A 记录说明你的服务器已经成功完成了一次递归查询如果返回 SERVFAIL 或超时大概率是根服务器的可达性问题或报文解析 bug。启动日志在调试时很有用。代码实现得比较完整的工程会打印[INFO] query: www.bupt.edu.cn type A from 127.0.0.1这样一行这一步能告诉你服务器确实收到了请求问题出在链路后半段而不是 socket 层。3.3 用 dig 验证解析从本机回环到外部域名的完整测试# 测试1正常解析 A 记录 dig 127.0.0.1 www.baidu.com short # 测试2解析不存在的域名期望返回 NXDOMAIN dig 127.0.0.1 none-exist-domain-test01.bupt.edu.cn noall comments | grep status # 测试3连续查询同一个域名第二次观察响应时间 time dig 127.0.0.1 www.bupt.edu.cn noall answer time dig 127.0.0.1 www.bupt.edu.cn noall answer # 测试4测试 CNAME 链注意看回答段是否包含两条记录 dig 127.0.0.1 www.microsoft.com noall answer第一条命令如果输出一个纯 IP 地址说明 A 记录查询成功了。short模式只显示 IP 不显示完整报文适合快速确认。第二条命令中grep status的作用是从带注释的输出里抽出状态行正确的响应应该是status: NXDOMAIN如果你看到SERVFAIL说明你的服务器把“域名不存在”和“查询失败”搞混了——这是两个完全不同的 RCODE。第三条命令的两次time很有参考价值。第一次如果走了外部递归耗时通常在 50~200 毫秒左右甚至更高第二次如果走了缓存应该降到 1 毫秒以内。两次时间差距明显说明缓存模块工作正常如果两次时间几乎相同去看缓存代码里的 TTL 判断逻辑多半是取当前时间的方式不对。第四条测试考的是 CNAME 链的处理。www.microsoft.com会先返回一个指向某 CDN 域名的 CNAME你的服务器必须把 CNAME 和最终 A 记录都放进回答段客户端才能正常解析。如果你只返回了第一条 CNAME 就结束dig 会显示解析失败。这里也是最容易踩的坑之一很多实现只处理了 QNAME 精确匹配没处理 CNAME 链的追加查询。提示测试前最好先清一次系统 DNS 缓存。macOS 用sudo dscacheutil -flushcacheLinux 视发行版用sudo systemd-resolve --flush-caches或重启。不然你可能查到的其实是系统缓存的历史结果白白浪费半小时排查。4. 避坑调试 DNS 服务器实验的常见问题与排查路径4.1Address already in use53 端口被系统服务占用了现象启动./dns_server报bind: Address already in use或者程序能启动但 dig 一直超时。检查后发现系统里systemd-resolved或dnsmasq也在监听 53 端口。原因现代 Linux 发行版默认启用了本地 DNS 解析缓存服务它们抢先绑定了 53 端口。两个进程绑同一个端口内核只让第一个成功你的程序自然起不来。解决三步走。先sudo lsof -i :53或ss -ulpn | grep 53看是哪个进程占着如果是systemd-resolved执行sudo systemctl stop systemd-resolved但注意这会影响系统的域名解析谨慎操作更省事的做法是让你的实验服务器监听 5353 端口测试时 dig 加-p 5353系统服务互不干扰。我一般直接用第二种课程设计关键在于协议实现不需要非占着 53 不可。4.2 dig 返回 SERVFAIL 但日志显示收到请求现象服务器日志打印了查询信息但客户端收到status: SERVFAIL。服务器端看起来一切正常处于一种“已收到、未正确处理”的状态。原因SERVFAIL 是响应码 2通常表示服务器在解析过程中出了问题。最常见的是向上级 DNS 发迭代请求时超时或者收到的响应包解析失败。由于 UDP 没有可靠传输丢包时如果程序没有重试逻辑直接返回 SERVFAIL 是最容易出现的错误。很多同学的实现只发一次请求等不到响应就直接放弃。解决检查递归循环里是否有超时重试机制。我一般会设置超时 800 毫秒重试 3 次每次重试前重新构造查询报文——因为 socket 是同一个但事务 ID 应该保持一致。如果还是失败在关键节点打日志根服务器是否能 ping 通、198.41.0.4的 53 端口 UDP 是否可达用nc -u -z -w3 198.41.0.4 53测一下。如果 UDP 连根服务器都不通多半是校园网防火墙限制换个网络环境测试。4.3 zip 解压后文件乱码且源码注释全变成问号现象压缩包在 Windows 下双击解压或右键解压后实验报告.md里的中文变成了乱码源码文件里的中文注释显示为错乱字符但代码本体能编译通过。原因zip 内的文件名和文本编码用的是 UTF-8而 Windows 自带解压工具尤其是中文版默认使用本地代码页GBK去解码文件名导致乱码。文件内容本身必须用 UTF-8 打开但大多数文本编辑器在 GBK 环境下会用错编码读取。解决推荐两个办法。一是安装 7-Zip解压时选择“以 UTF-8 编码解压文件名”二是用 Python 直接解压强制指定编码import zipfile import pathlib src pathlib.Path(BUPT计网_DNS实验.zip) dst pathlib.Path(dns_server_lab) dst.mkdir(exist_okTrue) with zipfile.ZipFile(src) as zf: for info in zf.infolist(): # 重新按 UTF-8 解码文件名修复乱码 name info.filename.encode(cp437).decode(utf-8) target dst / name if info.is_dir(): target.mkdir(parentsTrue, exist_okTrue) else: target.parent.mkdir(parentsTrue, exist_okTrue) target.write_bytes(zf.read(info))这段脚本的核心是encode(cp437).decode(utf-8)——zip 内部的文件名编码被 Windows 解压器破坏后先用 CP437 恢复原始字节再按 UTF-8 解码就能还原。源码文件打开后如果还是乱码把编辑器编码切换到 UTF-8 即可。这个坑跟 DNS 协议无关但是能让你的实验进度直接白费两小时别问我怎么知道的。4.4 解析成功但 TTL0缓存模块形同虚设现象缓存功能实现后连续两次查询的时间几乎没差别或者第二次查询虽然命中缓存但 TTL 显示 0、立刻过期。原因应答报文的回答段里TTL 字段是 4 字节无符号整数单位是秒。很多实现从报文里解析出 TTL 后直接作为绝对时间戳存了没做“当前时间 TTL”的相对计算还有的用time(NULL)做比较但没#include time.h编译器默认隐式声明导致返回值错误。解决缓存存储时应该记录两个值——遇到该记录时的time(NULL)和 TTL 秒数。查询时用time(NULL) - 存储时间 ttl判断是否过期。另一个容易踩的细节是 TTL 在缓存期间应该递减因为 DNS 的 TTL 是绝对时间客户端拿到后也会自己做一次倒计时。如果你的 HTTPDNS 类项目要求动态更新 TTL这里最容易出逻辑错。4.5 压缩指针处理不当导致解析到一半就炸现象测试某个具体域名时第一次成功、第二次失败或者某些域名永远解析不了但另一些域名一切正常。你甚至开始怀疑是玄学问题其实这是固定的报文格式 bug。原因DNS 响应里的域名会做指针压缩以节省空间。指针结构是0xC0 2 字节偏移指向报文内某个偏移量处的域名。如果解析时遇到0xC0但不按偏移跳转而是把它当成普通长度前缀继续读就会把剩余字节全部读错位。第一次成功的原因可能是这个特定响应恰好没用压缩指针第二次失败是因为换了请求场景响应结构变了。解决拿到响应后解析 QNAME 和回答段中的域名时都要处理指针。正确逻辑是遇到0xC0开头时取低 14 位作为偏移回溯解析并设置一个标志跳过剩余的压缩部分同时加一个跳转次数限制防止指针环导致死循环。写入递归查询时也别忘了把上一跳的响应缓存起来后续遇到同一域名避免重复解析。5. 进阶抓包验证、缓存调优与答辩时的技术自信如果你想让实验从“能跑”变成“能讲”最值得做的一件事是抓包验证。用 tcpdump 或 Wireshark 抓取 UDP 53 端口的完整交互过程能让你直观看到自己的服务器向谁发了请求、从谁那里收到了响应sudo tcpdump -i any -n -vv port 53 -w dns_lab.pcap # 另开终端执行 dig 127.0.0.1 www.baidu.com抓完用 Wireshark 打开重点观察三个时间点客户端到你的服务器的查询包、你的服务器向根服务器/权威服务器发出的迭代查询、以及最终带回答的响应包。如果你的实现走了完整三次迭代抓包里能看到至少三个不同 IP 的 DNS 请求。答辩时直接展示这张抓包图比讲十分钟原理更有说服力。缓存调优也是能加分的小细节。在缓存表里加入 LRU 淘汰策略当缓存条目超过 2048 条时优先淘汰过期条目中最近最少访问的。判题时如果跑批量域名解析这个策略能明显减少内存占用同时提高命中率。实现方案在dns_cache.c里用双向链表加哈希表代码量不大但体现系统设计能力。另外注意检查迭代查询时遇到 CNAME 链是否按顺序写入了缓存——先缓存 CNAME再缓存最终 A 记录两次查询返回的记录类型不同。如果你还有余力考虑补一个 TCP Fallback 的开关。DNS 标准规定 UDP 响应如果超过 512 字节或 EDNS0 协商值客户端会改用 TCP 53 端口重发查询你的服务器需要监听 TCP socket 并支持同样的解析逻辑。课程设计一般不强制要求但实现后能覆盖大 TXT 记录和 DNSZone Transfer 的场景。TCP 实现时注意处理粘包——DNS 报文的 TCP 封装是 2 字节长度前缀加报文本体不能直接复用 UDP 的解析函数。这个实验的评分核心从来不是你用了多炫的技术而是你能不能把整个解析链路讲清楚写明白。拿到这份压缩包我建议从报文解析开始读代码跑通后再逐步加入缓存和 TCP 支持不要一上来就全链路改。从那以后我每次拿到课设工程包都会强制走一遍「解压编码检查→编译参数审查→最小端口启动→抓包验证」这四部曲流程能避免九成以上可能浪费的时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑