使用 Python 的 ipaddress 模块学习 IP 地址概念

举报
Yuchuan 发表于 2021/08/20 08:37:48 2021/08/20
【摘要】 在本教程中,您了解了 Python 的ipaddress模块如何允许您使用常见的 Python 构造来处理 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 网络子网图

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)中,那么你可能知道一个名字你如eth0en0ens3。这两种类型的接口是不相关的。

换句话说,单独的 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

int(my_addr) 的调用堆栈

如果您向CPython开发人员询问了这一点,那么他们可能会告诉您这._ip是一个实现细节。虽然 Python 中没有真正私有的,但前导下划线表示它._ip是准私有的,不是公共ipaddressAPI 的一部分,如有更改,恕不另行通知。这就是为什么用 提取底层整数更稳定的原因int(addr)

尽管如此,正是底层._ip赋予了IPv4AddressIPv4Network类它们的魔力。

扩展 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_reprint(re.sub(r"[^01]", "", binary_repr), 2)第 14 行的调用有两个部分:

  1. 它从输入字符串中删除除 0 和 1 之外的任何内容。
  2. 它解析结果,假设基数为 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模块的更多信息:

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。