Python 网络编程学习笔记(二)——DNS 域名解析客户端程序设计

本博文介绍如何使用 Python 网络编程,设计并实现客户端的 DNS 域名解析及缓存服务。


1 基本概念

域名系统(DNS)是一个庞大的、全球的分布式数据库,,它主要用来把主机名转换成 IP 地址,DNS 以及相关系统之所以存在,主要有以下两个原因:

  • 它们可以使人们比较容易地记住名字,比如说对于百度,我们更容易记住 www.baidu.com,而不是 IP 地址 36.152.44.95;

  • 它们允许服务器改变 IP 地址,但是还用同样的名字。

DNS 的工作原理是这样的,对于我们的输入,它提供一系列的提名回答,而对每个提名又给出一个更详细的答案,直到获得最终答案,这句话是什么意思呢?

举一个例子,我们如果要查询清华大学官网的 IP 地址 www.tsinghua.edu.cn,首先,我们的程序会和操作系统指定的本地域名服务器通信,并向其发送一次查询请求,本地域名服务器是一个递归的域名服务器,如果这个服务器无法解析我们的请求,它收到请求后就会以 DNS 客户机的身份向其它域名服务器查询,直到得到最终的 IP 地址告诉本机,这是一个递归查询的过程。

那么本地域名服务器如何向其他域名服务器查询呢?是这个样子的,它是一个迭代查询的过程:

  • 本地域名服务器首先向一个根域名服务器查询,根域名服务器会告诉本地域名服务器,下一次应该查询顶级域名服务器 dns.cn 的 IP 地址;
  • 然后本地域名服务器向顶级域名服务器 dns.cn 进行查询,顶级域名服务器 dns.cn 会告诉本地域名服务器,下一次应查询的权限域名服务器 dns.edu.cn 的 IP 地址;
  • 接着本地域名服务器就会向权限域名服务器 dns.edu.cn 进行查询,然而权限域名服务器 dns.edu.cn 还是不能给出最后的查询回答,因此它会告诉本地域名服务器下一个权限域名服务器 tsinghua.edu.cn 的 IP 地址;
  • 本地域名服务器再向权限域名服务器 tsinghua.edu.cn 查询,这个权限域名服务器就会告诉本地域名服务器,说它知道清华大学官网的 IP 地址,并把该 IP 地址返回给本地域名服务器。

虽然这整个过程看起来很复杂,但是操作系统已经提供了执行基本 DNS 查找的服务,这些服务对大多数程序来说是足够的,直接节约了我们花费在处理各种 DNS 查询上的时间。

并且 Python 在它的 socket 标准库中,提供了访问这些基本操作系统服务的接口,一些第三方库还提供了很多更高级的功能。


2 使用 socket 标准库实现 DNS 正向查询及缓存

最基本的 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 地址之间的映射关系,我们通过一个例子试一下:
Python 网络编程学习笔记(二)——DNS 域名解析客户端程序设计_第1张图片
可以看到,在上面的例子中,第一次查询 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}")

3 使用 dnspython 第三方库实现 DNS 正向查询及缓存

除了 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

你可能感兴趣的:(Python 网络编程学习笔记(二)——DNS 域名解析客户端程序设计)