Python网络编程中的套接字名和DNS解析。

820 阅读6分钟
原文链接: zhuanlan.zhihu.com

距离上一次TCP的文章,这一次要讲的是套接字名和DNS,并且还会涉及到网络数据的发送接受和网络错误的发生和处理。

下面说套接字名,在创建和部署每个套接字对象时总共需要做5个主要的决定,主机名和IP地址是其中的最后两个。

一般创建和部署套接字的步骤如下:

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DREAM)
s.bind(('localhost', 1088))

可以看到我们指定了4个值,两个用来做对套接字做配置,另外两个提供bind()调用所需要的地址。第5个坐标则是因为socket()方法有第3个可选参数。

下面我们依次说这5个参数。

首先,第1个参数是地址族的选择,某个特定的机器可能连接到多个不同类型的网络。对地址族的选择指定了想要进行通信的网络类型。

这里面选择的是AF_INET作为地址族,即在IP网络层编写程序。这样对与Python程序员来说也是最有益的。

第2个参数就是套接字类型,然后我们解释一下套接字类型,尽管TCP和UDP是AF_INET协议族特有的,但是套接字接口的设计者决定基于数据报的套接字这一宏观的概念创建一些更通用的名字,这就是SOCK_DGRAM,而提供可靠传输与流量控制的数据流概念我们用SOCK_STREAM。这两个符号就可以覆盖不同地址族的很多协议了。

socket()调用的第3个参数是协议,但是一旦确定了协议族和套接字类型,可能使用的协议范围就被缩到了一个主要的选项。如果设置成0。在IP上使用流的时候自动选择TCP,设置数据报的时候自动选择UDP。

至于第4个和第5个参数就是IP地址和端口号。

当然现在如果要是使用IPV6地址族的话,那你可以看看AF_INET6。

下面说一下现代地址解析,使用socket模块中的一些旧式程序来解决地址问题的方法是相当琐碎的。

而下面要说getaddrinfo()这个工具,这个工具除了一些特定的工作,否则这个函数将是我们用来将用户指定的主机名和端口号转换为可供套接字方法使用的地址时所需的唯一方法。

这个工具还可以用来为服务器绑定端口,然后连接服务或者是请求规范主机名。当然这是3个最重要getaddrinfo()的标记操作。

至于其他的标记,不同的操作系统上可用标记有所不同,但是也有一些是跨平台的。比如AI_ALL,AI_NUMERICHOST和AI_NUMERICSERV等等。

至于更详细的一些东西,可以看相关的文档。

下面这段代码是把上面内容结合起来,设计了一个简单的例子。下面是使用getaddrinfo()创建并连接套接字。

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
    socket_args = info[0:3]
    address = info[4]
    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)

下面这三点要引起注意:

  1. 代码中没有提到使用IP协议,也没有提到TCP作为传输方式。如果用户正好输入了一个主机名,而系统认为该主机AppleTalk连接的。
  2. getaddrinfo()调用失败会引起一个特定的名称服务错误。而不是在脚本末尾检测的普通网络故障,这个Python把这个错误叫做gaierror。
  3. 我们并没有为socket()构造函数传入3个单独的参数。我们使用星号传入了参数列表。表示socket_args列表中的3个元素会被当作3个单独的参数传入构造函数中。使用实际返回的地址时的做法则恰恰相反。

下面说一下DNS解析。人们习惯记忆域名,但机器间互相只认IP地址,域名与IP地址之间是多对一的关系,一个ip地址不一定只对应一个域名,且一个域名只可以对应一个ip地址,它们之间的转换工作称为域名解析,域名解析需要由专门的域名解析服务器来完成,整个过程是自动进行的。

下面给出一个包含递归的简单DNS查询。

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)

按照打印的顺序,每行打印出来的键如下。

  • 查询的名称
  • 能够将该名称存入缓存的有效时间,以s为单位
  • 类,比如返回互联网地址响应的IN
  • 记录的类型,常见的比如表示IPV4地址的A,IPV6地址的AAAA
  • 最后是数据

下面给出最后的一段代码,解析电子邮件域名。解析邮箱域名是多数Python程序中对原始DNS查询的一个应用。

下面拿邮箱域名解析规则RFC5321来说,如果存在MX记录,则必须尝试与SMTP来进行通信。如果SMTP服务器没有响应,就返回一个错误,如果有响应就进入消息队列,按照优先级顺序从小到大尝试发起连接。如果提供了A和AAAA记录,就直接向对应地址发起连接。如果没有,但是给出了CNAME,就按照对应域名的MX记录和A记录。

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)
        print('This domain has', len(records), 'MX records')
        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)

上述代码就是算法可能的实现过程,进行一系列的查询,该算法得到了可能的目标地址,然后打印出来决定,像这样不断调整策略并返回地址,而不简单的打印出来,就可以实现一个Python的邮件分发工具。将邮件发送到远程地址。

到这里,文章就结束了,下一篇文章就是说网络数据和网络错误的处理。

寒假到这里,也就快过了一半了,大家加油。

最后的最后,大家注意身体。

提前祝大家,新年快乐。