注:该文用于个人学习记录和知识交流,如有不足,欢迎指点。

一、DNS协议(Domain Name System)

1.定义:

DNS 协议简单说就是将域名(如www.baidu.com)翻译成 IP 地址(如 14.215.177.38)的 “互联网地址簿协议”,是实现域名与 IP 地址对应关系的核心技术。

它运行在 UDP 协议之上(端口 53),采用分布式、层级化的域名解析体系,解决了 “IP 地址难记” 和 “IP 地址可能变更但域名不变” 的问题。

2. 核心作用:

换句话说为什么不直接使用ip连接就好了,还整出个DNS,是大牛们无聊吗? 当然不是,谜底就在下面。

1. 地址解析:这是最核心功能,把用户易记的域名转换成计算机能识别的 IP 地址,让数据能找到目标服务器。

2. 负载均衡:通过返回多个 IP 地址,让访问请求分散到不同服务器,避免单一服务器过载(比如大型网站常用)。

3. 故障冗余:当某个 IP 对应的服务器故障时,可返回其他正常 IP,保证服务不中断。

基于以上特性,可以说,DNS 是大型高并发服务器能 “触达用户”“承载压力”“稳定运行” 的前提之一

3. DNS的底层工作原理(即DNS是如何实现的):

DNS 的实现是 “分布式架构 + 层级化管理 + 缓存机制” 的完美结合:

  • 层级化解决了 “全球域名管理” 的问题,避免集中式瓶颈;
  • 分布式服务器集群确保了高可用和全球覆盖;
  • 缓存和负载均衡机制大幅提升了解析效率

3.1 DNS 核心实现基础三大模块表

核心模块 分类 / 子项 具体说明 关键作用与价值 典型示例
一、层级化的域名空间 根域(.) 域名层级的最顶层,全球共 13 组根服务器(由 12 个组织管理,含多台物理机副本),无实际域名映射,仅作为解析起点。 作为全球 DNS 解析的 “入口”,引导本地 DNS 服务器查询下一级(顶级域)服务器,避免集中式管理瓶颈。 用户查询www.baidu.com时,本地 DNS 先向根服务器请求,根服务器返回.com顶级域服务器地址。
顶级域(TLD) 根域下的第一层域名,按用途 / 地域分类,由专门机构管理(如.com由 Verisign 管理,.cn由中国互联网络信息中心管理)。 划分域名管理范围,让不同类型 / 地区的域名有专属管理机构,保障域名分配有序。 商业类.com、组织类.org、国家类.jp(日本)、.uk(英国)。
二级域 顶级域下的域名,需企业或个人向域名注册商申请注册(如baidu.comgithub.com),注册后拥有该域名的管理权限。 作为企业 / 个人的 “网络标识”,是域名的核心部分,用户可通过二级域识别品牌或服务。 taobao.com(淘宝)、tencent.com(腾讯)、163.com(网易)。
子域 由二级域管理者自行创建和分配(无需注册),用于区分内部不同服务,格式为 “子域。二级域。顶级域”。 细化二级域的服务分类,方便内部管理,让用户快速识别服务类型。 www.baidu.com(百度网页服务)、mail.163.com(网易邮件服务)、map.qq.com(腾讯地图服务)。
二、分布式解析服务器集群 本地 DNS 服务器 用户网络接入时默认配置的 DNS(如宽带运营商提供的202.96.134.133,或手动设置的公共 DNS 如谷歌8.8.8.8、阿里223.5.5.5)。 作为用户与上层 DNS 的 “中间代理”,接收用户查询请求,优先返回缓存结果,无缓存则代理解析,减少用户等待时间。 家用电脑默认使用电信 DNS202.96.134.133,查询www.bilibili.com时,先查该服务器缓存。
权威 DNS 服务器 管理特定域名解析记录的服务器(由域名持有者配置,如百度为baidu.com配置ns1.baidu.com),保存该域名的最终解析记录(A/AAAA/MX 等)。 存储域名的 “官方解析信息”,是域名解析的 “最终数据源”,确保解析结果的准确性。 baidu.com的权威服务器ns1.baidu.com,保存www.baidu.com对应的 IPv4 地址180.101.49.11
根服务器 管理根域(.)的服务器集群,全球共 13 组,不直接返回域名 IP,仅告知 “对应顶级域服务器的地址”。 衔接本地 DNS 与顶级域服务器,是全球 DNS 解析的 “桥梁”,保障解析流程的启动与衔接。 本地 DNS 查询www.qq.com时,根服务器返回.com顶级域服务器的地址列表。
三、标准化的协议与记录类型 协议规范 默认运行在 UDP 协议(端口 53),查询响应小于 512 字节时用 UDP(速度快);复杂查询(如带 DNSSEC 签名、响应超 512 字节)自动切换到 TCP。 兼顾解析速度与数据完整性:UDP 满足多数简单查询的高效需求,TCP 处理复杂场景的数据可靠性。 查询www.baidu.com的 A 记录(响应仅含 IP,小于 512 字节)用 UDP;查询带 DNSSEC 签名的记录用 TCP。
A 记录 最常用记录类型,定义 “域名→IPv4 地址” 的映射关系,是网页访问、App 联网的核心记录。 将易记域名转换为计算机可识别的 IPv4 地址,支撑绝大多数基于 IPv4 的网络服务。 www.baidu.com. A 180.101.49.11api.weixin.qq.com. A 123.151.137.18
AAAA 记录 定义 “域名→IPv6 地址” 的映射关系,适配 IPv6 网络环境,解决 IPv4 地址耗尽问题。 支撑基于 IPv6 的网络服务,是未来互联网的核心解析记录类型。 www.baidu.com. AAAA 2400:da00::6666ipv6.taobao.com. AAAA 240e:ff:f101:12::2
CNAME 记录 定义 “域名别名”,将一个域名指向另一个域名(目标域名需通过 A/AAAA 记录解析为 IP)。 简化域名管理:变更 IP 时仅需修改目标域名的记录,无需修改所有别名域名;实现服务别名映射。 www.baidu.com. CNAME baidu.com.wwwbaidu.com的别名)、blog.github.com. CNAME github.io.
MX 记录 定义 “域名→邮件服务器地址” 的映射,含 “优先级” 字段(数字越小优先级越高),支持多服务器容灾。 明确邮件投递目标,确保邮件准确发送到专门的邮件服务器,而非网站服务器;多服务器保障邮件不丢失。 gmail.com. MX 5 gmail-smtp-in.l.google.com.(优先级 5,主邮件服务器)、gmail.com. MX 10 alt1.gmail-smtp-in.l.google.com.(优先级 10,备用服务器)。

3.2 DNS 解析完整流程表(以www.baidu.com为例)

1 本地缓存查询

1. 用户输入域名后,浏览器先查自身缓存;

2. 未命中则查操作系统缓存(如 Windows 的ipconfig /displaydns);3. 仍未命中则查路由器缓存。

- 缓存命中直接使用 IP,避免网络查询,加速访问;

- 缓存有效期由 TTL(生存时间)控制(如 300 秒)。

若浏览器缓存中保存www.baidu.com → 180.101.49.11(且未过期),直接用该 IP 访问。
2 本地 DNS 服务器查询

1. 本地缓存未命中时,操作系统向本地 DNS 服务器(如运营商114.114.114.114或公共8.8.8.8)发送查询请求;

2. 本地 DNS 服务器检查自身缓存。

本地 DNS 作为 “代理”,优先返回缓存结果,减少上层服务器查询压力。 本地 DNS 服务器若缓存过www.baidu.com的 IP,直接返回给操作系统,无需后续层级查询。
3 层级化递归查询

Step 1:查询根服务器本地 DNS 向根服务器(.)发送请求:“www.baidu.com的 IP 是什么?”

根服务器回复:“.com顶级域服务器地址是a.gtld-servers.net等”。

根服务器不直接解析域名,仅指引下一级查询方向,是全球 DNS 解析的 “入口”。 根服务器返回 13 组.com顶级域服务器的 IP 列表,供本地 DNS 选择。
4

Step 2:查询顶级域服务器本地 DNS 向.com服务器发送请求:“www.baidu.com的 IP 是什么?”

.com服务器回复:“baidu.com的权威服务器是ns1.baidu.com等”。

顶级域服务器管理二级域的权威服务器地址,实现域名管理的层级划分。 .com服务器返回baidu.com的 4 台权威服务器地址(如ns1.baidu.comns2.baidu.com)。
5

Step 3:查询权威服务器本地 DNS 向baidu.com的权威服务器发送请求:“www.baidu.com的 IP 是什么?”

权威服务器返回对应的 IPv4/IPv6 地址(可能多个)。

权威服务器是域名解析的 “最终数据源”,保存该域名的官方解析记录。 baidu.com权威服务器返回www.baidu.com的 IP:180.101.49.11180.101.49.12(负载均衡)。
6 结果返回与缓存

1. 本地 DNS 服务器将权威服务器返回的 IP 缓存(按 TTL 设置有效期,如 300 秒);

2. 将 IP 地址返回给用户的操作系统 / 浏览器;

3. 浏览器基于 IP 通过 TCP 连接服务器,加载网页。

- 缓存结果供后续查询复用,提升效率;

- 多 IP 返回支持负载均衡,避免单服务器过载。

本地 DNS 缓存www.baidu.com的 IP,返回给浏览器;浏览器用180.101.49.11建立 TCP 连接,加载百度首页。

4. 应用场景

主要应用场景

场景分类 具体应用场景 DNS 作用细节 用户 / 开发者感知 & 实际价值 典型示例
日常上网 浏览器访问网站、电脑端软件联网(如视频播放器加载内容)

1. 用户输入域名(如www.baidu.com)后,操作系统先查本地 DNS 缓存;

2. 缓存未命中则请求本地 DNS 服务器(如运营商的114.114.114.114或公共的8.8.8.8);

3. DNS 服务器返回域名对应的 IPv4/IPv6 地址(如180.101.49.11);

4. 浏览器基于 IP 地址与网站服务器建立 TCP 连接,加载网页内容。

- 用户无需记忆复杂 IP,只需输入易记域名;

- 隐藏底层网络地址,即使网站服务器 IP 变更,域名不变也能正常访问。

输入www.bilibili.com,DNS 解析为120.92.142.45,浏览器加载 B 站首页;电脑端微信登录时,解析wx.qq.com获取服务器 IP。
邮件发送 个人 / 企业邮件客户端(如 Outlook、Foxmail)发送邮件,企业邮件系统间传递邮件

1. 发送方邮件系统提取收件人邮箱后缀(如xxx@gmail.comgmail.com);

2. 向 DNS 服务器查询该域名的MX 记录(邮件交换记录);

3. MX 记录返回收件方邮件服务器列表及优先级(如gmail-smtp-in.l.google.com,优先级 5);

4. 进一步解析 MX 记录中的服务器域名为 IP,通过 SMTP 协议将邮件投递到该服务器。

- 确保邮件准确投递到 “专门处理邮件的服务器”,而非网站服务器;

- 多 MX 记录支持容灾:主服务器不可用时,自动切换到备用服务器(低优先级),避免邮件丢失。

用 QQ 邮箱给xxx@163.com发邮件,DNS 查询163.com的 MX 记录,得到mx.163.com(优先级 10),解析 IP 后投递邮件;企业向客户发送营销邮件,通过 MX 记录定位客户公司的邮件服务器。
移动应用联网 手机 App 访问后端 API 接口、加载云端数据(如社交 App 刷动态、电商 App 查商品)

1. App 启动后,调用系统 DNS 接口(如 Android 的InetAddress.getByName、iOS 的getaddrinfo);

2. 解析后端 API 域名(如api.weixin.qq.com)为 IP;

3. App 基于 IP 地址发起 HTTP/HTTPS 请求,获取用户数据(如微信消息、朋友圈内容)。

- 对用户无感知,App 后台自动完成解析;

- 支持 API 服务器扩容:同一域名绑定多个 IP,DNS 轮询分发请求,避免单服务器过载。

抖音 App 刷视频时,解析api.amemv.com获取视频数据服务器 IP;支付宝转账时,解析transfer.alipay.com定位交易接口服务器。
云服务与 CDN 大型网站(如电商、视频平台)通过 CDN 加速内容分发,云服务器负载均衡

1. 用户请求网站域名(如www.tmall.com),DNS 服务器先查询该域名的CDN 节点记录

2. 根据用户地理位置(如北京)、网络运营商(如联通),返回最近的 CDN 节点 IP(如北京联通的 CDN 节点202.99.16.19);

3. 用户从 CDN 节点加载静态资源(如商品图片、视频片段),而非直接访问源服务器。

- 用户访问速度大幅提升:静态资源从就近节点加载,延迟降低(如视频缓冲更快);- 减轻源服务器压力:90% 以上的静态请求由 CDN 承接,源服务器仅处理核心业务(如订单支付)。 淘宝双 11 期间,用户在上海访问www.taobao.com,DNS 返回上海电信 CDN 节点 IP,加载商品图片;Netflix 用户观看海外剧集,DNS 返回就近的 CDN 节点,降低视频卡顿率。
企业内网服务 企业员工访问内部系统(如 OA 办公系统、财务系统)、内部服务器间通信

1. 企业部署私有 DNS 服务器(仅内网可访问),配置内部域名与内网 IP 的映射(如oa.company.com192.168.1.100);

2. 员工电脑 / 办公设备默认使用私有 DNS 服务器;

3. 访问内部域名时,私有 DNS 直接返回内网 IP,无需经过公网 DNS。

- 提升内网访问效率:避免公网 DNS 解析延迟,内网服务访问更流畅;- 保障内网安全:内部域名不对外暴露,降低被外部攻击的风险;- 简化管理:即使内网服务器 IP 变更,只需修改私有 DNS 配置,员工无需调整访问地址。 企业员工在电脑输入oa.company.com,私有 DNS 解析为内网 IP192.168.1.100,访问办公系统;内部财务系统服务器finance.company.com解析为192.168.2.50,供财务部门专用。

浏览器通过 DNS 访问网站的详细流程

步骤 详细过程 关键说明
1 用户在浏览器输入域名(如www.baidu.com),按下回车触发访问请求。 人类记忆域名更轻松,输入域名后需先转为 IP 才能让计算机定位服务器。
2 操作系统先查询本地 DNS 缓存(包括系统缓存、浏览器缓存、路由器缓存)。

- 若缓存命中(如之前访问过百度,本地保存了www.baidu.com180.101.49.11),直接跳至步骤 5;

- 若缓存未命中(首次访问或缓存过期,缓存有效期由 DNS 记录的 TTL 值控制),进入步骤 3。

3 操作系统向本地 DNS 服务器发送查询请求(默认是宽带运营商提供的 DNS,如电信202.96.134.133;也可手动设置公共 DNS,如谷歌8.8.8.8、阿里223.5.5.5)。

本地 DNS 服务器是用户网络接入的 “第一级 DNS 代理”,通常会缓存常用域名的解析结果。

若本地 DNS 服务器也没有缓存,会启动层级查询流程

4 本地 DNS 服务器执行层级查询(若自身无缓存):

这是 DNS 的核心分布式解析逻辑,通过多级服务器协作找到最终 IP:

1. 本地 DNS 先向根域名服务器.,全球共 13 组)查询,根服务器返回.com顶级域名服务器的地址;

2. 本地 DNS 向.com顶级域名服务器查询,得到baidu.com权威域名服务器的地址;

3. 本地 DNS 向baidu.com权威服务器查询,最终获取www.baidu.com对应的 IPv4/IPv6 地址(可能返回多个 IP,用于负载均衡);

4. 本地 DNS 服务器缓存该解析结果(按 TTL 值设置有效期),并将 IP 返回给操作系统。

5 操作系统将 IP 地址传递给浏览器,浏览器基于 IP 地址与百度服务器建立 TCP 连接(三次握手)。 TCP 是可靠传输协议,网页数据需通过 TCP 连接传输,确保数据不丢失、不混乱。
6 连接建立后,浏览器向百度服务器发送HTTP/HTTPS 请求(如请求www.baidu.com的首页 HTML 文件)。 - 若用 HTTP,请求直接明文发送;- 若用 HTTPS(现在主流),会先进行 TLS 握手建立加密通道,再发送请求,保障数据安全。
7 百度服务器接收请求,处理后返回响应数据(如 HTML、CSS、JavaScript、图片等资源)。 服务器会根据请求内容(如首页、搜索结果)生成对应的响应,通过 TCP 连接回传。
8 浏览器接收响应数据,解析并渲染(如解析 HTML 构建 DOM 树、加载 CSS 渲染样式、执行 JS 实现交互),最终呈现完整的百度首页。 这一步是浏览器的核心功能,将服务器返回的 “代码” 转换成用户能看到的 “网页界面”。

MX的相关说明

信息类别 具体说明 示例(以@gmail.com为例)
MX 记录定义 DNS 中专门用于指定域名对应邮件服务器地址的记录,包含 “服务器地址” 和 “优先级” 两个核心字段。 记录格式:域名. MX 优先级 邮件服务器域名.
MX 记录核心字段

1. 优先级:数字越小优先级越高,用于多服务器时确定投递顺序;

2. 服务器地址:接收该域名邮件的服务器域名(需进一步解析为 IP)。

gmail.com. MX 5 gmail-smtp-in.l.google.com.(优先级 5,高于优先级 10 的服务器)
邮件发送时 MX 查询流程

1. 提取收件人域名:从邮箱地址中分离出后缀(如someone@gmail.comgmail.com);

2. 查询 MX 记录:向 DNS 服务器请求该域名的 MX 记录,获取邮件服务器列表;

3. 解析服务器 IP:将 MX 记录中的邮件服务器域名解析为 IP(如gmail-smtp-in.l.google.com142.251.9.27);

4. 发送邮件:通过 SMTP 协议将邮件发送到该 IP 对应的服务器,由其递交给目标邮箱。

1. 提取域名:gmail.com

2. 查询 MX 记录:得到gmail-smtp-in.l.google.com(优先级 5);

3. 解析 IP:142.251.9.27

4. 发送邮件:通过 SMTP 将邮件发给该 IP。

MX 记录的必要性

1. 区分服务器角色:域名的 A 记录通常指向网站服务器(如gmail.com的 A 记录指向网页邮箱),MX 记录专门指向邮件服务器;

2. 确保准确投递:避免邮件误发送到网站服务器,保证投递到专门处理邮件的服务器。

若没有 MX 记录,发送给@gmail.com的邮件会默认投递到gmail.com的网站服务器,导致邮件丢失。

 

5. 报文结构

1. 定义:

DNS 报文是 DNS 协议进行域名解析时客户端与服务器之间传递的数据包,采用固定的二进制结构,用于在查询请求(客户端→服务器)和响应(服务器→客户端)中携带关键信息(如查询的域名、解析类型、结果 IP 等)。

2. 结构:

DNS 报文由首部(Header)、问题区(Question)、回答区(Answer)、授权区(Authority)、附加区(Additional) 5 部分组成,整体长度不超过 512 字节(UDP 传输时)。

字段区域 核心作用 关键组成(以首部为例)
首部(Header) 控制整个 DNS 报文,共 12 字节

- ID(2 字节):标识请求,用于匹配响应

- 标志位(2 字节):如 “查询 / 响应”“是否权威”

- 4 个计数字段(各 2 字节):分别记录问题数、回答数、授权数、附加数

问题区(Question) 存放用户的查询请求(如 “www.baidu.com的 A 记录”)

- 域名(变长):以 “.” 分割,每个部分前加长度,末尾用 0 结束- 查询类型(2 字节):如 A(IPv4 地址)、AAAA(IPv6 地址)、CNAME(别名)

- 查询类(2 字节):通常为 1(表示互联网地址)

回答区(Answer) 存放 DNS 服务器的解析结果 结构与问题区类似,但多了 “生存时间(TTL)”“数据长度”“解析数据”(如具体 IP)
授权区(Authority) 存放权威 DNS 服务器的地址(当当前服务器非权威时) 提供能解析该域名的权威服务器域名及 IP
附加区(Additional) 提供额外辅助信息(如授权服务器的 IP) 避免用户再次查询权威服务器地址,减少网络请求

2.1 头部(Header,12 字节)

头部是 DNS 报文的 “控制中心”,由 6 个 2 字节字段组成:

字段(2 字节) 含义与取值说明
标识(ID) 客户端生成的随机数,用于匹配查询与响应(如客户端发送 ID=12345 的查询,服务器用相同 ID 回复)。
标志(Flags) 包含多个子字段,核心有:- 操作码(Opcode):0 = 标准查询,1 = 反向查询;- 响应码(Rcode):0 = 成功,3 = 域名不存在;- QR 位:0 = 查询报文,1 = 响应报文;- AA 位:1 = 权威服务器的响应;- RD 位:1 = 客户端请求递归查询;- RA 位:1 = 服务器支持递归查询。
问题数(Qdcount) 问题区中查询项的数量(通常为 1)。
回答数(Ancount) 回答区中资源记录的数量(响应报文才有,如查询www.baidu.com可能返回 2 条 A 记录)。
授权数(Nscount) 授权区中资源记录的数量(如返回baidu.com的 NS 记录数量)。
附加数(Arcount) 附加区中资源记录的数量(如返回 NS 服务器对应的 IP 数量)。
2.2 问题区(Question,可变长度)

用于描述客户端的查询需求,结构为:

字段 长度(字节) 说明
查询域名(Qname) 可变 以 “长度 + 字符串” 形式编码(如www.baidu.com编码为3www5baidu3com0,0 表示结束)。
查询类型(Qtype) 2 指定解析类型:1=A 记录(IPv4),28=AAAA 记录(IPv6),5=CNAME,15=MX 等。
查询类(Qclass) 2 通常为 1(表示互联网地址,即 IPv4)。
2.3 回答区 / 授权区 / 附加区(均为资源记录结构)

这三个区域的每条记录都遵循 “资源记录(RR)” 格式,结构为:

字段 长度(字节) 说明
域名(Name) 可变 与问题区 Qname 编码方式相同,指该记录对应的域名(如www.baidu.com)。
类型(Type) 2 同 Qtype(如 1=A 记录)。
类(Class) 2 同 Qclass(通常为 1)。
生存时间(TTL) 4 记录在缓存中的有效期(秒),如 300 表示可缓存 5 分钟。
数据长度(Rdlength) 2 资源数据(Rdata)的字节长度。
资源数据(Rdata) 可变 解析结果:- A 记录:4 字节 IPv4 地址(如180.101.49.11);- CNAME:域名(如baidu.com);- MX:优先级(2 字节)+ 邮件服务器域名。

3. 示例:简化的 DNS 查询与响应报文

查询报文(客户端→服务器):
  • 头部:ID=0x1234,QR=0(查询),Qdcount=1(1 个问题);
  • 问题区:Qname=www.baidu.com,Qtype=1(A 记录),Qclass=1;
  • 其他区域为空。
响应报文(服务器→客户端):
  • 头部:ID=0x1234(与查询匹配),QR=1(响应),Ancount=2(2 条 A 记录);
  • 问题区:与查询报文相同(重复客户端的查询需求);
  • 回答区:2 条 A 记录,分别为www.baidu.com → 180.101.49.11(TTL=300)和www.baidu.com → 180.101.49.12(TTL=300);
  • 授权区:baidu.com的 NS 记录(如ns1.baidu.com);
  • 附加区:ns1.baidu.com的 A 记录(如202.108.22.220)。

二、C语言实现简单的 DNS 客户端,编写报文用于查询指定域名的 IP 地址。

1.DNS请求模块(报文)的构建

dns_create_header

初始化 DNS 报文头部,生成随机会话 ID,设置标志位和问题数量。

// DNS报文头部结构
struct dns_header {
	unsigned short id;         // 会话标识,用于匹配请求和响应
	unsigned short flags;      // 标志位,包含查询/响应、授权等信息
	unsigned short questions;  // 问题数
	unsigned short answer;     // 回答资源记录数
	unsigned short authority;  // 授权资源记录数
	unsigned short additional; // 附加资源记录数
};

// 创建DNS报文头部
int dns_create_header(struct dns_header *header) {
	if (header == NULL) return -1;
	memset(header, 0, sizeof(struct dns_header));  // 初始化头部为0

	// 生成随机ID,用于标识当前查询会话
	srandom(time(NULL));
	header->id = random();

	// 设置标志位:0x0100表示标准查询
	header->flags = htons(0x0100);
	header->questions = htons(1);  // 问题数为1

	return 0;
}

dns_create_question

将普通域名(如 www.baidu.com)转换为 DNS 压缩格式(如 3www5baidu3com0),并设置查询类型和查询类。

// DNS查询问题结构
struct dns_question {
	int length;               // 域名长度
	unsigned short qtype;     // 查询类型(如A记录、CNAME等)
	unsigned short qclass;    // 查询类(通常为IN,即互联网)
	unsigned char *name;      // 待查询的域名(采用DNS压缩格式)
};


// 构建DNS查询问题部分
// hostname: 待查询的域名,如www.baidu.com
// DNS格式的域名会将每个部分的长度加在前面,如"3www5baidu3com0"
int dns_create_question(struct dns_question *question, const char *hostname) {
	if (question == NULL || hostname == NULL) return -1;
	memset(question, 0, sizeof(struct dns_question));  // 初始化问题结构

	// 为域名分配内存,长度为原域名长度+2(额外的长度标识位)
	question->name = (char*)malloc(strlen(hostname) + 2);
	if (question->name == NULL) {
		return -2;  // 内存分配失败
	}

	question->length = strlen(hostname) + 2;
	question->qtype = htons(1);  // 查询类型为A记录(主机地址)
	question->qclass = htons(1); // 查询类为IN(互联网)

	// 分割域名并转换为DNS格式
	const char delim[2] = ".";  // 以点分割域名
	char *qname = question->name;
	
	// 复制原域名以便分割(strtok会修改原字符串)
	char *hostname_dup = strdup(hostname);
	char *token = strtok(hostname_dup, delim);  // 获取第一个部分(如www)

	while (token != NULL) {
		size_t len = strlen(token);  // 获取当前部分的长度

		*qname = len;  // 存储长度
		qname++;

		// 复制域名部分
		strncpy(qname, token, len+1); // len+1确保复制字符串末尾的是'\0',即最后一次存储为com0。
		qname += len;

		// 获取下一个部分(如baidu、com)
		token = strtok(NULL, delim);
	}

	free(hostname_dup);  // 释放复制的域名内存
	return 0;  // 添加了遗漏的返回值
}

dns_build_request

整合头部和问题部分,构建完整的 DNS 请求报文。

// 构建完整的DNS请求报文
// header: DNS头部
// question: DNS查询问题
// request: 输出的请求报文
// rlen: 请求报文的最大长度
int dns_build_request(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);  // 初始化请求缓冲区

	// 复制头部到请求报文
	memcpy(request, header, sizeof(struct dns_header));
	int offset = sizeof(struct dns_header);

	// 复制域名到请求报文
	memcpy(request+offset, question->name, question->length);
	offset += question->length;

	// 复制查询类型到请求报文
	memcpy(request+offset, &question->qtype, sizeof(question->qtype));
	offset += sizeof(question->qtype);

	// 复制查询类到请求报文
	memcpy(request+offset, &question->qclass, sizeof(question->qclass));
	offset += sizeof(question->qclass);

	return offset;  // 返回构建的请求报文长度
}

2.网络通信模块

  • 创建 UDP 套接字,连接到指定的 DNS 服务器。

  • 通过 sendto 发送构建好的 DNS 请求报文。

  • 通过 recvfrom 接收 DNS 服务器返回的响应报文

3.响应解析模块

负责解析 DNS 服务器返回的响应报文,提取出域名对应的 IP 地址:

is_pointer

判断是否为 DNS 压缩格式的指针(用于处理域名压缩)。

// 判断是否为指针(DNS压缩格式)
static int is_pointer(int in) {
	return ((in & 0xC0) == 0xC0);  // 0xC0是指针的标志位
}

dns_parse_name

解析 DNS 报文中的域名(处理可能的压缩格式)。

    // 解析DNS报文中的域名(处理可能的压缩格式)
    // chunk: 整个DNS响应报文
    // ptr: 当前解析位置
    // out: 输出的域名
    // len: 输出域名的长度
    static void dns_parse_name(unsigned char *chunk, unsigned char *ptr, char *out, int *len) {
    	int flag = 0, n = 0;
    	char *pos = out + (*len);  // 从输出缓冲区的当前位置开始
    
    	while (1) {
    		flag = (int)ptr[0];
    		if (flag == 0) break;  // 遇到0表示域名结束
    
    		// 如果是指针,需要跳转解析
    		if (is_pointer(flag)) {
    			n = (int)ptr[1];  // 指针指向的偏移量
    			ptr = chunk + n;  // 跳转到指针指向的位置
    			dns_parse_name(chunk, ptr, out, len);  // 递归解析
    			break;
    		} else {
    			// 不是指针,直接解析标签
    			ptr++;  // 跳过长度字节
    			memcpy(pos, ptr, flag);  // 复制标签内容
    			pos += flag;
    			ptr += flag;
    
    			*len += flag;  // 更新长度
    			// 如果不是最后一个标签,添加点分隔符
    			if ((int)ptr[0] != 0) {
    				memcpy(pos, ".", 1);
    				pos += 1;
    				(*len) += 1;
    			}
    		}
    	}
    }

    dns_parse_response

    解析整个响应报文,提取问题数、回答数,处理 CNAME 记录和 A 记录,最终将解析结果存入 dns_item 结构体列表。

    // 解析DNS服务器的响应报文
    // buffer: 响应报文数据
    // domains: 输出的解析结果列表
    static int dns_parse_response(char *buffer, struct dns_item **domains) {
    	int i = 0;
    	unsigned char *ptr = buffer;  // 解析指针
    
    	// 跳过ID和标志位,指向问题数
    	ptr += 4;
    	int querys = ntohs(*(unsigned short*)ptr);  // 问题数量
    
    	// 指向回答数
    	ptr += 2;
    	int answers = ntohs(*(unsigned short*)ptr);  // 回答数量
    
    	// 跳过授权数和附加数
    	ptr += 6;
    
    	// 跳过问题部分
    	for (i = 0; i < querys; i++) {
    		while (1) {
    			int flag = (int)ptr[0];
    			ptr += (flag + 1);  // 跳过标签和长度字节
    			if (flag == 0) break;  // 域名结束
    		}
    		ptr += 4;  // 跳过查询类型和查询类
    	}
    
    	char cname[128], aname[128], ip[20], netip[4];
    	int len, type, ttl, datalen;
    
    	int cnt = 0;
    	// 为解析结果分配内存
    	struct dns_item *list = (struct dns_item*)calloc(answers, sizeof(struct dns_item));
    	if (list == NULL) {
    		return -1;  // 内存分配失败
    	}
    
    	// 解析每个回答
    	for (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)
    		ttl = htons(*(unsigned short*)ptr);
    		ptr += 4;  // 跳TTL
    
    		// 获取数据长度
    		datalen = ntohs(*(unsigned short*)ptr);
    		ptr += 2;  // 跳过数据长度
    
    		// 处理CNAME记录
    		if (type == DNS_CNAME) {
    			bzero(cname, sizeof(cname));
    			len = 0;
    			dns_parse_name(buffer, ptr, cname, &len);  // 解析别名
    			ptr += datalen;  // 移动指针到下一个记录
    		} 
    		// 处理A记录(IP地址)
    		else if (type == DNS_HOST) {
    			bzero(ip, sizeof(ip));
    
    			// IPv4地址长度为4字节
    			if (datalen == 4) {
    				memcpy(netip, ptr, datalen);  // 复制网络字节序的IP
    				// 转换为点分十进制字符串
    				inet_ntop(AF_INET, netip, ip, sizeof(struct sockaddr));
    
    				// 打印解析结果
    				printf("%s has address %s\n", aname, ip);
    				printf("\tTime 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;  // 返回解析到的IP数量
    }

    4. 主流程控制

    dns_client_commit

    整合请求构建、网络通信和响应解析的全过程,完成一次 DNS 查询。

    // 执行DNS查询的主要函数
    int dns_client_commit(const char *domain) {
    	// 创建UDP套接字
    	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    	if (sockfd < 0) {
    		return -1;  // 套接字创建失败
    	}
    
    	// 设置DNS服务器地址
    	struct sockaddr_in servaddr = {0};
    	servaddr.sin_family = AF_INET;
    	servaddr.sin_port = htons(DNS_SERVER_PORT);
    	servaddr.sin_addr.s_addr = inet_addr(DNS_SERVER_IP);
    
    	// 连接到DNS服务器(UDP的connect只是设置默认目标地址)
    	int ret = connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
    	printf("connect : %d\n", ret);  // 打印连接结果(UDP通常返回0)
    
    	// 构建DNS请求头部
    	struct dns_header header = {0};
    	dns_create_header(&header);
    
    	// 构建DNS查询问题
    	struct dns_question question = {0};
    	dns_create_question(&question, domain);
    
    	// 构建完整的请求报文
    	char request[1024] = {0};
    	int length = dns_build_request(&header, &question, request, 1024);
    
    	// 发送DNS查询请求
    	int slen = sendto(sockfd, request, length, 0, 
    					  (struct sockaddr*)&servaddr, sizeof(struct sockaddr)); // 注意用strlen(request)替换length会导致 DNS 请求报文发送不完整,大概率触发服务器响应错误。DNS的报文里面域名以0结尾,strlen计算到0就结束了,导致后续的qtype和qclass没有发送出去。
    
    
    	// 接收DNS服务器的响应
    	char response[1024] = {0};
    	struct sockaddr_in addr;
    	size_t addr_len = sizeof(struct sockaddr_in);
    	int n = recvfrom(sockfd, response, sizeof(response), 0, 
    					(struct sockaddr*)&addr, (socklen_t*)&addr_len); 
    
    	// 解析响应报文
    	struct dns_item *dns_domain = NULL;
    	dns_parse_response(response, &dns_domain);
    
    	// 释放资源
    	free(dns_domain);
    	close(sockfd);  // 关闭套接字(添加了遗漏的关闭操作)
    	
    	return n;  // 返回接收的字节数
    }
    

    main 函数:

    检查输入参数,调用 dns_client_commit 执行查询,并处理程序入口逻辑。

      #include <stdio.h>
      #include <string.h>
      #include <stdlib.h>
      #include <time.h>  // 补充time.h头文件,用于srandom生成随机数种子
      
      #include <sys/socket.h>
      #include <netinet/in.h>
      #include <arpa/inet.h>  // 补充arpa/inet.h头文件,用于inet_ntop进行IP地址转换
      #include <unistd.h>
      
      // 这是由ai对dns.c文件的注释
      
      // DNS服务器端口号,标准DNS端口为53
      #define DNS_SERVER_PORT		53
      // 使用114.114.114.114作为DNS服务器(国内常用的公共DNS)
      #define DNS_SERVER_IP		"114.114.114.114"
      
      // DNS资源记录类型:主机地址(A记录)
      #define DNS_HOST			0x01
      // DNS资源记录类型:规范名称(CNAME记录)
      #define DNS_CNAME			0x05
      
      // 主函数
      int main(int argc, char *argv[]) {
      	// 检查参数,需要提供域名作为参数
      	if (argc < 2) 
      	{
      		printf("Input: ./dns_test domain\n");
      		return -1;
      	}
      	// 执行DNS查询
      	dns_client_commit(argv[1]);
      
      	return 0;  // 添加了遗漏的返回值
      }

      三、代码测试

      panda@ubuntu02:~$ ./dns_test www.baidu.com
      connect : 0
      www.a.shifen.com has address 183.2.172.177
              Time to live: 0 minutes , 0 seconds
      www.a.shifen.com has address 183.2.172.17
              Time to live: 0 minutes , 0 seconds

      可知域名www.baidu.com对应两个ip分别是183.2.172.177和183.2.172.17

      Logo

      有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

      更多推荐