使用 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 源代码还ipaddress
IPv4Address
提供了一些关于如何使用称为组合的模式为您自己的代码提供惯用 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
通过扩展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)