本博文介绍如何使用 Python 网络编程,设计并实现客户端的 DNS 域名解析及缓存服务。
域名系统(DNS)是一个庞大的、全球的分布式数据库,,它主要用来把主机名转换成 IP 地址,DNS 以及相关系统之所以存在,主要有以下两个原因:
它们可以使人们比较容易地记住名字,比如说对于百度,我们更容易记住 www.baidu.com,而不是 IP 地址 36.152.44.95;
它们允许服务器改变 IP 地址,但是还用同样的名字。
DNS 的工作原理是这样的,对于我们的输入,它提供一系列的提名回答,而对每个提名又给出一个更详细的答案,直到获得最终答案,这句话是什么意思呢?
举一个例子,我们如果要查询清华大学官网的 IP 地址 www.tsinghua.edu.cn,首先,我们的程序会和操作系统指定的本地域名服务器通信,并向其发送一次查询请求,本地域名服务器是一个递归的域名服务器,如果这个服务器无法解析我们的请求,它收到请求后就会以 DNS 客户机的身份向其它域名服务器查询,直到得到最终的 IP 地址告诉本机,这是一个递归查询的过程。
那么本地域名服务器如何向其他域名服务器查询呢?是这个样子的,它是一个迭代查询的过程:
虽然这整个过程看起来很复杂,但是操作系统已经提供了执行基本 DNS 查找的服务,这些服务对大多数程序来说是足够的,直接节约了我们花费在处理各种 DNS 查询上的时间。
并且 Python 在它的 socket 标准库中,提供了访问这些基本操作系统服务的接口,一些第三方库还提供了很多更高级的功能。
最基本的 DNS 查询是正向查询,它根据一个主机名来查找 IP 地址,在 Python 当中,为了实现 DNS 正向查询,我们将用到函数 socket.getaddrinfo()
,Python 是这么定义它的:
getaddrinfo(host, port[, family[, socktype[, proto[, flags]]]])
其中,host 参数就是我们输入的域名,port 参数是域名对应主机的端口号,其他的参数只有当我们想把结果直接传递给 socket.socket()
或 socket.connect()
的时候才用到,它们会在输出中限制显示什么协议,以及为了建立 socket 而根据默认值得到的结果。
我们可以设置 port 值为 None,然后省略其他的参数来进行一个基本的查询,socket.getaddrinfo()
函数的返回值是一个 tuple 元组的列表,每一个 tuple 的格式如下,其中,sockaddr 实际上就是我们要查询的域名的 IP 地址:
(family, socktype, proto, canonname, sockaddr)
socket.getaddrinfo()
返回一个 tuple 的列表是因为对于一个域名的查询,可能有多个答案,即一个域名可能有多个 IP 地址,因为一些网站流量很大,同时将域名映射在多个服务器上可以有效解决负载问题,不过,如果我们只是想得到一个简单的 IP 地址来连接,我们选择列表中的第一个 tuple 即可。
对于上述所说,我们通过例子来验证一下,我们运行下面的代码:
print(socket.getaddrinfo("www.baidu.com", None))
可以看到,确实打印了一个列表,列表的每个元素是一个元组:
因此我们就可以根据索引找到我们要的 IP 地址:
print(socket.getaddrinfo("www.baidu.com", None)[0][4][0])
运行代码,结果如下:
如果只需要找到域名的一个 IP 地址的话,还可以使用 socket.gethostbyname()
函数:
print(socket.gethostbyname("www.baidu.com"))
它的参数是要查询的域名,返回的是域名对应的一个 IP 地址:
以上就是使用 socket 标准库实现 DNS 正向查询的过程,我们总结一下,有如下两个非常简便的方法实现 DNS 正向查询:
print(socket.getaddrinfo("www.baidu.com", None)[0][4][0])
print(socket.gethostbyname("www.baidu.com"))
一般地,在 DNS 查询过程中,我们会有一个 DNS 缓存机制,DNS 缓存可以减小下一次查询时域名到 IP 地址的映射时间消耗,DNS 缓存的实现可以在操作系统、虚拟机、解释器,以及应用层上进行。
在获取域名和 IP 地址的对应关系后,我们可以用列表的形式来维护这些关系,以在应用层上实现我们自己的 DNS 缓存:
def dnsQuery(domain_name_list, ip_list, source): # source 为域名,表示要获得域名对应的 IP 地址
if source in domain_name_list: # DNS 缓存命中
print("DNS 缓存命中,直接读取...")
index = domain_name_list.index(source)
target = ip_list[index]
print("读取成功!")
else: # DNS 缓存缺失
print("DNS 缓存缺失,查询域名服务器中...")
domain_name_list.append(source)
target = socket.getaddrinfo(source, None)[0][4][0] # 获得相应的 IP 地址
ip_list.append(target)
print("查询成功!")
return target
这里用 domain_name_list
域名列表和 ip_list
IP 地址列表以索引的方式来维护域名和 IP 地址之间的映射关系,我们通过一个例子试一下:
可以看到,在上面的例子中,第一次查询 www.baidu.com 的 IP 地址时,域名列表和 IP 地址列表为空,所以 DNS 缓存缺失,于是使用 socket.getaddrinfo()
函数去查询域名服务器,当第二次查询时,域名列表和 IP 地址列表当中已经存有了对应的域名和 IP 地址,因此 DNS 缓存命中,从 IP 地址列表当中直接读取。
上述过程的完整代码如下:
import socket
def dnsQuery(domain_name_list, ip_list, source): # source 为域名,表示要获得域名对应的 IP 地址
if source in domain_name_list: # DNS 缓存命中
print("DNS 缓存命中,直接读取...")
index = domain_name_list.index(source)
target = ip_list[index]
print("读取成功!")
else: # DNS 缓存缺失
print("DNS 缓存缺失,查询域名服务器中...")
domain_name_list.append(source)
target = socket.getaddrinfo(source, None)[0][4][0] # 获得相应的 IP 地址
ip_list.append(target)
print("查询成功!")
return target
if __name__ == "__main__":
domain_name_list = []
ip_list = []
domain_name = "www.baidu.com"
ip = dnsQuery(domain_name_list, ip_list, domain_name)
print(f"{domain_name} 的 IP 地址:{ip}")
ip = dnsQuery(domain_name_list, ip_list, domain_name)
print(f"{domain_name} 的 IP 地址:{ip}")
除了 socket 标准库,我们还可以使用一些第三方库来实现 DNS 查询的一些更高级的功能,比如说 dnspython 这个强大而便利的工具,接下来我们来了解一下如何通过 dnspython 实现 DNS 正向查询及缓存。
对于 dnspython 的安装,pip 一下即可:
pip install dnspython
对于 DNS 的正向查询,dnspython 也可以很简便地实现:
import dns.resolver
a = dns.resolver.query("www.tsinghua.edu.cn", 'A') # A 表示将主机名转换为 IP 地址
ip = a.response.answer[0].items[0].address # 获得相应的 IP 地址
print(ip)
可以看到,通过几句代码就可以完成 IP 地址的查询:
不过,关于 dnspython 我还不熟悉,上面的代码中的一些参数也不清楚,但是我觉得在以后使用的过程中应该会逐渐熟悉它们。
对于 dnspython,类似地,我们也可以使用它来实现应用层上的 DNS 缓存机制:
def dnsQuery(domain_name_list, ip_list, source): # source 为域名,表示要获得域名对应的 IP 地址
if source in domain_name_list: # DNS 缓存命中
print("DNS 缓存命中,直接读取...")
index = domain_name_list.index(source)
target = ip_list[index]
print("读取成功!")
else: # DNS 缓存缺失
print("DNS 缓存缺失,查询域名服务器中...")
domain_name_list.append(source)
a = dns.resolver.query(source, "A") # A 表示将域名转换为 IP 地址
target = a.response.answer[0].items[0].address # 获得相应的 IP 地址
ip_list.append(target)
print("查询成功!")
return target