Linux C/C++ 学习日记(7):DNS协议及用C语言发送报文实现查询指定域名的IP地址
注:该文用于个人学习记录和知识交流,如有不足,欢迎指点。
一、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.com 、github.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.11 、api.weixin.qq.com. A 123.151.137.18 。 |
|
AAAA 记录 | 定义 “域名→IPv6 地址” 的映射关系,适配 IPv6 网络环境,解决 IPv4 地址耗尽问题。 | 支撑基于 IPv6 的网络服务,是未来互联网的核心解析记录类型。 | www.baidu.com. AAAA 2400:da00::6666 、ipv6.taobao.com. AAAA 240e:ff:f101:12::2 。 |
|
CNAME 记录 | 定义 “域名别名”,将一个域名指向另一个域名(目标域名需通过 A/AAAA 记录解析为 IP)。 | 简化域名管理:变更 IP 时仅需修改目标域名的记录,无需修改所有别名域名;实现服务别名映射。 | www.baidu.com. CNAME baidu.com. (www 是baidu.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 的 |
- 缓存命中直接使用 IP,避免网络查询,加速访问; - 缓存有效期由 TTL(生存时间)控制(如 300 秒)。 |
若浏览器缓存中保存www.baidu.com → 180.101.49.11 (且未过期),直接用该 IP 访问。 |
2 | 本地 DNS 服务器查询 |
1. 本地缓存未命中时,操作系统向本地 DNS 服务器(如运营商 2. 本地 DNS 服务器检查自身缓存。 |
本地 DNS 作为 “代理”,优先返回缓存结果,减少上层服务器查询压力。 | 本地 DNS 服务器若缓存过www.baidu.com 的 IP,直接返回给操作系统,无需后续层级查询。 |
3 | 层级化递归查询 |
Step 1:查询根服务器本地 DNS 向根服务器(.)发送请求:“ 根服务器回复:“ |
根服务器不直接解析域名,仅指引下一级查询方向,是全球 DNS 解析的 “入口”。 | 根服务器返回 13 组.com 顶级域服务器的 IP 列表,供本地 DNS 选择。 |
4 |
Step 2:查询顶级域服务器本地 DNS 向
|
顶级域服务器管理二级域的权威服务器地址,实现域名管理的层级划分。 | .com 服务器返回baidu.com 的 4 台权威服务器地址(如ns1.baidu.com 、ns2.baidu.com )。 |
|
5 |
Step 3:查询权威服务器本地 DNS 向 权威服务器返回对应的 IPv4/IPv6 地址(可能多个)。 |
权威服务器是域名解析的 “最终数据源”,保存该域名的官方解析记录。 | baidu.com 权威服务器返回www.baidu.com 的 IP:180.101.49.11 、180.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. 用户输入域名(如 2. 缓存未命中则请求本地 DNS 服务器(如运营商的 3. DNS 服务器返回域名对应的 IPv4/IPv6 地址(如 4. 浏览器基于 IP 地址与网站服务器建立 TCP 连接,加载网页内容。 |
- 用户无需记忆复杂 IP,只需输入易记域名; - 隐藏底层网络地址,即使网站服务器 IP 变更,域名不变也能正常访问。 |
输入www.bilibili.com ,DNS 解析为120.92.142.45 ,浏览器加载 B 站首页;电脑端微信登录时,解析wx.qq.com 获取服务器 IP。 |
邮件发送 | 个人 / 企业邮件客户端(如 Outlook、Foxmail)发送邮件,企业邮件系统间传递邮件 |
1. 发送方邮件系统提取收件人邮箱后缀(如 2. 向 DNS 服务器查询该域名的MX 记录(邮件交换记录); 3. MX 记录返回收件方邮件服务器列表及优先级(如 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 的 2. 解析后端 API 域名(如 3. App 基于 IP 地址发起 HTTP/HTTPS 请求,获取用户数据(如微信消息、朋友圈内容)。 |
- 对用户无感知,App 后台自动完成解析; - 支持 API 服务器扩容:同一域名绑定多个 IP,DNS 轮询分发请求,避免单服务器过载。 |
抖音 App 刷视频时,解析api.amemv.com 获取视频数据服务器 IP;支付宝转账时,解析transfer.alipay.com 定位交易接口服务器。 |
云服务与 CDN | 大型网站(如电商、视频平台)通过 CDN 加速内容分发,云服务器负载均衡 |
1. 用户请求网站域名(如 2. 根据用户地理位置(如北京)、网络运营商(如联通),返回最近的 CDN 节点 IP(如北京联通的 CDN 节点 3. 用户从 CDN 节点加载静态资源(如商品图片、视频片段),而非直接访问源服务器。 |
- 用户访问速度大幅提升:静态资源从就近节点加载,延迟降低(如视频缓冲更快);- 减轻源服务器压力:90% 以上的静态请求由 CDN 承接,源服务器仅处理核心业务(如订单支付)。 | 淘宝双 11 期间,用户在上海访问www.taobao.com ,DNS 返回上海电信 CDN 节点 IP,加载商品图片;Netflix 用户观看海外剧集,DNS 返回就近的 CDN 节点,降低视频卡顿率。 |
企业内网服务 | 企业员工访问内部系统(如 OA 办公系统、财务系统)、内部服务器间通信 |
1. 企业部署私有 DNS 服务器(仅内网可访问),配置内部域名与内网 IP 的映射(如 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 缓存(包括系统缓存、浏览器缓存、路由器缓存)。 |
- 若缓存命中(如之前访问过百度,本地保存了 - 若缓存未命中(首次访问或缓存过期,缓存有效期由 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 先向根域名服务器( 2. 本地 DNS 向 3. 本地 DNS 向 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. 提取收件人域名:从邮箱地址中分离出后缀(如 2. 查询 MX 记录:向 DNS 服务器请求该域名的 MX 记录,获取邮件服务器列表; 3. 解析服务器 IP:将 MX 记录中的邮件服务器域名解析为 IP(如 4. 发送邮件:通过 SMTP 协议将邮件发送到该 IP 对应的服务器,由其递交给目标邮箱。 |
1. 提取域名: 2. 查询 MX 记录:得到 3. 解析 IP: 4. 发送邮件:通过 SMTP 将邮件发给该 IP。 |
MX 记录的必要性 |
1. 区分服务器角色:域名的 A 记录通常指向网站服务器(如 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
更多推荐
所有评论(0)