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)
[(<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 0, '
', ('127.0.0.1', 25))]
>>> getaddrinfo('localhost', 'smtp', 0, socket.SOCK_STREAM, 0)
[(<AddressFamily.AF_INET6: 23>, <SocketKind.SOCK_STREAM: 1>,
0, '', ('::1', 25, 0, 0)), (<AddressFamily.AF_INET: 2>, <Sock
etKind.SOCK_STREAM: 1>, 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)
[(<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 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)
[(<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 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_(<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 0, 'mit.edu', ('23.213.151.198', 80))
socket_args = info[0:3]
# print('socket_args_{}'.format(socket_args)) # socket_args_(<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 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)的更多相关文章

  1. 读书笔记_python网络编程3(5)

    5. 网络数据与网络错误 应该如何准备需要传输的数据? 应该如何对数据进行编码与格式化? Py程序需要提供哪些类型的错误? 5.1. 字节与字符串 PC与网卡都支持将字节作为通用传输单元.字节将8比特 ...

  2. 读书笔记_python网络编程3_(2)

    2.UDP 2.0.数据包表示较短的信息,大小通常不会超过几千字节,在浏览器与服务器进行会话/电子邮件客户端与ISP的邮件服务器进行会话时,这些独立而小型的数据包是如何组成会话的呢? 2.0.1.IP ...

  3. 读书笔记_python网络编程3_(1)

    0.前言 代码目录: https://github.com/brandon-rhodes/fopnp/tree/m/py3 0.1.网络实验环境:理解客户端与服务器是如何通过网络进行通信的 每台机器通 ...

  4. 读书笔记_python网络编程3(6)

    6.TLS/SSL 6.0. 传输层安全协议(TLS, Transport Layer Security)是如今web上应用最广泛的加密方法了,1999年成为互联网标准.前身是安全套接层(SSL, S ...

  5. 读书笔记_python网络编程3_(3)

    3.TCP:传输控制协议 第一个版本在1974年定义,建立在网际层协议(IP)提供的数据包传输技术之上.TCP使程序可以使用连续的数据流进行相互通信. 除非网络原因导致连接中断/冻结,TCP都能保证将 ...

  6. python学习笔记11 ----网络编程

    网络编程 网络编程需要知道的概念 网络体系结构就是使用这些用不同媒介连接起来的不同设备和网络系统在不同的应用环境下实现互操作性,并满足各种业务需求的一种粘合剂.网络体系结构解决互质性问题彩是分层方法. ...

  7. nodejs学习笔记之网络编程

    了解一下OSI七层模型   OSI层 功能 TCP/IP协议 应用层 文件传输,电子邮件,文件服务,虚拟终端  TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet 表示层 数据格式化 ...

  8. Java精选笔记_网络编程

    网络编程 概述 现在的网络编程基本上都是基于请求/响应方式的,也就是一个设备发送请求数据给另外一个,然后接收另一个设备的反馈. 在网络编程中,发起连接程序,也就是发送第一次请求的程序,被称作客户端(C ...

  9. python学习笔记10 ----网络编程

    网络编程 网络编程需要知道的概念 网络体系结构就是使用这些用不同媒介连接起来的不同设备和网络系统在不同的应用环境下实现互操作性,并满足各种业务需求的一种粘合剂.网络体系结构解决互质性问题彩是分层方法. ...

随机推荐

  1. linux 文件管理命令

    一,文件查看more,less,head,tail,cat,tac 分屏查看文件内容 more:和man用法一样,但翻屏到尾部自动推出. less:和man用法一样. head:查看文件的前n行.n默 ...

  2. TopCoder12729 「SRM589Medium」GearsDiv1 二分图最小点覆盖

    问题描述 \(N\) 个齿轮每个齿轮有颜色(RGB),有些齿轮之间会咬合,你需要删除尽量少的齿轮并给每种颜色安排方向使得咬合齿轮不同向.问最多保留多少个齿轮.保证不存在两个相同颜色的齿轮咬合. sub ...

  3. Maven发布封装到中央仓库时候报错:no default secret key: No secret key

    今天因为发布swagger-spring-boot-starter做一个问题的修复,然后碰到了下面这个问题,记录一下解决过程,帮助后续碰到类似问题的童鞋: *gpg: WARNING: "- ...

  4. GO基础之接口

    一.概念1. 面向对象语言中,接口用于定义对象的行为.接口只指定对象应该做什么,实现这种行为的方法(实现细节)是由对象来决定.2. 在Go语言中,接口是一组方法签名. •接口只指定了类型应该具有的方法 ...

  5. vue2y引入iview,在ie11报错的解决办法

    在ie11中打开vue项目一片空白,F12打开控制台发现如下错误 只需要在build/webpack.base.conf.js中加一行代码即可

  6. Excel上下标如何设置?

    Excel表格怎么设置上下标?Excel上下标设置技巧 在21世纪的我们,平时的工作和学习中,经常会使用到一些专业的文档,比如方程式.数据的公式和科学计数等,其中均会涉及到许多的上下标符号输入以及使用 ...

  7. August 18th, 2019. Week 34th, Sunday

    Fear doesn't shut you down, it wakes you up. 恐惧不会消磨你的意志,它能激发你的潜能. We all know that fear is powerful, ...

  8. 第05组 Beta版本演示

    第05组 Beta版本演示 小组信息 组名:天码行空 组长博客:地址 组内成员: 组员 学号 卢欢(组长) 031702513 陈天恒 031702527 古力亚尔·艾山 031702511 张聪 0 ...

  9. java之类的构造方法

    构造器的特征: 具有和类相同的名称: 不声明返回值的类型: 不能被static.final.synchronized.abstract.native修饰,不能有return语句返回值: 构造器的作用: ...

  10. 如何用css实现太极图

    <!DOCTYPE html><html> <head> <meta charset="utf-8"> <title>太 ...