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

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

4.1. 主机名与socket

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

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

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

相较于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。

如何使用getaddrinfo()来支持3种基本网络操作(绑定、连接、识别已经向我们发送信息的远程主机)
想要得到一个地址,将其作为参数提供给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 1 etkind.sock_stream:>, 0, '', ('127.0.0.1', 25))]
</sock></addressfamily.af_inet:></socketkind.sock_stream:></addressfamily.af_inet6:></socketkind.sock_stream:></addressfamily.af_inet:>

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

&#x9664;&#x4E86;&#x7ED1;&#x5B9A;&#x672C;&#x5730;IP&#x81EA;&#x884C;&#x63D0;&#x4F9B;&#x670D;&#x52A1;&#x5916;&#xFF0C;&#x8FD8;&#x53EF;&#x4F7F;&#x7528;getaddrinfo()&#x83B7;&#x53D6;&#x8FDE;&#x63A5;&#x5230;&#x5176;&#x4ED6;&#x670D;&#x52A1;&#x6240;&#x9700;&#x7684;&#x4FE1;&#x606F;&#x3002;&#x67E5;&#x8BE2;&#x670D;&#x52A1;&#x65F6;&#xFF0C;&#x53EF;&#x4F7F;&#x7528;&#x4E00;&#x4E2A;&#x7A7A;&#x5B57;&#x7B26;&#x4E32;&#x8868;&#x793A;&#x8981;&#x901A;&#x8FC7;&#x81EA;&#x73AF;&#x63A5;&#x53E3;&#x8FDE;&#x63A5;&#x56DE;&#x672C;&#x673A;&#xFF0C;&#x4E5F;&#x53EF;&#x63D0;&#x4F9B;&#x4E00;&#x4E2A;&#x5305;&#x542B;IPv4/6/&#x4E3B;&#x673A;&#x540D;&#x7684;&#x5B57;&#x7B26;&#x4E32;&#x6765;&#x6307;&#x5B9A;&#x76EE;&#x6807;&#x5730;&#x5740;

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。

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))]
</socketkind.sock_stream:></addressfamily.af_inet:>

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

>>> 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))]
</socketkind.sock_stream:></addressfamily.af_inet:>
getaddrinfo()&#x6D41;&#x884C;&#x524D;&#xFF0C;&#x7A0B;&#x5E8F;&#x5458;&#x901A;&#x8FC7;OS&#x652F;&#x6301;&#x7684;&#x66F4;&#x7B80;&#x5355;&#x7684;&#x540D;&#x79F0;&#x670D;&#x52A1;&#x7A0B;&#x5E8F;&#x6765;&#x8FDB;&#x884C;socket&#x7684;&#x7F16;&#x7A0B;&#xFF0C;&#x591A;&#x6570;&#x662F;&#x786C;&#x7F16;&#x7801;&#xFF0C;&#x53EA;&#x652F;&#x6301;IPv4&#xFF0C;&#x5E94;&#x8BE5;&#x907F;&#x514D;&#x4F7F;&#x7528;&#x8FD9;&#x4E9B;&#x7A0B;&#x5E8F;&#x3002;
1)socket&#x6A21;&#x5757;&#x7684;&#x6807;&#x51C6;&#x5E93;&#x9875;&#x9762;&#x627E;&#x5230;&#x76F8;&#x5173;&#x7684;&#x6587;&#x6863;&#xFF0C;&#x6709;&#x4E24;&#x4E2A;&#x8C03;&#x7528;&#x80FD;&#x8FD4;&#x56DE;&#x5F53;&#x524D;&#x673A;&#x5668;&#x7684;hostname
socket.gethostname()
'DESKTOP-S1A6RSJ'
socket.getfqdn()
'DESKTOP-S1A6RSJ'
2)&#x8FD8;&#x6709;&#x4E24;&#x4E2A;&#x80FD;&#x591F;&#x5BF9;IPv4-hostname&#x548C;IP&#x8FDB;&#x884C;&#x76F8;&#x4E92;&#x8F6C;&#x6362;
socket.gethostbyname('cern.ch')
'188.184.9.234'
socket.gethostbyaddr('188.184.9.234')
('webrlb01.cern.ch', [], ['188.184.9.234'])
3)&#x6709;&#x4E09;&#x4E2A;&#x7A0B;&#x5E8F;&#x53EF;&#x4EE5;&#x901A;&#x8FC7;OS&#x5DF2;&#x77E5;&#x7684;&#x7B26;&#x53F7;&#x540D;&#x67E5;&#x8BE2;&#x534F;&#x8BAE;&#x53F7;&#x53CA;port
socket.getprotobyname('UDP')
17
socket.getservbyname('www')
80
socket.getservbyport(80)
'http'
4)&#x60F3;&#x8981;&#x83B7;&#x53D6;Py&#x7684;&#x673A;&#x5668;&#x7684;&#x4E3B;IP&#xFF0C;&#x53EF;&#x4EE5;&#x5C06;&#x5B8C;&#x5168;&#x9650;&#x5B9A;&#x4E3B;&#x673A;&#x540D;&#x4F20;&#x5404;gethostbyname()&#x8C03;&#x7528;&#x3002;
socket.gethostbyname(socket.getfqdn())
'192.168.137.1'
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)

</socketkind.sock_stream:></addressfamily.af_inet:></socketkind.sock_stream:></addressfamily.af_inet:>
>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
&#x8BE5;&#x811A;&#x672C;&#x6709;3&#x70B9;&#x503C;&#x5F97;&#x6CE8;&#x610F;:

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

DNS&#x534F;&#x8BAE;
&#x76EE;&#x7684;: &#x89E3;&#x6790;hostname&#xFF0C;&#x8FD4;&#x56DE;IP&#x5730;&#x5740;
&#x6807;&#x51C6;: RFC 1034&#x4E0E;RFC 1035(1987)
&#x4F20;&#x8F93;&#x5C42;&#x534F;&#x8BAE;: UDP/IP&#x4E0E;TCP/IP
&#x7AEF;&#x53E3;&#x53F7;: 53
&#x5E93;: &#x7B2C;&#x4E09;&#x65B9;&#xFF0C;&#x5305;&#x62EC;dnspython3

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

$ 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)。

&#x63A8;&#x8350;&#x505A;&#x6CD5;&#x662F;&#xFF0C;&#x9664;&#x975E;&#x7531;&#x4E8E;&#x7279;&#x6B8A;&#x539F;&#x56E0;&#x5FC5;&#x987B;&#x8FDB;&#x884C;DNS&#x67E5;&#x8BE2;&#xFF0C;&#x5426;&#x5219;&#x6C38;&#x8FDC;&#x90FD;&#x901A;&#x8FC7;getaddrinfo()/&#x5176;&#x4ED6;&#x7CFB;&#x7EDF;&#x652F;&#x6301;&#x7684;&#x673A;&#x5236;&#x6765;&#x89E3;&#x6790;hostname&#xFF0C;&#x901A;&#x8FC7;OS&#x6765;&#x67E5;&#x8BE2;hostname&#x4F1A;&#x5E26;&#x6765;&#x5982;&#x4E0B;&#x597D;&#x5904;:

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

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

$ 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通信所需的信息。

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

4-3 &#x89E3;&#x6790;&#x7535;&#x5B50;&#x90AE;&#x4EF6;&#x57DF;&#x540D; 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. 小结

Original: https://www.cnblogs.com/wangxue533/p/11992990.html
Author: 罗生堂下
Title: 读书笔记_python网络编程3_(4)

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/591364/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球