24 KiB
通过 Python 的 IP Address 模块学习 IP 地址概念
Python 的 ipaddress 模块是 Python 标准库中一颗不被看好的宝石。你不必是一个成熟的网络工程师,也可以在野外接触到 IP 地址。 IP 地址和网络在软件开发和基础设施中无处不在。它们是计算机如何相互寻址的基础。
边做边学是掌握 IP 地址的有效方法。通过将 IP 地址作为 Python 对象来查看和操作,ipaddress模块允许您这样做。在本教程中,您将通过使用 Python 的ipaddress模块的一些特性来更好地掌握 IP 地址。
在本教程中,您将学习:
- IP 地址在理论上和 Python 代码中是如何工作的
- IP 网络如何表示 IP 地址组,以及如何检查两者之间的关系
- Python 的
ipaddress模块如何巧妙运用经典设计模式让你事半功倍
要跟进,您只需要 Python 3.3 或更高版本,因为在那个版本中ipaddress被添加到了 Python 标准库中。本教程中的例子是使用 Python 3.8 生成的。
免费下载: 从 CPython Internals:您的 Python 3 解释器指南获得一个示例章节,向您展示如何解锁 Python 语言的内部工作机制,从源代码编译 Python 解释器,并参与 CPython 的开发。
理论和实践中的 IP 地址
如果你只记得一个关于 IP 地址的概念,那么记住这个:*IP 地址是一个整数。*这条信息将帮助您更好地理解 IP 地址的功能以及如何将它们表示为 Python 对象。
在开始编写任何 Python 代码之前,看看这个概念在数学上是如何具体化的会很有帮助。如果你在这里只是为了一些如何使用ipaddress模块的例子,那么你可以跳到下一节,关于使用模块本身。
IP 地址的机制
你在上面看到 IP 地址可以归结为一个整数。更全面的定义是, IPv4 地址是一个 32 位整数,用于表示网络上的主机。术语主机有时用作地址的同义词。
由此得出有 2 个 32 可能的 IPv4 地址,从 0 到 4,294,967,295(其中上限为 2 32 - 1)。但这是给人类的教程,不是给机器人的。没人想 ping IP 地址0xdc0e0925。
更常见的表示 IPv4 地址的方式是使用四点符号,它由四个点分隔的十进制整数组成:
220.14.9.37
不过,地址220.14.9.37代表什么样的底层整数并不明显。按照公式,您可以将 IP 地址220.14.9.37分成四个八位字节组成部分:
>>> (
... 220 * (256 ** 3) +
... 14 * (256 ** 2) +
... 9 * (256 ** 1) +
... 37 * (256 ** 0)
... )
3691907365
如上图,地址220.14.9.37代表整数 3,691,907,365。每个八位字节是一个字节,或者是一个从 0 到 255 的数字。考虑到这一点,您可以推断出最大的 IPv4 地址是255.255.255.255(或者十六进制的FF.FF.FF.FF),而最小的是0.0.0.0。
接下来,您将看到 Python 的ipaddress模块如何为您进行这种计算,允许您使用人类可读的形式,并让地址运算在看不见的地方发生。
Python ipaddress模块
要继续操作,您可以在命令行中获取计算机的外部 IP 地址:
$ curl -sS ifconfig.me/ip
220.14.9.37
这将从站点 ifconfig.me 请求您的 IP 地址,该地址可用于显示有关您的连接和网络的一系列详细信息。
注意:出于技术正确性的考虑,这很可能不是你电脑自己的公共 IP 地址。如果你的连接位于一个终端路由器之后,那么最好把它看作一个“代理”IP,通过它你可以到达互联网。
现在打开一个 Python REPL。您可以使用IPv4Address类来构建封装地址的 Python 对象:
>>> from ipaddress import IPv4Address
>>> addr = IPv4Address("220.14.9.37")
>>> addr
IPv4Address('220.14.9.37')
向IPv4Address构造函数传递一个像"220.14.9.37"这样的str是最常见的方法。但是,该类也可以接受其他类型:
>>> IPv4Address(3691907365) # From an int
IPv4Address('220.14.9.37')
>>> IPv4Address(b"\xdc\x0e\t%") # From bytes (packed form)
IPv4Address('220.14.9.37')
虽然从人类可读的str开始构建可能是更常见的方式,但是如果您使用类似于 TCP 包数据的东西,您可能会看到bytes输入。
上述转换在其他方向也是可能的:
>>> int(addr)
3691907365
>>> addr.packed
b'\xdc\x0e\t%'
除了允许不同 Python 类型的往返输入和输出,IPv4Address的实例也是可散列的。这意味着您可以将它们用作映射数据类型中的键,例如字典:
>>> hash(IPv4Address("220.14.9.37"))
4035855712965130587
>>> num_connections = {
... IPv4Address("220.14.9.37"): 2,
... IPv4Address("100.201.0.4"): 16,
... IPv4Address("8.240.12.2"): 4,
... }
最重要的是,IPv4Address还实现了方法,允许使用底层整数进行比较:
>>> IPv4Address("220.14.9.37") > IPv4Address("8.240.12.2")
True
>>> addrs = (
... IPv4Address("220.14.9.37"),
... IPv4Address("8.240.12.2"),
... IPv4Address("100.201.0.4"),
... )
>>> for a in sorted(addrs):
... print(a)
...
8.240.12.2
100.201.0.4
220.14.9.37
您可以使用任何标准的比较运算符来比较地址对象的整数值。
注:本教程重点介绍互联网协议第 4 版(IPv4)地址。还有 IPv6 地址,是 128 位而不是 32 位,用2001:0:3238:dfe1:63::fefb之类更 headier 的形式表示。因为地址的算法基本相同,所以本教程从等式中去掉一个变量,集中讨论 IPv4 地址。
ipaddress模块具有更灵活的工厂函数、 ip_address() ,它接受代表 IPv4 或 IPv6 地址的参数,并尽最大努力分别返回IPv4Address或IPv6Address实例。
在本教程中,您将切入正题,直接用IPv4Address构建地址对象。
正如你在上面看到的,IPv4Address的构造函数本身是简短的。当你开始把地址组合成组或网络时,事情就变得更有趣了。
IP 网络和接口
一个网络是一组 IP 地址。网络被描述和显示为连续的地址范围。例如,网络可以由地址192.4.2.0到192.4.2.255组成,一个包含 256 个地址的网络。
您可以通过 IP 地址的上限和下限来识别网络,但是如何用更简洁的约定来显示呢?这就是 CIDR 记谱法的由来。
CIDR 符号
使用网络地址加上 无类域间路由(CIDR)符号 中的前缀来定义网络:
>>> from ipaddress import IPv4Network
>>> net = IPv4Network("192.4.2.0/24")
>>> net.num_addresses
256
CIDR 符号将网络表示为<network_address>/<prefix>。路由前缀(或前缀长度,或仅仅是前缀),在这种情况下是 24,是用于回答诸如某个地址是否是网络的一部分或网络中有多少地址等问题的前导位计数。(此处前导位是指二进制中从整数左边数起的第一个 N 位。)
您可以使用.prefixlen属性找到路由前缀:
>>> net.prefixlen
24
让我们直接看一个例子。地址192.4.2.12是否在网络192.4.2.0/24中?这种情况下的答案是肯定的,因为192.4.2.12的前 24 位是前三个八位字节(192.4.2)。有了/24前缀,你可以简单地砍掉最后一个八位字节,然后看到192.4.2.xxx部分匹配。
如图所示,/24前缀被翻译成网络掩码,顾名思义,它被用来屏蔽被比较地址中的位:
>>> net.netmask
IPv4Address('255.255.255.0')
您比较前导位来确定一个地址是否是网络的一部分。如果前导位匹配,则该地址是网络的一部分:
11000000 00000100 00000010 00001100 # 192.4.2.12 # Host IP address
11000000 00000100 00000010 00000000 # 192.4.2.0 # Network address
|
^ 24th bit (stop here!)
|_________________________|
|
These bits match
以上,192.4.2.12的最后 8 位被屏蔽(用0),在比较中被忽略。Python 的ipaddress再一次为您节省了数学体操,并支持惯用的成员测试:
>>> net = IPv4Network("192.4.2.0/24")
>>> IPv4Address("192.4.2.12") in net
True
>>> IPv4Address("192.4.20.2") in net
False
这是通过操作符重载来实现的,其中IPv4Network定义了__contains__()来允许使用in操作符进行成员测试。
在 CIDR 符号192.4.2.0/24中,192.4.2.0部分是网络地址,用于标识网络:
>>> net.network_address
IPv4Address('192.4.2.0')
正如您在上面看到的,当对主机 IP 地址应用掩码时,网络地址192.4.2.0可以被视为预期的结果:
11000000 00000100 00000010 00001100 # Host IP address
11111111 11111111 11111111 00000000 # Netmask, 255.255.255.0 or /24
11000000 00000100 00000010 00000000 # Result (compared to network address)
当你这样想的时候,你可以看到前缀/24实际上是如何翻译成真正的IPv4Address:
>>> net.prefixlen
24
>>> net.netmask
IPv4Address('255.255.255.0') # 11111111 11111111 11111111 00000000
事实上,如果您感兴趣,您可以直接从两个地址构造一个IPv4Network:
>>> IPv4Network("192.4.2.0/255.255.255.0")
IPv4Network('192.4.2.0/24')
上图中,192.4.2.0是网络地址,而255.255.255.0是网络掩码。
网络中的另一端是其最终地址,或称 广播地址 ,它是一个可用于与其网络中所有主机通信的单一地址:
>>> net.broadcast_address
IPv4Address('192.4.2.255')
关于网络掩码,还有一点值得一提。您最常看到的前缀长度是 8 的倍数:
| 前缀长度 | 地址数量 | 网络掩码 |
|---|---|---|
| eight | Sixteen million seven hundred and seventy-seven thousand two hundred and sixteen | 255.0.0.0 |
| Sixteen | Sixty-five thousand five hundred and thirty-six | 255.255.0.0 |
| Twenty-four | Two hundred and fifty-six | 255.255.255.0 |
| Thirty-two | one | 255.255.255.255 |
但是,0 到 32 之间的任何整数都是有效的,尽管不太常见:
>>> net = IPv4Network("100.64.0.0/10")
>>> net.num_addresses
4194304
>>> net.netmask
IPv4Address('255.192.0.0')
在这一节中,您看到了如何构建一个IPv4Network实例,并测试其中是否有某个 IP 地址。在下一节中,您将学习如何在网络中循环访问地址。
通过网络循环
IPv4Network类支持迭代,这意味着您可以在一个 for循环中迭代它的单个地址:
>>> net = IPv4Network("192.4.2.0/28")
>>> for addr in net:
... print(addr)
...
192.4.2.0
192.4.2.1
192.4.2.2
...
192.4.2.13
192.4.2.14
192.4.2.15
类似地,net.hosts()返回一个生成器,它将产生上面显示的地址,不包括网络和广播地址:
>>> h = net.hosts()
>>> type(h)
<class 'generator'>
>>> next(h)
IPv4Address('192.4.2.1')
>>> next(h)
IPv4Address('192.4.2.2')
在下一节中,您将深入了解一个与网络密切相关的概念:子网。
子网
子网是 IP 网络的细分:
>>> small_net = IPv4Network("192.0.2.0/28")
>>> big_net = IPv4Network("192.0.0.0/16")
>>> small_net.subnet_of(big_net)
True
>>> big_net.supernet_of(small_net)
True
以上,small_net只包含 16 个地址,足够你和你身边的几个小隔间使用。相反,big_net包含 65,536 个地址。
实现子网划分的常见方法是将网络的前缀长度增加 1。让我们举一个维基百科的例子:
这个例子从一个/24网络开始:
net = IPv4Network("200.100.10.0/24")
通过将前缀长度从 24 增加到 25 来划分子网,需要移动位来将网络分成更小的部分。这在数学上有点复杂。幸运的是,IPv4Network使它变得很容易,因为.subnets()在子网上返回一个迭代器:
>>> for sn in net.subnets():
... print(sn)
...
200.100.10.0/25
200.100.10.128/25
你也可以告诉.subnets()新的前缀应该是什么。更高的前缀意味着更多更小的子网:
>>> for sn in net.subnets(new_prefix=28):
... print(sn)
...
200.100.10.0/28
200.100.10.16/28
200.100.10.32/28
...
200.100.10.208/28
200.100.10.224/28
200.100.10.240/28
除了地址和网络,接下来您将看到ipaddress模块的第三个核心部分。
主机接口
最后但同样重要的是,Python 的ipaddress模块导出了一个用于表示主机接口的IPv4Interface类。主机接口是一种以简洁的形式描述主机 IP 地址及其所在网络的方式:
>>> from ipaddress import IPv4Interface
>>> ifc = IPv4Interface("192.168.1.6/24")
>>> ifc.ip # The host IP address
IPv4Address('192.168.1.6')
>>> ifc.network # Network in which the host IP resides
IPv4Network('192.168.1.0/24')
以上,192.168.1.6/24表示“网络192.168.1.0/24中的 IP 地址192.168.1.6
注:在计算机网络环境中,接口也可以指网络接口,最常见的是网络接口卡(NIC)。如果您曾经使用过 ifconfig 工具(*nix)或 ipconfig (Windows),那么您可能会知道您的工具的名称,如eth0、en0或ens3。这两种类型的接口是不相关的。
换句话说,IP 地址本身并不能告诉你该地址位于哪个(哪些)网络中,网络地址是一组 IP 地址而不是单个地址。IPv4Interface提供了一种通过 CIDR 符号同时表示单个主机 IP 地址及其网络的方法。
特殊地址范围
既然您已经对 IP 地址和网络有了大致的了解,那么知道并非所有的 IP 地址都是平等的(有些是特殊的)也很重要。
互联网数字地址分配机构(IANA)与互联网工程任务组(IETF)共同监督不同地址范围的分配。IANA 的 IPv4 专用地址注册中心是一个非常重要的表,它规定了某些地址范围应该具有特殊的含义。
一个常见的例子是私有地址。专用 IP 地址用于网络上不需要连接到公共互联网的设备之间的内部通信。以下范围仅供私人使用:
| 范围 | 地址数量 | 网络地址 | 广播地址 |
|---|---|---|---|
10.0.0.0/8 |
Sixteen million seven hundred and seventy-seven thousand two hundred and sixteen | 10.0.0.0 |
10.255.255.255 |
172.16.0.0/12 |
One million forty-eight thousand five hundred and seventy-six | 172.16.0.0 |
172.31.255.255 |
192.168.0.0/16 |
Sixty-five thousand five hundred and thirty-six | 192.168.0.0 |
192.168.255.255 |
一个随机选择的例子是10.243.156.214。那么,你怎么知道这个地址是私人的呢?您可以确认它落在10.0.0.0/8范围内:
>>> IPv4Address("10.243.156.214") in IPv4Network("10.0.0.0/8")
True
第二种特殊地址类型是链路本地地址,它只能从给定的子网内到达。一个例子是亚马逊时间同步服务,它可用于链接本地 IP 169.254.169.123上的 AWS EC2 实例。如果您的 EC2 实例位于一个虚拟私有云 (VPC),那么您不需要互联网连接来告诉您的实例现在是什么时间。块 169.254.0.0/16 保留给本地链路地址:
>>> timesync_addr = IPv4Address("169.254.169.123")
>>> timesync_addr.is_link_local
True
从上面可以看出,确认10.243.156.214是私有地址的一种方法是测试它是否位于10.0.0.0/8范围内。但是 Python 的ipaddress模块也提供了一组属性,用于测试地址是否为特殊类型:
>>> IPv4Address("10.243.156.214").is_private
True
>>> IPv4Address("127.0.0.1").is_loopback
True
>>> [i for i in dir(IPv4Address) if i.startswith("is_")] # "is_X" properties
['is_global',
'is_link_local',
'is_loopback',
'is_multicast',
'is_private',
'is_reserved',
'is_unspecified']
关于.is_private有一点需要注意,它使用了比上表所示的三个 IANA 范围更广泛的私有网络定义。Python 的ipaddress模块还集成了其他分配给私有网络的地址:
这不是一个详尽的列表,但它涵盖了最常见的情况。
引擎盖下的 Python ipaddress模块
除了其记录的 API 之外,ipaddress模块及其 IPv4Address 类的 CPython 源代码提供了一些很好的见解,让你知道如何使用一个叫做组合的模式来为你自己的代码提供一个惯用的 API。
作文的核心作用
ipaddress模块利用了一种叫做组合 的面向对象模式。它的IPv4Address类是一个复合,包装了一个普通的 Python 整数。毕竟,IP 地址基本上是整数。
注意:公平地说,ipaddress模块也使用了健康剂量的继承,主要是为了减少代码重复。
每个IPv4Address实例都有一个准私有的._ip属性,它本身就是一个int。该类的许多其他属性和方法都是由该属性的值驱动的:
>>> addr = IPv4Address("220.14.9.37")
>>> addr._ip
3691907365
._ip属性实际上负责产生int(addr)。这个调用链是int(my_addr)调用my_addr.__int__(),而IPv4Address只实现为my_addr._ip:
如果你问 CPython 的开发者,他们可能会告诉你._ip是一个实现细节。虽然在 Python 中没有什么是真正私有的,但是前导下划线表示._ip是准私有的,不是公共ipaddress API 的一部分,并且可能会在没有通知的情况下发生变化。这就是为什么用int(addr)提取底层整数更稳定的原因。
尽管如此,是底层的._ip赋予了IPv4Address和IPv4Network类魔力。
扩展IPv4Address
您可以通过扩展IP v4 地址类来展示底层._ip整数的威力:
from ipaddress import IPv4Address
class MyIPv4(IPv4Address):
def __and__(self, other: IPv4Address):
if not isinstance(other, (int, IPv4Address)):
raise NotImplementedError
return self.__class__(int(self) & int(other))
添加 .__and__() 允许您使用二进制 AND ( &)运算符。现在,您可以直接将网络掩码应用到主机 IP:
>>> addr = MyIPv4("100.127.40.32")
>>> mask = MyIPv4("255.192.0.0") # A /10 prefix
>>> addr & mask
MyIPv4('100.64.0.0')
>>> addr & 0xffc00000 # Hex literal for 255.192.0.0
MyIPv4('100.64.0.0')
上面,.__and__()允许你直接使用另一个IPv4Address或一个int作为蒙版。因为MyIPv4是IPv4Address的子类,在那种情况下isinstance()检查将返回True。
除了运算符重载之外,您还可以添加全新的属性:
1import re
2from ipaddress import IPv4Address
3
4class MyIPv4(IPv4Address):
5 @property
6 def binary_repr(self, sep=".") -> str:
7 """Represent IPv4 as 4 blocks of 8 bits."""
8 return sep.join(f"{i:08b}" for i in self.packed) 9
10 @classmethod
11 def from_binary_repr(cls, binary_repr: str):
12 """Construct IPv4 from binary representation."""
13 # Remove anything that's not a 0 or 1
14 i = int(re.sub(r"[^01]", "", binary_repr), 2) 15 return cls(i)
在.binary_repr ( 第 8 行)中,使用.packed将 IP 地址转换成字节数组,然后将其格式化为二进制形式的字符串表示。
在.from_binary_repr中,对线 14 上int(re.sub(r"[^01]", "", binary_repr), 2)的调用有两部分:
- 它从输入字符串中删除除 0 和 1 之外的任何内容。
- 它用
int(<string>, 2)解析结果,假设基数为 2。
使用.binary_repr()和.from_binary_repr()允许您转换和构造二进制记数法中 1 和 0 的str:
>>> MyIPv4("220.14.9.37").binary_repr
'11011100.00001110.00001001.00100101'
>>> MyIPv4("255.255.0.0").binary_repr # A /16 netmask
'11111111.11111111.00000000.00000000'
>>> MyIPv4.from_binary_repr("11011100 00001110 00001001 00100101")
MyIPv4('220.14.9.37')
这些只是展示利用 IP-as-integer 模式如何帮助您用少量额外代码扩展IPv4Address功能的几种方式。
结论
在本教程中,您看到了 Python 的ipaddress模块如何允许您使用常见的 Python 结构处理 IP 地址和网络。
以下是你可以吸取的一些要点:
- IP 地址从根本上来说是一个整数**,这是你如何用地址做手工运算以及如何使用组合设计来自
ipaddress的 Python 类的基础。** ***ipaddress模块利用操作符重载来允许你推断地址和网络之间的关系。*ipaddress模块使用组合,您可以根据需要扩展该功能以增加行为。*
*和往常一样,如果你想更深入,那么阅读模块源代码是一个很好的方法。
延伸阅读
以下是一些深入的资源,您可以查看以了解更多关于ipaddress模块的信息:

