自上而下的理解网络(1)——DNS篇
一.引言
现代生活中,网络可谓是无处不在,购物需要网络,付款需要网络,各种生活缴费需要网络,在各行各业的工作中,更是离不开网络。说到底,网络的作用无非是支持计算机间进行数据交换。世界各地有着不计其数的网络设备,这些网络设备是如何有序正常的进行数据交流的呢?网络以及各种协议的工作原理又是怎样的呢?本系列博客,我们将尝试自上而下的对网路的工作原理进行介绍,从应用层开始,逐层向下,详细的帮助你理解网络的核心工作原理。当然,网络协议多如牛毛,在网络分层中每一层的知识也是非常浩渺,希望这些博客可以起到抛砖引玉的作用,能够使你对于天天使用的互联网网络在宏观上有认识,在微观上也有了解。
二.访问网站的第一步是什么?
说到网络,对于普通用户来说,使用最多的可能就是浏览各种网站了,虽然现在移动设备上的App基本代替了传统的PC应用和网站,但是这些App里提供的数据本质上网站中提供的数据并无不同,使用的网络技术并无不同。
我们知道,不论是访问网站还是App内进行接口请求,这些数据都是存储在“服务器”这种特殊的远程设备上的,要向服务器获取数据,首先我们需要找到服务器的位置,这很好理解,只有找到它,我们才能和它产生数据交流。互联网无论多大,本质上依然是通过电缆、光纤或各种无线设备这类连接介质连接在一起的,如果一台设备没有硬件上连接入互联网,那么说破天我们也无法和它产生数据交互。要找到一台互联网设备,实际上是通过其物理Mac地址来找到的,这就像现实中的门牌号一样,每家的门牌号都不同,说到这,我们要再老生常谈一下,抛出网络分层模型给你看:
关于这个网络分层模型,它在我们后面博客中的出境还少不了,现在你可以先不用管它,你只需要先知道物理层是负责设备物理媒介相关的协议,数据链路层通过硬件的Mac地址找到具体要网络设备,网络层通过IP协议来封装真实的Mac地址,传输层是对网络层的一种封装,TCP,UDP等传输协议在这一层工作,而最上层的应用层就是我们常说的网络应用协议,如DNS,HTTP,HTTPS和FPT协议工作在这一层。
关于网络分层模型,我们先把多说了,我们的宗旨是自上而下的理解网络,那么还是回到第一步来。我们在访问网站时,都会现在浏览器输入网站的地址,这通常是一个域名,例如我要访问自己的技术博客网站,我会在浏览器输入如下的地址:
huishao.cc就是一个域名,首先只通过域名我们是找不到要访问的对方服务器的,这就好像现实中我要去小王家,可以我只知道小王的名字“王某某”是无法找到他的家的,我需要有一个住址簿,告诉我小王究竟住在哪了,这样我才能找到他。当然,此住址可能也不是真正的物理位置,可能是一个社区,比如小王住在“光明社区”,具体光明社区在哪,我们可以再通过查看地图获取。对应到互联网中,域名就是一个名字,它方便我们对网站进行记忆,IP地址则是要访问的对方在逻辑上的地址,这方便互联网的网络管理,最终的硬件地址则是真正的对方位置。IP地址到硬件地址的映射,等我们讨论到了再细聊,本篇博客我们就说域名到IP地址映射这一过程。
三. DNS服务器
现在你应该已经明确,要通过域名找到某个设备,第一步是先得到此域名对应的IP地址,那么此IP地址是怎么得到的呢?首先,一定有一个地方维护了域名与IP地址的映射关系,如果你有过建站的经历,那么你一定进行过域名绑定操作,一个网站建成后,理论上就已经可以使用IP的方式来进行访问,但是为了易记和动态变动IP,通常会对其进行域名绑定。由域名获取到IP的这一过程,我们称之为域名解析。
域名解析是一种服务,提供域名解析服务的服务器即是DNS服务器,下图可以很形象的表示域名服务器的工作方式:
可以发现,映射表中记录了域名与IP间的映射关系,在实际的应用中,上图中描述的场景看似可行,实际却并非如此,世界上的域名与IP总数是一个非常庞大的数字,由一台服务器来维护所有域名IP信息几乎不可能,而且对于域名解析服务,请求量是巨大的,会有大量的用户频繁的进行域名解析请求,单服务器明显是不能满足需求的。因此,实际生产环境中的DNS解析是采用层层递进,多级缓存,递归查询的方式进行的。再看下图:
上图看似复杂,实际上只是描述了三个关键词:层层递进,多级缓存,递归查询。
四.DNS解析过程
下面我们来解释域名要解析成正确的IP地址,要经过的几个重要过程。
1. 本机hosts文件
本机hosts文件是优先级最高的域名IP映射表,对于Mac操作系统,这个文件在根目录的etc文件夹下,我们可以直接将域名与对应的IP写在这个文件中,在进行域名解析时,首先会从这个文件中找。广播IP和本机IP对应的域名实际上就定义在这里,如下:
你也可以在其中新增任意映射,例如将huishao.cc的域名映射到127.0.0.1的本机IP,保存后,在浏览器再输入huishao.cc,你将无法再访问到珲少的博客网站,如下图所示:
更多时候,hosts的正确用法是开发应用程序时,测试环境和正式环境可以将域名配置到不同的IP,这样无需应用程序代码中做逻辑,只需要切换hosts文件即可实现环境的切换。
2. 本机应用缓存
本机应用缓存是多级缓存中的第一级,例如当我们在浏览器中访问过某个域名后,其解析的结果会被浏览器缓存下来,当我们再次访问这个域名时,其首先会检查浏览器缓存,如果缓存能够命中此域名,则直接使用,缓存的有效时间会受TTL配置影响(我们后面会介绍)。
3. 本机系统缓存
与本机应用缓存类似,操作系统中也会有一份域名解析的缓存,如果本机应用缓存中没有命中,会从操作系统缓存中检查是否之前有过此域名的解析记录。如果能够命中则会直接使用。
4. 路由器域名解析缓存
如果本机系统缓存依然没有命中,而你的设备又是通过路由器接入的公网,此时你的域名解析服务很大可能是路由器提供的,可以打开网络设置的DNS一栏,观察DNS服务器的地址,如果是192.168.x.x类型内网地址,则说明是由路由器来完成DNS解析了。如下图所示:
路由器内,实际上也会缓存一张DNS解析表,会从其中寻找是否有可以命中的缓存,如果存在并且未过期,则直接使用。有时候,你会发现电脑可以直接使用IP访问网站但是无法使用域名进行访问,很大可能是路由器的DNS服务出问题了,最简单的解决方式就是将配置的DNS服务器IP地址改成公共的。
5. 访问本地域名服务器
如果以上的缓存都没有命中,那么逻辑上我们就需要通过外网的DNS服务来进行解析了,首先本地服务器(LDNS)来解析域名,这里的本地服务器是指城市或区域的DNS服务器,一般就有运营商部署在当地,距离近,性能好,并且也有缓存机制,几乎可以覆盖大多数的域名解析请求。
6. 转发与递归
如果你访问的域名比较冷门,本地服务器依然无法解析,则会进行转发,将此请求转发到更高级的运营商DNS服务器或者根DNS服务器,根DNS服务器会根据域名来返回顶级的域名服务器地址,本地服务器可以继续向顶级域名服务器请求解析。如此递归进行,直到解析成功,再将IP地址依次返回到我们的设备,并逐层做缓存,以便我们下次访问时可以快速得到响应。
上面过程中,我们有提到根域名服务器,其是最高级别的域名服务器,它负责返回顶级域名服务器,目前全球有13个根域名服务器站。顶级域名服务器用来针对某个顶级域名进行解析,例如.com顶级域名,.edu顶级域名,.cc顶级域名和.cn顶级域名等。顶级域名服务器在解析时会将查询到的主域名服务器返回。主域名服务器负责某个区域的域名解析,同样,主域名服务器会配套辅助域名服务器进行备份与分担负载。
五.DNS协议
前面说了这么多,都是宏观上的认识。现在,我们要讨论一些更深入的东西了。虽然对于DNS是干什么的,解析的过程是怎样的我们有了一些了解。但是DNS协议究竟是怎么操作的呢?IP数据是怎么得到的?我们可以手动来进行DNS解析么?要了解这些问题,首先需要对DNS协议本身做个了解。
DNS协议是工作在应用层的一种协议,全称Domain Name System。DNS协议是基于UDP之上实现的,前面说过UDP是工作在传输层的一种网络协议,等我们说到它的时候再深入探讨。现在你只需要知道,基于UDP任何人都可以实现一个DNS解析服务。DNS解析分为两步,首先需要客户端向服务器发送一个DNS请求报文,服务器收到报文,解析完成后再返回一个DNS报文给客户端,此报文中就包含解析的数据。
DNS协议规定其请求报文与响应报文的结构是一致的,都包含Header,Question,Answer,Authority,Additional这5个部分。
1. Header部分
Header部分的长度是一定的,固定为12个字节。DNS协议文档中有一张图,很好的描述了Header的数据结构:
ID:ID占了两个字节,它是一个标识符,由客户端请求的时候填充,DNS服务器解析后,会将此ID返回,用来让客户端将响应与请求对应起来。
配置字段:上图中第2行的都是配置字段,其占了两个字节。
QR占1为,设置为0表示当前是DNS请求报文,设置为1表示当前为DNS响应报文。
Opcode占4位,此值由请求报文设置,并且被复制到响应报文返回。其用来设置查询的类型,设置为0表示标准查询,即由域名解析出IP,设置为1表示反向查询,即由IP反查出域名,设置为2用来查询服务器的状态,3-15为保留字段,以待后续使用。
AA字段占1位,只在返回的响应报文中有,0表示返回数据的服务器不是权威服务器,1表示返回数据的服务器是权威服务器。需要注意,返回的响应报文中可能有多个应答,此字段表明的是第一个应答的服务器类型。
TC字段占1位,表示此报文是否由于数据的传输大小而被截断,当此字段的为1时,数据不可信。
RD字段占1位,该值需要在请求报文中设置,响应报文会直接复制该值。此值表示是否希望服务器进行递归查询。
RA字段占1位,其在响应报文中设置,表示服务端是否支持递归查询。
Z字段占3位,是保留字段。
rcode字段占4位,是响应报文的响应码,0表示没有错误;1表示请求格式有误,服务端无法解析;2表示服务器出错;3表示请求的域名不存在;4表示服务器不支持这类请求;5表示服务器拒绝此次请求;6-15是保留参数。
QDCOUNT:占16位,表明Question部分包含的实例个数,是无符号数。
ANCOUNT:占16位,表明Answer部分包含的回答个数,是无符号数。
NSCOUNT:占16位,表明Authority部分包含的授权服务器数量,是无符号整数。
ARCOUNT:占16位,表明Additional部分中包含的资源记录数量,是无符号整数。
2. Question部分
这个部分用来定义查询的问题,问题的个数在QDCOUNT指明,通常只会携带一个问题。每个问题的格式定义如下:
QNAME:此部分字节数不定,描述要查询的域名。在解析的时候,这部分以0x00结尾。需要注意,域名通常由符号“.”进行分割,每段的长度不定,QNAME每段的开头会先指明此段的长度,以huishao.cc域名为例,其构造出的QNAME部分如下:
0x07 0x68 0x75 0x69 0x73 0x68 0x61 0x6f 0x02 0x63 0x63 0x00
其中最后一个字节0x00标记了QNAME部分的结束,0x07表示第一段的长度为7个字节,即0x68 0x75 0x69 0x73 0x68 0x61 0x6f是第一段,通过查询ascii码对照表可知,这段数据就是huishao,同理,之后的一个字节为0x02,表示第二段的长度为2个字节,0x63对应ascii表中的字母c,最终可以解析为huishao.cc。
QTYPE:占两个字节,对应查询的类型,定义如下:
Type:意义 | 对应的值 |
A:iPv4主机地址 | 1 |
NS:权威域名服务器 | 2 |
MD:邮箱地址(弃用,使用MX) | 3 |
MF:转发邮箱(弃用,使用MX) | 4 |
CNAME:规范的别名 | 5 |
SOA:标记权威区域开始 | 6 |
MB:邮箱域名 | 7 |
MG:邮箱成员 | 8 |
MR:邮箱重命名域名 | 9 |
NULL:空的类型 | 10 |
WKS:服务描述 | 11 |
PTR:域名指针 | 12 |
HINFO:主机信息 | 13 |
MINFO:邮箱或者邮件列表信息 | 14 |
MX:邮件交换 | 15 |
TXT:字符串 | 16 |
AAAA: IPv6域名 | 28 |
上面列举的查询类型中,有两个我们需要额外关注,A和CNAME,A类型即是我们查询域名IP所要使用的,CNAME别名技术也很常用,后面会介绍。
QCLASS:占两个字节,表明查询的类别,定义如下:
CLASS:意义 | 对应的值 |
IN:Internet查询 | 1 |
CS:弃用,RFC查询 | 2 |
CH:the CHOAS class | 3 |
HS:Hesiod | 4 |
进行DNS解析时,只需要设置成IN类即可。
3. Answer部分
这部分是响应的返回数据,可能包含多条资源记录,其格式如下:
NAME:此记录所属的域名,长度不定,需要注意,这一部分存放的可能是真正的域名(格式和QNAME一致),也可能是指针,指向真正存放域名的字节位置,甚至可以是一部分是域名,一部分是指针。这样做的好处是可以节省响应报文的数据空间,当检查到某个字节的高两位为11时,则此字节及之后一个字节就是一个指针。例如对于huishao.cc域名的解析,其响应的完整的DNS报文如下(16进制):
b3 a4 81 80 00 01 00 01 00 00 00 00 07 68 75 69
73 68 61 6f 02 63 63 00 00 01 00 01 c0 0c 00 01
00 01 00 00 02 58 00 04 b9 c7 6d 99
其中开头的12个字节为Header部,随后的16个字节为Question部,后面的即为Answer部,Answer部分开头的c0字节高两位为11,表明其是一个指针,占两个字节,c0,0c两个字节将前两位的1去掉后为十进制数12,表明NAME的真实值在第12个字节处开始,即复用了QNAME的数据。
TYPE:占两个字节,与QTYPE定义一致。
CLASS:占两个字节,与QCLASS定义一致。
TTL:占4个字节,此字段非常重要,标记了缓存的有效时长,单位是秒。顺便分析一下上面的数据,此DNS解析数据的缓存有效期为0x0258,即600秒,10分钟。
RDLENGTH:占两个字节,表明RDATA字段的字节数。
RDATA:真正的解析数据,与TYPE有关,如果是IPv4域名解析,此处为解析的结果。
4. Authority,Additional
这两部分的数据结构与Answer部分完全一致,解析方式也完全一致。
六.纸上得来终觉浅,绝知此事要躬行
通过前面的介绍,DNS协议的工作原理应该是明了了,如果需要更深入的了解细节,可以阅读其官方的文档:
https://datatracker.ietf.org/doc/html/rfc1035
当然,如果你还是感觉云里雾里也没有关系,我们通过实践来验证理论。
1.抓个活物来看看
Wireshark是一个网络封包分析软件,能够截取网络封包,对于网络传输的数据包进行分析十分方便。我们打开此软件后,找一个域名进行访问,即可抓取到对应的DNS数据包,以huishao.cc为例,如下图所示:
可以看到,Wireshark可以分析出此次网络交互的时间,发起方IP,目标方IP,协议类型,数据长度和相信信息。在上面的示例中,第一条记录是DNS请求报文,第二条记录是DNS响应报文。我们先看看DNS请求报文的数据:
可以看到,Wireshark将每一层网络协议都分析了出来,我们先只关注最上层的Domian Name System部分,这部分的十六进制数据是上图中选中的部分。可以发现其和我们上面介绍的协议格式是一一对应的。在看响应报文:
数据的格式也是完全对应的,理论诚不欺我啊。
2. 手动实现DNS解析
下面,我们可以以huishao.cc域名为例,手动使用UDP协议来试一试发送DNS请求以及对请求到的数据进行解析。首先先看完整的测试代码:
上面的代码有详细的注释,你可以尝试运行下进行域名解析,需要注意,上面填写的192.168.1.1是本地路由器的域名服务器地址,你需要将其替换成自己的,当然你也可以使用通用的域名解析服务器,如114.114.114.144。上面的代码采用C语言编写,因此在处理数据的时候会有一些复杂,有些点需要注意。
1. 关于结构体位域
简单理解,位域可以让结构体中的数据以Byte为单位已经存储,例如上面定义的DNS_HEADER结构体,我们按照DNS协议的结构对其内数据所占的位进行了定义,有一点需要额外注意,在定义结构体时,位域字段的顺序与实际填充的顺序是相反的,位域的填充是从低字节开始的,如上代码所示,对于1个字节的位域来说,我们定义的时候,先定义的RD字段,最后定义的QR字段,实际在存储数据时,这一个字节的最高位会存储QR,最低位会存储RD。
2. 关于字节对齐
在定义结构体时,还有一个细节需要注意,如果结构体中的数据字节数不是一致的,则其创建的内存大小可能和实际所需要的并不一致,例如R_DATA结构体,其中有int和short类型的数据,则其会以4字节为标准进行对齐,我们需要手动设置其对齐位数,不然后续数据填充时会出现偏差。
3.网络字节序与主机字节序
网络字节序是TCP/IP协议中定义的一种数据格式,其采用的是大端(big-endian)的排序方式,即对于一个字(两个字节)的数据,低字节在前,高字节在后。这与我们可读的主机字节序刚好是相反的,在C语言中,使用htons可以把short类型的数据进行网络和主机字节序的转换,htonl把long类型的数据进行网络和主机字节序的转换。
可以在如下地址下载到完整的上述代码:
https://gitee.com/jaki/dns_c
温馨提示,上面代码中解析的域名只返回了一个A类型的解析应答,如果你解析其他域名,可能会有很多CNAME类型的应答,应答个数也可能不止一个,你可以尝试下优化下代码,完整的实现DNS的解析逻辑。
七. 结尾
本篇博客到此就结束了,我相信你对从域名获取到IP的过程有了更多的认识,如果遇到了域名解析的问题,你应该明白如何查看响应结果来定位问题了,但是,这只是我们日常使用的网络中的第一步,目前我们连应用层的核心都还没有接触到,不积跬步,无以至千里,与君共勉。