使用 Python 的 ipaddress 模块学习 IP 地址概念
目录
Python 的ipaddress模块是 Python 标准库中被低估的珍宝。您不必是一个成熟的网络工程师,就可以在野外暴露于 IP 地址。IP 地址和网络在软件开发和基础设施中无处不在。它们是计算机如何相互寻址的基础。
边做边学是掌握IP地址的有效方法。该ipaddress模块允许您通过将 IP 地址作为 Python 对象查看和操作来实现这一点。在本教程中,您将通过使用 Pythonipaddress模块的一些功能更好地掌握 IP 地址。
在本教程中,您将学习:
- 如何IP地址的工作,无论是在理论上还是在Python代码
- IP 网络如何表示 IP 地址组以及如何检查两者之间的关系
- Python 的
ipaddress模块如何巧妙地使用经典的设计模式,让你事半功倍
要继续,您只需要 Python 3.3 或更高版本,因为它ipaddress已添加到该版本的 Python 标准库中。本教程中的示例是使用Python 3.8生成的。
理论与实践中的 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。每个八位字节是一个byte,或一个从 0 到 255 的数字。鉴于此,您可以推断出 IPv4 地址的最大值为255.255.255.255(或FF.FF.FF.FF十六进制表示法),而最小值为0.0.0.0。
接下来,您将看到 Python 的ipaddress模块如何为您执行此计算,从而允许您使用人类可读的形式并让地址算术在视线之外发生。
Pythonipaddress模块
接下来,您可以获取计算机的外部 IP 地址以在命令行中使用:
$ curl -sS ifconfig.me/ip
220.14.9.37
这会从站点ifconfig.me请求您的 IP 地址,该地址可用于显示有关您的连接和网络的一系列详细信息。
注意:为了技术上的正确性,这很可能不是您计算机自己的公共 IP 地址。如果您的连接位于NATed路由器后面,那么最好将其视为您访问 Internet 的“代理”IP。
现在打开一个 Python REPL。您可以使用IPv4Address该类来构建一个封装地址的 Python 对象:
>>> from ipaddress import IPv4Address
>>> addr = IPv4Address("220.14.9.37")
>>> addr
IPv4Address('220.14.9.37')
将str诸如此类传递"220.14.9.37"给IPv4Address构造函数是最常见的方法。但是,该类也可以接受其他类型:
>>> 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可能是更常见的方式,但bytes如果您正在处理诸如TCP packet data 之类的东西,您可能会看到输入。
上面的转换也可以在另一个方向上进行:
>>> 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
您可以使用任何标准比较运算符来比较地址对象的整数值。
注意:本教程重点介绍 Internet 协议版本 4 (IPv4) 地址。还有一些 IPv6 地址,它们是 128 位而不是 32 位,并以标题形式表示,例如2001:0:3238:dfe1:63::fefb. 由于地址的算术基本相同,因此本教程从等式中删除了一个变量,重点关注 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吗?在这种情况下,答案是肯定的,因为 的前 24 位192.4.2.12是前三个八位字节 ( 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
上面,最后 8 位192.4.2.12被屏蔽(用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')
正如您在上面192.4.2.0看到的,当掩码应用于主机 IP 地址时,网络地址可以看作是预期的结果:
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前缀实际上是如何转换为 true 的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 的倍数:
| 前缀长度 | 地址数 | 网络掩码 |
|---|---|---|
| 8 | 16,777,216 | 255.0.0.0 |
| 16 | 65,536 | 255.255.0.0 |
| 24 | 256 | 255.255.255.0 |
| 32 | 1 | 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。让我们以维基百科中的这个例子为例:

IPv4 网络子网划分
这个例子从一个/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.6网络中的 IP 地址192.168.1.0/24”。
注意:在计算机网络的上下文中,接口也可以指网络接口,最常见的是网络接口卡 (NIC)。如果你曾经使用过的ifconfig工具(* nix中)或ipconfig(Windows)中,那么你可能知道一个名字你如eth0,en0或ens3。这两种类型的接口是不相关的。
换句话说,单独的 IP 地址并不能告诉您该地址位于哪个或哪些网络,并且网络地址是一组 IP 地址而不是单个 IP 地址。这IPv4Interface为您提供了一种通过 CIDR 表示法同时表示单个主机 IP 地址及其网络的方法。
特殊地址范围
既然您对 IP 地址和网络都有较高的了解,那么了解并非所有 IP 地址都是平等的——有些是特殊的,了解这一点也很重要。
互联网号码分配机构 (IANA) 与互联网工程任务组 (IETF) 协同监督不同地址范围的分配。IANA 的IPv4 特殊用途地址注册表是一个非常重要的表格,规定某些地址范围应具有特殊含义。
一个常见的例子是私有地址。专用 IP 地址用于网络上不需要连接到公共 Internet 的设备之间的内部通信。以下范围保留供私人使用:
| 范围 | 地址数 | 网络地址 | 广播地址 |
|---|---|---|---|
10.0.0.0/8 |
16,777,216 | 10.0.0.0 |
10.255.255.255 |
172.16.0.0/12 |
1,048,576 | 172.16.0.0 |
172.31.255.255 |
192.168.0.0/16 |
65,536 | 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
第二种特殊地址类型是链路本地地址,它只能从给定的子网内访问。一个例子是Amazon Time Sync Service,它可用于链接本地 IP 上的AWS EC2实例169.254.169.123。如果您的 EC2 实例位于Virtual Private Cloud (VPC) 中,那么您不需要 Internet 连接来告诉您的实例现在是什么时间。块 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模块还包含为专用网络分配的其他地址:
这不是一个详尽的列表,但它涵盖了最常见的情况。
ipaddress引擎盖下的Python模块
除了其文档化的 API 之外,模块及其类的CPython 源代码还ipaddressIPv4Address提供了一些关于如何使用称为组合的模式为您自己的代码提供惯用 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是准私有的,不是公共ipaddressAPI 的一部分,如有更改,恕不另行通知。这就是为什么用 提取底层整数更稳定的原因int(addr)。
尽管如此,正是底层._ip赋予了IPv4Address和IPv4Network类它们的魔力。
扩展 IPv4Address
您可以._ip通过扩展IPv4 地址类来展示底层整数的强大功能:
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__()允许您使用 anotherIPv4Address或 anint直接作为掩码。因为MyIPv4是 的子类IPv4Address,在这种情况下isinstance()检查将返回True。
除了运算符重载之外,您还可以添加全新的属性:
import 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 行)中, using.packed将 IP 地址转换为字节数组,然后将其格式化为其二进制形式的字符串表示形式。
在 中.from_binary_repr,int(re.sub(r"[^01]", "", binary_repr), 2)对第 14 行的调用有两个部分:
- 它从输入字符串中删除除 0 和 1 之外的任何内容。
- 它解析结果,假设基数为 2,
int(<string>, 2).
使用.binary_repr()和.from_binary_repr()允许您以str二进制表示法转换为1 和 0 的a 并从中构造:
>>> 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 地址基本上是一个integer,这是如何使用地址进行手动算术以及如何
ipaddress使用组合设计Python 类的基础。 - 该
ipaddress模块利用运算符重载来推断地址和网络之间的关系。 - 该
ipaddress模块使用组合,您可以根据需要扩展该功能以添加行为。
与往常一样,如果您想深入了解,那么阅读模块源代码是一个很好的方法。
进一步阅读
以下是一些深入的资源,您可以查看这些资源以了解有关该ipaddress模块的更多信息:
- 点赞
- 收藏
- 关注作者
评论(0)