套接字名
域名服务(DNS, Domain Name Service): 域名和真实IP地址的映射。
套接字的5个坐标
- 地址族(Adress Family),比如
AF_INET
,AF_UNIX
- 套接字类型(Socket Type), 比如
SOCK_DGRAM
,SOCK_STREAM
- 协议 (protocol), 前两个确定后协议可选很少,所以一般不写,或者写0,表示自动选择,比如
IPPROTO_TCP
,IPPROTO_UDP
- 主机名
- 端口号
IPV6的特殊情况:
IPV6中套接字名不止包括主机名、端口号,还包括“流”信息,“范围”标识等额外坐标。
现代地址解析-getaddrinfo()的使用
套接字需要的5个坐标都可以得到。生产环境代码很少使用 AF_INET
这类socket模块的常量,采用的是 getaddrinfo()
>>> from pprint import pprint // 打印元组列表好看
// 调用
>>> infolist = socket.getaddrinfo('gatech.edu', 'www')
// 等同于
>>> infolist = socket.getaddrinfo('gatech.edu', 80)
// 返回
>>> pprint(infolist)
[(2, 1, 6, '', ('130.208.77.55', 80)),
(2, 2, 17, '', ('130.208.77.55', 80)]
>>> info = infolist[0]
// 元组前三项分别为地址族、套接字类型、协议,用来初始化套接字
>>> s = socket.socket(*info[0:3])
// 元组第5项为(主机名,端口), 可用来调用
>>> s.connect(info[4])
使用 getaddrinfo()
为服务器绑定端口
参数:getaddrinfo(host, port, address_family, sock_type, protocl, flag)
如果某个字段为数字,可以使用0代表通配符
场景:正在创建套接字或者希望客户端从一个可预计的地址连接至其他主机, 需要bind()
。
用法:把主机名设为 None
, 127.0.0.1
, localhost
等,提供端口号、套接字类型等。
>>> from socket import getaddrinfo
// 想用 TCP 来支持 smtp 数据传输的话,应该通过 bind() 把套接字绑定到哪个地址
>>> getaddrinfo(None, 'smtp', 0, socket.SOCK_STREAM, 0, socket.AI_PSASSIVE)
// 返回告诉我们可以bind到本机的任何IPV4或IPV6地址
[(2, 1, 6, '', ('0.0.0.0', 25)), (10, 1, 6, '', ('::', 25, 0, 0))]
使用 getaddrinfo()
连接服务
场景:connect()
或 sendto()
参数:
-
AI_ADDRCONFIG
标记: 过滤计算机无法连接的地址 -
AI_V4MAPPED
标记:本机只有IPV6,但是连接的服务只支持IPV4,指定后,会将服务的IPV4地址编码为可用的IPV6地址。
用法:
>>> getaddrinfo('ftp.kernel.org', 'ftp', 0, socket.SOCK_STREAM, 0,
... socket.AI_ADDRCONFIG | socket.AI_V4MAPPED)
[(2, 1, 6, '', ('204.13.33.3', 21)),
(2, 1, 6, '', ('204.13.33.4', 21)]
返回的列表是有顺序的,经过了负载均衡,一般选择第一个。
使用 getaddrinfo()
请求规范主机名
反向DNS查询:IP映射到主机名。
风险:
- IP地址拥有者可以随意设置主机名。因此要验证。
- 耗时。
用法:设置 AI_CANNONNAME
标志, 返回项第四项为规范主机名。
其他 getaddrinfo()
标记
总结下之前的:
-
AI_PASSIVE
标记:被动的,用于bind()
,节点为null时,返回通配地址,否则返回回环地址。 -
AI_ADDRCONFIG
标记: 过滤计算机无法连接的地址 -
AI_V4MAPPED
标记:本机只有IPV6,但是连接的服务只支持IPV4,指定后,会将服务的IPV4地址编码为可用的IPV6地址。 -
AI_CANNONNAME
标记, 返回项第四项为规范主机名。
其他:
-
AI_ALL
: 与AI_V4MAPPED
结合使用,表示包含已知的与目标主机的所有地址。 -
AI_NUMERICHOST
: 调用的节点名必须是 IPV4 或者 IPV6 地址,不是字符串 -
AI_NUMRICSERV
: 禁用"www"这种端口,只用"80"这种。
在 getaddrinfo()
之前经常使用的原始名称服务程序
// 返回主机名
socket.gethostname()
socket.getfqdn()
// 对IPV4主机名和IP地址互换
socket.gethostbyname('cern.h')
socket.gethostbyaddr('128.189.22.1')
// 查询协议号和端口号
>>> socket.getprotobyname('UDP')
17
>>> socket.getservbyname('www')
80
>>> socket.getservbyport(80)
'www'
// 获取机器主IP地址
socket.gethostbyname(socket.getfqdn())
// 调用都可能失败,要做好二手准备。
代码
// 使用 `getaddrinfo()`创建并连接套接字
#!/usr/bin/env python3
# Foundations of Python Network Programming, Third Edition
# https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter04/www_ping.py
# Find the WWW service of an arbitrary host using 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,
)
// getaddrinfo失败后特殊错误 gaierror
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)
DNS 协议
- 目的:解析主机名,返回IP地址
- 标准:RFC 1034和 RFC 1035
- 传输层协议:UDP/IP 与 TCP/IP
- 端口号:53
- 库:第三方,包括 dnspython3
DNS查询只有在缓存、多播DNS等失败后才会启动DNS服务器,毕竟这很耗时。
使用 Python 进行 DNS 查询
// 一个包含递归的简单 DNS 查询
#!/usr/bin/env python3
# Foundations of Python Network Programming, Third Edition
# https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter04/dns_basic.py
# Basic DNS query
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)
answer.rrset
返回格式:查询名称,有效时间,类型(IN, 表示互联网地址响应), 记录的类型qtype(A: IPV4, AAAA: IPV6, NS: name service, MX: 邮件服务器)