读数笔记_python网络编程3(4)

4.套接字名与DNS

讨论网络地址,描述将主机名解析为原始IP地址的分布式服务

4.1. 主机名与socket

浏览器汇总一般键入域名。有些域名标识整个机构。如,python.org,而另一些指定了主机/服务。如,www.google.com/asaph.rhodesmill.org。访问一些站点时,可以使用主机名的缩写。如,asaph,站点会自动填充主机名剩余部分。无论已经在本地进行了任何自定义设置,使用包含了顶级域名及其他所有部分的完全限定域名(fully qualified domain name)总是正确无误的。

4.1.0. 顶级域名(TLD):要么是.com、.net、.org、.gov、.mil,要么是两个字母组成的国际公认国家代号。现在出现了跟多顶级域名,如.beer,区分就困难了。

4.1.0.1. 每个TLD都有自己的服务器,由机构运行,负责为该TLD下所有的域名进行授权。当注册一个域名时,机构会在Serv上增加一个相应域名的条目。当世界上任意一处运行的Cli希望解析属于该域名时,顶级Serv就会把Cli请求转至机构自己的域名Serv。机构就可以为其创建各种主机名,返回对应的地址。这种名称系统将顶级名称与与机构各自Serv维护的名称结合起来。世界各地使用该系统对名称查询作出相应的Serv集合提供了域名服务(DNS, Domain Name Service)

4.1.0.2. 所有需要提供某种形式的socket名,作为参数的主要socket方法:

4.1.0.2.1. mysocket.accept():由TCP流的监听socket调用。当有准备好发送至程序的连接请求时,该方法就会被调用。会返回一个二元组,第二项是已连接的远程地址(第一项是新建的连接至远程地址的socket)
4.1.0.2.2. mysocket.bind(address):将特定的本地地址(为要发送的数据包的源地址)分配给socket。如其他机器要发起连接请求,该地址也可作为要连接的地址。
4.1.0.2.3. mysocket.connect(address):通过socket发送的数据会被传输至特定的远程地址。对于UDP来说,只是设置了一个默认地址。如调用方没有使用sendto()和recvfrom()方法,而是使用了send()和recv(),就会使用这一默认地址。该方法本身没有马上做任何网络通信操作。对于TCP,该方法会与另一台机器通过三次握手建立一个新的流,且在建立失败时抛出一个Py异常。
4.1.0.2.4. mysocket.getpeername():返回与socket连接的远程地址
4.1.0.2.5. mysocket.getsockname():返回socket自身的本地端点地址
4.1.0.2.6. mysocket.recvfrom(...):用于UDP,返回一个二元组,包含返回数据的字符串和数据的来源地址
4.1.0.2.7. mysocket.sendto(data, address):未连接的UDP-port想特定远程地址发送数据。

4.1.1. socket的5个坐标

创建和部署每个socket对象,总共需要作出5个主要的决定,hostname和ip只是其中最后两个,创建及部署socket步骤如下:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('127.0.0.1', 1060))
指定了4个值: 两个用来对socket做配置,另外两个提供bind()调用需要的ip。还有第5个坐标

4.1.1.1. 地址族(address family)的选择是最重要的决定。某个特定的机器可能连接到多个不同类型的网络。对地址族的选择指定了想要进行通信的网络类型。在POSIX上流行AF_UNIX地址族,提供的连接直接运行于同一机器的程序之间,连接的是文件名,而不是主机名和端口号组成的地址。

4.1.1.2. 套接字类型(socket type):给出希望在已经选择的网络上使用特定通信技术。尽管UDP和TCP确实是AF_INET协议族特有的,但是socket为基于数据报的socket创建了更通用的名字SOCK_DGRAM,提供可靠传输与流量控制的数据流用SOCK_STREAM来表示,只需使用这两个符号就足以覆盖大量不同协议族的很多协议了。

4.1.1.3. 第3个参数是协议(protocol),该参数很少使用,常常不指定该参数,或把它设为0,表示自动选择协议。如果希望在IP层上使用流,选择TCP。想使用数据报,选择UDP。实际应用中,几乎不需要。

4.1.1.4. IP

4.1.1.5. port

4.1.1.6. socket名之所以由hostname和port两部分组成,因为特别指定了socket的前3个坐标,5个坐标,其实是新建socket所必须的3个固定坐标,后面跟着使用特定地址族进行网络连接所需的任意数量的坐标

4.1.2. IPv6

4.1.2.1. AF_INET之外的另一个地址族,未来的主流地址族-IPv6。32位地址只能提供40亿个IP,不足以为每个人都提供IP,另Py程序兼容IPv6,需要做的非常简单。

Py中,可通过检查socket内置的has_ipv6来直接测试当前平台是否支持IPv6,并不表示实际的IPv6已经运行并配置完成,可用来发送数据包了,仅表明OS的实现是否提供IPv6支持,与是否已经使用IPv6无关。

In[63]: import socket
In[64]: socket.has_ipv6
Out[64]: True

4.1.2.2. IPv6对Py代码的影响:

4.1.2.2.1. 使用AF_INET6来创建socket
4.1.2.2.2. socket名不仅有IP和port组成,还包括提供了“流"信息和“范围”标识的额外坐标
4.1.2.2.3. IPv6的表达形式包含大量冒号、16进制数值

相较于IPv4实现,IPv6协议对链路层安全等很多特性提供了更完整的支持

4.2. 现代地址解析

Py-socket用户工具集最强大的工具之一---getaddrinfo():socket模块中涉及地址的众多操作之一,可能是将username和port转换为可供socket方法使用的地址时,所需的唯一方法。该方法能指明要创建的连接所需的一切已知信息,将返回全部坐标,这些坐标是创建并将socket连接至指定目标地址所必须的。
>>> import socket
>>> from pprint import pprint
>>> infolist = socket.getaddrinfo('gatech.edu', 'www')
>>> pprint(infolist)
[(2, 1, 6,'',('130.207.244.244', 80)),(2, 1, 17,'',('130.207.244.244', 80))]
>>> info = infolist[0]
>>> info[0:3]
(2, 1, 6)
>>> s = socket.socket(*info[0:3])
>>> info[4]
('130.207.244.244', 80)
>>> s.connect(info[4])

info变量包含了创建一个socket并使用该socket发起一个连接需要的所有信息。提供了地址族、类型、协议、规范名称、地址信息。
提供给getaddrinfo()的参数有哪些?
请求的是连接到主机gatech.edu提供的HTTP服务所需的可能方法,返回值是包含两个元素的列表。返回值中得知,两种方法可以用来发起该连接。可创建一个使用IPPROTO_TCP(代号为6)的SOCK_STREAM-socket(socket类型为1),也可创建一个使用IPPROTO_UDP(代号为17)的SOCK_DGRAM(socket类型为2)的socket。

4.2.0. 3-2中使用了AF_INET这样的真实符号,明确了socket的底层工作机制,而生产环境中的Py代码除非要向getaddrinfo()指明行啊要的地址类型,否则不会引用socket模块中的任何符号。将使用getaddrinfo()返回值的前3项作为socket()构造函数的参数,使用返回值的第5项作为传入地址,用于任何需要socket地址的调用,如connect()

4.2.0.1. getaddrinfo()除了允许提供hostname外,还允许提供www(不是int)作为port名。如果用户想使用www/smtp这样的符号作为port,而不使用80/25,就无需再之前的Py代码中进行额外的调用了。

如何使用getaddrinfo()来支持3种基本网络操作(绑定、连接、识别已经向我们发送信息的远程主机)

4.2.1. 使用getaddrinfo()为Serv绑定port

想要得到一个地址,将其作为参数提供给bind(),原因可能是正在创建一个Serv-socket,也可能是希望Cli从一个可预计的地址连接至其他主机,此时可调用getaddrinfo(),将主机名设为None,但提供port与socket类型,如果某个字段为数字,可用0来表示通配符。
>>> from socket import getaddrinfo
>>> getaddrinfo(None, 'smtp', 0, socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
[(2,1,6, '', ('0.0.0.0', 25)), (10, 1, 6, '', ('::', 25, 0, 0))]
>>> getaddrinfo(None, 53, 0, socket.SOCK_DGRAM, 0, socket.AI_PASSIVE)
[(10, 2, 17, '', ('::', 53, 0, 0)), (2, 2, 17, '', ('0.0.0.0', 53))]

做了两个查询:
1)使用字符串作为port标识符 ->想知道,如果使用TCP来支持SMTP数据传输,应该bind()到那个地址,如果想通过bind()到本机上的一个特定IP,应该通过bind()把socket绑定到哪个地址,该查询返回的答案是合适的通配符地址,表示可以绑定到本机上的任何IPv4及IPv6接口。还需提供正确的socket地址族、socket类型及协议。
相反,如果通过bind()绑定到本机的一个特定IP,且该地址已配置完成,应省略AI_PASSIVE,并制定hostname。
2)使用原始数字port
以下为两种可用于尝试将socket绑定到localhost的方式:

>>> getaddrinfo('127.0.0.1', 'smtp', 0, socket.SOCK_STREAM, 0)
[(, , 0, '
', ('127.0.0.1', 25))]
>>> getaddrinfo('localhost', 'smtp', 0, socket.SOCK_STREAM, 0)
[(, ,
0, '', ('::1', 25, 0, 0)), (, , 0, '', ('127.0.0.1', 25))]

如果使用IPv4地址表示本地主机,只会接收通过IPv4发起的连接;如果使用localhost,IPv4/6的本地名在该机器上均可用。

4.2.2. 使用getaddrinfo()连接Serv

除了绑定本地IP自行提供服务外,还可使用getaddrinfo()获取连接到其他服务所需的信息。查询服务时,可使用一个空字符串表示要通过自环接口连接回本机,也可提供一个包含IPv4/6/主机名的字符串来指定目标地址

4.2.2.1. 调用connect()/sendto()连接Serv/向Serv发送数据时,调用getaddrinfo(),并设置AI_ADDRCONFIG标记,将把计算机无法连接的所有IP都过滤掉。

1)如,某机构可能既有IPv4的IP,也有IPv6的IP。如果特定主机只支持IPv4,希望将结果中的非IPv4过滤掉。
2)本机只有IPv6,连接的Serv却只支持IPv4,也需指定AI_V4MAPPED,指定该标记后,会将IPv4地址重新编码为可实际使用的IPv6
将上述拼凑起来,得到在socket连接前,使用getaddrinfo()的常用方法

>>> getaddrinfo('ftp.kernel.org', 'ftp', 0, socket.SOCK_STREAM, 0, socket.AI_ADDRCONFIG | socket.AI_V4MAPPED)
[(2, 1, 5, '', ('204.152.191.37', 21)), (2, 1, 6, '', ('149.20.29.133', 21))]

就从getaddrinfo()的返回值中得到了所需的信息:这是一个列表,包含了通过TCP连接ftp.kernel.org主机FTP端口的所有方式。返回值中包括了多个IP。为了负载均衡,该Serv部署在了多个不同IP上。当返回多个地址时,通常应该使用返回的第一个IP。只有连接失败,才尝试剩下的IP。

4.2.2.2. 另一个查询,想通过该查询得知如何连接到IANA的HTTP接口。

getaddrinfo('iana.org', 'www', 0, socket.SOCK_STREAM, 0, socket.AI_ADDRCONFIG | socket.AI_V4MAPPED)
[(, , 0, '', ('192.0.43.8', 80))]

4.2.3. 使用getaddrinfo()请求规范主机名

4.2.3.1. 需要知道属于对方Ip的官方主机名,可能由于我们正在建立一个新的链接,可能是由于某个Serv-socket刚接受了一个连接请求。->会带来威胁:机器进行从IP到主机名的发现查询时,IP的拥有者可以令DNS返回任意值作为查询结果,如google.com/python.org。在请求属于某IP的hostname时,IP的拥有者能完全控制想返回的字符串

4.2.3.2. 因为对规范主机名的查询,会将IP映射到一个hostname,而不是将hostname映射到IP,故称之为发现DNS查询。得到返回的hostname后,要先查阅并确认它,可以被解析为原始的IP,才能信任该返回结果。

规范主机名查询相当耗时,导致对全球DNS的一次额外的查询往返,故在日志时常常会跳过。如果一个服务会反向查询与每个IP对应的主机名,会使得连接响应变得异常缓慢。OS-MA的常用作法是只对IP进行日志记录,如果某个IP引发了问题,可以先从日志文件中找到该IP,手动查询对应的hostname

4.2.3.2. 反向查询,只要在运行getaddrinfo()时设置AI_CANONNAME。返回元组中的第4项将包含规范主机名。

>>> getaddrinfo('iana.org', 'www', 0, socket.SOCK_STREAM, 0, socket.AI_ADDRCONFIG | socket.AI_V4MAPPED | socket.AI_CANONNAME)
[(, , 0, 'iana.org', ('192.0.43.8', 80))]

4.2.4. 其他getaddrinfo()标记

4.2.4.1. AI_ALL: 如果希望在通过IPv6连接的主机上看到所有弟子,可将该标记与AI_V4MAPPED标记结合起来,返回的列表会包含已知的与目标主机对应的所有地址

4.2.4.2. AI_NUMERICHOST:禁止对hostname参数以cern.ch文本方式进行解析,只会将hostname作为IPv4/6来解析,如74.207.234.78/fe80::fcfd:4aff:fecf:ea4e。设置后速度更快,不会DNS往返,可以防止OS被不可信的用户输入控制,避免强制查询受他方控制的名称Serv

4.2.4.3. AI_NUMERICSERV: 禁用了www符号形式的port名,坚持使用"80"的port。在POSIX-OS上,解析一个符号形式的port,只需快速扫描/etc/services文件(需要检查/etc/nsswitch.conf文件的服务选项进行确认)

4.2.4.4. Py会检测字符串是否需要特殊编码方式,自动转换,Py包含了一个'idna'编解码器,能完成与国际域名间的相互转换

4.2.5. 原始名称服务程序

getaddrinfo()流行前,程序员通过OS支持的更简单的名称服务程序来进行socket的编程,多数是硬编码,只支持IPv4,应该避免使用这些程序。
1)socket模块的标准库页面找到相关的文档,有两个调用能返回当前机器的hostname
socket.gethostname()
'DESKTOP-S1A6RSJ'
socket.getfqdn()
'DESKTOP-S1A6RSJ'
2)还有两个能够对IPv4-hostname和IP进行相互转换
socket.gethostbyname('cern.ch')
'188.184.9.234'
socket.gethostbyaddr('188.184.9.234')
('webrlb01.cern.ch', [], ['188.184.9.234'])
3)有三个程序可以通过OS已知的符号名查询协议号及port
socket.getprotobyname('UDP')
17
socket.getservbyname('www')
80
socket.getservbyport(80)
'http'
4)想要获取Py的机器的主IP,可以将完全限定主机名传各gethostbyname()调用。
socket.gethostbyname(socket.getfqdn())
'192.168.137.1'

4.2.6. 代码中使用getsockaddr()

# 4-1 www_ping.py
import argparse, socket, sys

def connect_to(hostname_or_ip):
    try:
        infolist = socket.getaddrinfo(
            hostname_or_ip, 'www', 0, socket.SOCK_STREAM, 0,
            socket.AI_ADDRCONFIG | socket.AI_V4MAPPED | socket.AI_CANONNAME,
        )
    except socket.gaierror as e:
        print('Name service failure:', e.args[1])
        sys.exit(1)

    info = infolist[0] # per standard recommendation, try the first one
    # print('info_{}'.format(info)) # info_(, , 0, 'mit.edu', ('23.213.151.198', 80))
    socket_args = info[0:3]
    # print('socket_args_{}'.format(socket_args)) # socket_args_(, , 0)
    address = info[4]
    # print('address_{}'.format(address)) # address_('23.213.151.198', 80)
    s = socket.socket(*socket_args)
    try:
        s.connect(address)
    except socket.error as e:
        print('Network failure:', e.args[1])
    else:
        print('Success: host', info[3], 'is listening on port 80')

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Try connecting to port 80')
    parser.add_argument('hostname', help='hostname that you want to contact')
    connect_to(parser.parse_args().hostname)
>python www_ping.py mit.edu
Success: host mit.edu is listening on port 80
>python www_ping.py smtp.google.com
Name service failure: getaddrinfo failed
>python www_ping.py no-such-host.com
Name service failure: getaddrinfo failed
该脚本有3点值得注意:

4.2.6.1. 该脚本完全通用,没有提到使用IP协议/TCP作为传输方式,如果输入了hostname是通过AppleTalk连接的,那么getaddrinfo()将返回AppleTalk的socket族、类型及协议,最终创建并连接的socket就是该类型的

4.2.6.2. getaddrinfo()调用的失败会引起特定名称服务错误gaierror,而不是在脚本末尾检测的普通网络故障,导致的socket错误。

4.2.6.3. 没有未socket的构造函数传入3个单独的参数,使用*传入了参数列表,表示socket_args列表中的3个元素会被当做3个单独的参数传入到构造函数中。返回地址会被当做单独的单元,传入所有需要使用它的socket程序中。

4.3. DNS协议:域名系统(DNS,Domain Name System)是成千上万互联网主机相互协作,对hostname与IP映射关系查询做出响应的一种机制。不用记住IP地址82.xx.xx.xx,DNS就是背后支撑这一切的机制。

DNS协议
目的: 解析hostname,返回IP地址
标准: RFC 1034与RFC 1035(1987)
传输层协议: UDP/IP与TCP/IP
端口号: 53
库: 第三方,包括dnspython3

为完成解析,PC发送的信息会遍历Serv组成的层级结构。PC和名称Serv有可能无法解析hostname。原因是该hostname既不属于本地机构,也没有在近期访问并仍然处于名称Serv的缓存中。这种情况下,需查询世界上的某个顶级名称Serv,获取负责查询的域名的DNS,一旦返回了DNS的IP,就可以反过来访问该IP,完成域名查询
如何开始这一操作

4.3.0. 以www.python.org这一域名为例。若浏览器需要解析该地址,会运行一个类似于getaddrinfo()的调用,请求OS对该域名进行解析。OS本身知道其是否运行自己的名称Serv,连接的网络是否会提供名称Serv。

4.3.0.1 PC通常会在链接到网络时通过DHCP,自动配置名称Serv信息,可通过公司办公室的LAN,也可通过无线网络/DSL连接到网络。OS-MA设置机器时会手动配置DNS的IP地址。无论上述那种情况,都必须指定DNS的原始IP,因为在能够通过其他方法连接到DNS前,不能进行任何DNS查询。

4.3.0.2 又是,对ISP提供的DNS及性能不满意,会自己配置一个三方的DNS,如谷歌的8.8.8.8和8.8.4.4,不过,要进行名称解析,必须指定DNS-Sevr

4.3.0.3 即使不查询域名,PC也知道一些hostname对应的IP。当调用getaddrinfo()时,其实OS做的第一件事不是想DNS查询hostname,DNS查询相当耗时,常常是最后一个选择。OS在向DNS查询hostname前,会从其他地方查询。

4.3.0.4. 使用POSIX-OS,要查询的文件取决于/etc/nsswitch.conf中的hosts条目。如,Ubuntu,先检查/etc/hosts,会尽可能使用多播DNS的专用协议,只有操作失败/不可用时,才会启用完整的DNS查询来获取与hostname对应的IP。

4.3.0.5. 使用Windows, 取决于控制面板选项。

4.3.0.6.假如本地机器上没有www.python.org这一域名,也没有在足够短的时间内访问过该域名,则浏览器的本地缓存上没有与该域名对应的IP,这种情况下,PC会查询本地DNS,会发送一个基于UDP的DNS查询数据包,问题就交给真正的DNS服务器了

1) 我们的DNS会检查自己最近查询域名的缓存,看www.python.org是否最近几分钟或几小时内由其他机器想DNS查询过,如果存在一个条目,且未过期,就可以马上返回IP,若我们今天是第一个尝试访问www.python.org的人,那么DNS就需要从头开始查询与hostname对应的IP地址。
2) 我们的DNS服务器会从世界上DNS层级结构的最顶层开始,递归查询www.python.org。根节点的名称Serv能识别所有顶级域名(TLD),如.com、.org、.net,且存储了负责响应顶级域名的Serv群信息。为了在连接至域名系统(DNS)前找到域名Serv,名称Serv软件通常内置了顶级Serv的IP,经过一次UDP往返后,我们的DNS就能获取保存完整.org域名索引的Serv了。
3) 发送第二个DNS请求,这次发送给某个.org服务器,徐闻负责保存python.org的Serv,可使用whois获取顶级Serv中存储的关于某域名的信息。

$ whois python.org
Domain Name: PYTHON.ORG
...
Registrar URL: http://www.gandi.net
Updated Date: 2019-02-25T03:01:59Z
Creation Date: 1995-03-27T05:00:00Z
Registry Expiry Date: 2020-03-28T05:00:00Z
Registrar Registration Expiration Date:
...
Name Server: NS3.P11.DYNECT.NET
Name Server: NS1.P11.DYNECT.NET
Name Server: NS2.P11.DYNECT.NET
Name Server: NS4.P11.DYNECT.NET
DNSSEC: unsigned

无论身处世界何处,对任何属于python.org的hostname的DNS请求,都会被发送至上面列出的4个DNS中的一个。现在,我们的DNS已经完成了与根节点DNS以及顶级.org DNS的通信,可以直接向NS3.P11.DYNECT.NET查询python.org了,根据python.org对其名称Serv的不同配置,DNS还需进行查询的次数也会不同。上面的4个Serv之一可以直接返回www.python.org查询的结果,我们的DNS服务器也就可以向浏览器返回一个UDP数据包(包含了对应的IP)。

4.3.0.7. 这一过程需要4次独立的网络往返:PC想我们的DNS发送请求,获取响应。为了得到查询结果,我们的DNS进行递归查询,该查询包含了与其他Serv之间的3次不同的往返。故,第一次在浏览器中输入一个域名时,需要等待时间

4.3.1. 为何不使用原始DNS

推荐做法是,除非由于特殊原因必须进行DNS查询,否则永远都通过getaddrinfo()/其他系统支持的机制来解析hostname,通过OS来查询hostname会带来如下好处:

1)DNS通常不是OS获取名称信息的唯一途径。如果作为第一选择,PC名称突然在应用程序中变得不可用了,而在浏览器、文件共享路径等处均可用,由于没有OS那样通过类似于WINS、/etc/hosts的机制来查询域名,自己的程序中是无法使用这些名称的。
2)PC的缓存保存了最近查询过的域名,可能已包含了需要的域名的IP,如果尝试自行做DNS查询的话,意味着重复了已经完成的工作
3)运行Py脚本的系统可能已有了本地域名Serv的信息,原因可能是OS-MA做了手动配置,使用了类似DHCP的网络安装协议。如果自己的Py程序中开始DNS查询,需要知道如何获取特定OS的相关信息。
4)如果不使用本地DNS,就无法利用本地DNS自身的缓存,该缓存可防止程序及其他运行在同一网络中的程序,对本地频繁使用的hostname进行查询。
5)世界上的DNS会做一些调整,OS的库和守护进程也会逐步更新以适应最新的变化。如果直接在程序中进行原始DNS调用,需要自己跟踪这些变化,确保代码与TLD上的IP、国际化约定及DNS本身的变化同步。
6)Py没有把任何DNS工具内置到标准库中,要使用Py进行DNS操作,必须选择第三方库dnspython3

4.3.2. 使用Py进行DNS查询

有一个使用Py进行DNS调用的理由。如果编写一个邮件Serv/不需本地邮件中继就尝试直接向收件人发送邮件的Cli,会像得到与某域名关联的MX记录,就能找到朋友的@example.com的正确邮件Serv了

4.3.2.1. dnspython3可能是支持Py3库中最好的一个,安装:

$ pip install dnspython3

该库使用自己的方法来获取Win/POSIX-OS正在使用的域名Serv,请求这些Serv代表其进行递归查询。故,OS-MA/网络配置Serv已经正确配置好能够运行的名称Serv

# 4-2 dns_basic.py

import argparse, dns.resolver

def lookup(name):
    for qtype in 'A', 'AAAA', 'CNAME', 'MX', 'NS':
        answer = dns.resolver.query(name, qtype, raise_on_no_answer=False)
        if answer.rrset is not None:
            print(answer.rrset)
    
    if __name__ == '__main__':
        parser = argparse.ArgumentParser(description='Resolve a name using DNS')
        parser.add_argument('name', help='name that you want to look up in DNS')
        lookup(parser.parse_args().name)

每次只能尝试一种DNS查询,该脚本在命令行汇总提供了一个hostname作为参数,然后循环查询属于该hostname的不同类型的记录,以python.org作为参数运行,可以得到如下DNS信息。

$ python dns_basic.py pythonorg
python.org. 42945 IN A 140.211.10.69
python.org. 86140 IN MX 50 mail.python.org
python.org. 86146 IN NS ns4.p11.dynect.net
...

可以看到,返回的每个响应都通过一个对象序列来表示,按照顺序,每行打印的键如下:
1)查询的名称
2)将该名称存入缓存的有效时间,s为单位
3)"类",如表示返回web地址响应的IN
4)记录的“类型”,常见的表示IPv4的A、IPv6的AAAA、名称Serv记录的NS、域名使用的邮件Serv的MX
5)“数据”,提供要连接或与Serv通信所需的信息。

4.3.2.2. 得知了关于python.org域名的3点信息:

1)A记录告诉我们,如果想连接到真正的python.org机器(发起一个HTTP连接、开始一个SSH会话等),应该吧数据包发送至IP-140.211.10.69,
2)NS记录告诉我们,想查询任何属于python.org的hostname,应该请求ns1.p11.dynect.net至ns4.p11.dynect.net(按照给出的顺序,不是数字顺序)这4台服务器进行解析。
3)想向邮箱地址在@python.org域名下的用户发送电子邮件,需要查阅hostname-mail.python.org
DNS查询页可能返回CNAME这一记录类型,表示查询的hostname其实只是另一个hostname的别名,需要单独查询该原始hostname,因为这个过程需要两次往返,所以这一纪录类型不流行,但有时会碰到

4.3.3. 解析邮箱域名

4.3.3.1.解析邮箱域名是多数Py程序中对原始DNS查询的一个合理应用。规则是,如果存在MX记录,必须尝试与这些SMTP-Serv进行通信。如果没有任何SMTP-Serv接收消息,必须向用户返回一个错误(或将该消息放入重试队列汇总)。

4.3.3.2. 如果优先级不同,就按照优先级序号,从小到大尝试这些SMTP-Serv,如果不存在MX记录,但域名提供了A/AAAA,可以尝试向该A/AAAA对应的IP发起连接。如果域名没有提供任一记录,但给出了CNAME,应该使用相同的规则搜索该CNAME对应域名的MX记录或A记录

4-3展示了该算法的可能实现方法,通过进行一系列的DNS查询,得到可能的目标IP,并打印出它的决定,像这样不断调整策略并返回地址,而不是打印出来,就可以实现一个Py邮件分发工具,将邮件发送至远程地址。

# 4-3 解析电子邮件域名 dns_mx.py
import argparse, dns.resolver

def resolve_hostname(hostname, indent=''):
    'Print an A or AAAA record for hostname; follow CNAMEs if necessary.'
    indent = indent + '   '
    answer = dns.resolver.query(hostname, 'A')
    if answer.rrset is not None:
        for record in answer:
            print(indent, hostname, 'has A address', record.address)
        return
    answer = dns.resolver.query(hostname, 'AAAA')
    if answer.rrset is not None:
        for record in answer:
            print(indent, hostname, 'has AAAA address', record.address)
        return
    answer = dns.resolver.query(hostname, 'CNAME')
    if answer.rrset is not None:
        record = answer[0]
        cname = record.address
        print(indent, hostname, 'is a CNAME alias for', cname)
        resolve_hostname(cname, indent)
        return
    print(indent, 'ERROR: no A, AAAA, or CNAME records for', hostname)

def resolve_email_domain(domain):
    "For an email address name@domain find its mail server IP addresses."
    try:
        answer = dns.resolver.query(domain, 'MX', raise_on_no_answer=False)
    except dns.resolver.NXDOMAIN:
        print('Error: No such domain', domain)
        return 
    if answer.rrset is not None:
        records = sorted(answer, key=lambda record: record.preference)
        for record in records:
            name = record.exchange.to_text(omit_final_dot=True)
            print('Priority', record.preference)
            resolve_hostname(name)
    else:
        print('This domain has no explicit MX records')
        print('Attempting to resolve it as an A, AAAA, or CNAME')
        resolve_hostname(domain)

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Find mailserver IP address')
    parser.add_argument('domain', help='domain that you want to send mail to')
    resolve_email_domain(parser.parse_args().domain)

1)resolve_hostname()会根据当前主机连接到的是IPv4还是IPv6来对A与AAAA进行动态选择,故展示的并不健壮。此时,应该使用getsockaddr(),而不是尝试自己解析邮件Serv的hostname,4-3只是用于展示DNS的工作原理,了解查询是如何被解析的
2)真实的邮件Serv不会讲邮件Serv的地址打印出来,会向这些地址发送邮件。只要有一次发送成功,就停止继续发送。(发送成功后继续遍历Serv列表,会生成电子邮件的多个副本,对应每个发送成功的Serv会有一个副本),python.org只有一个邮件Serv的IP

$ python dns_mx.py python.org
This domain has 1 MX records
Priority 50
    mail.python.org has A address 82.94.164.166

无论该IP是属于一台机器还是由一个主机集群共享,无法从外表简单看出来。IANA有不少于6个电子邮件Serv。

$ python dns_mx.py iana.org
...

通过尝试对许多不同的域名运行这个脚本,可以看到大/小机构是如何将收到的邮件路由到不同IP

4.4. 小结

4.4.1. Py程序通常需要将hostname转换为可以实际连接的socket地址

4.4.2. 多数hostname查询都通过socket模块的getsockaddr()完成,因为该函数的智能性是由OS提供的,不仅知道如何使用所有可用的机制来查询域名,还知道本地IP栈配置支持的地址类型(IPv4/6)

4.4.3. 传统IPv4仍然最流行,但IPv6变得越来越常见。使用getsockaddr()进行hostname和port的查询,Py能将地址看成单一的字符串,无需单行如何解析与解释地址。

4.4.4. DNS是多数名称解析方法背后的原理。是一个分布在世界各地的数据库,用于将域名查询直接指向拥有相应域名的机构的Serv。在Py中直接使用原始DNS查询的频率不高,但在基于电子邮件地址中@符号后的域名直接发送电子邮件时,还是有帮助的

你可能感兴趣的:(读数笔记_python网络编程3(4))