深入探索IPv4与IPv6地址背后,那些鲜为人知、甚至堪称奇葩的表示方法及其历史渊源。
在我(原文作者)探索编写高速 IPv4+6 解析器的过程中,我先是写了一个慢吞吞但自认为还算准确的版本,用作后续比较的基准。没想到,就在这个过程中,我意外撞见了更多以前从未留意过的、堪称‘奇葩’的 IP 地址表示方式。来,咱们一起探个究竟!
咱们先从简单的入手,看看IPv4和IPv6的所谓“标准格式”:比如192.168.0.1和1:2:3:4:5:6:7:8。前者,按照各种规范的叫法,是“点分四组”(更具体说是“点分十进制”),用点号分隔的每个字段代表1个字节;后者则是“冒号十六进制”,用冒号分隔的每个字段代表2个字节。
IPv6开始让事情变得稍微复杂了点。要是完全按照标准格式写,常见的地址中间往往会出现一长串连续的零。为了简洁,::应运而生,它允许你省略掉一个或多个连续的16位零块。举个例子,1:2::3:4实际上就等同于1:2:0:0:0:0:3:4。
接下来,还有个更‘诡异’的历史遗留问题:IPv6竟然允许你把地址的最后32位,也就是末尾的部分,直接写成我们熟悉的IPv4点分四组格式!这简直就像是把一个完整的IPv4地址直接‘粘’到了IPv6地址的屁股后面。比如,1:2:3:4:5:6:77.77.88.88这个地址,它真正的意思是1:2:3:4:5:6:4d4d:5858。
当然,这两种简化写法还能组合使用!比如fe80::1.2.3.4,它代表的就是fe80:0:0:0:0:0:102:304。
这个::的存在,也给解析带来了不大不小的麻烦,特别是在处理边界情况时。因为::可以出现在地址的开头或末尾,甚至单独存在。当它出现在开头或末尾时,地址“空”的那一侧并不代表一个16位的字段。比如,::1表示0:0:0:0:0:0:0:1;1::则表示1:0:0:0:0:0:0:0;而单独一个::就代表全零地址0:0:0:0:0:0:0:0。虽然这是::规则下自然而然的结果,但确实给解析器的编写添了点堵。
关于IPv6还有最后一条规则:严格来说,每个冒号十六进制字段应该是4位十六进制数,但正如我前面一直做的,你可以省略掉前面的零。所以,从最最标准的角度看,::其实是0000:0000:0000:0000:0000:0000:0000:0000。
好了,IPv6的部分基本就这些。现在,轮到IPv4登场了!
有趣的是,在IPv6为了定义它那个奇特的‘尾随点分四组’表示法而需要一套语法规则之前,IPv4的文本表示方式竟然从未在任何官方文件中被正式标准化过!所以,我们现在所用的IPv4写法,很大程度上是一个约定俗成的、事实上的标准,主要源自于“当初4.2BSD系统能识别哪些格式?”以及“其他操作系统在借鉴4.2BSD时保留了哪些特性?”这两个问题的答案。
说到4.2BSD,嘿,各位可得坐稳了,因为它对IP地址的看法可真是有点‘放飞自我’!我们就拿192.168.140.255这个大家一看就觉得“嗯,这绝对是个正经IPv4地址”的例子来说吧。猜猜看,表示同一个地址,还能有多少种写法?
比如这个:3232271615。没错,这也是同一个IP地址!它是怎么来的呢?就是把IPv4地址的4个字节看作一个大端序的无符号32位整数,然后直接打印出来。这还引出了一个经典的小把戏:如果你试着在浏览器(比如Chrome)里访问 http://3232271615,它实际上会加载 http://192.168.140.255。
好吧,你可能会觉得,这虽然有点反直觉,但勉强还能理解,对吧?毕竟IPv4地址就是4个字节,把它表示成一个单独的数字虽然对人类不太友好,但逻辑上似乎也说得过去?
那 0300.0250.0214.0377 呢?告诉你,这仍然是那个地址!格式还是点分四组,只不过这次每个部分都用了八进制来表示。
既然八进制都支持了,你可能会想,十六进制是不是也行?恭喜你,猜对了!根据4.2BSD的规矩,192.168.140.255同样可以写作0xc0.0xa8.0x8c.0xff。
现在,让我们回溯到CIDR出现之前的时代。那时候,IPv4地址被划分为A类、B类或C类地址。那真是一个现在看来有些奇怪的划分方式。
而那个时代的划分方式,竟然也渗透到了IP地址的表示法里!我们最熟悉的192.168.140.255这种点分四组的写法,其实技术上讲是所谓的“C类”表示法。但你同样可以用“B类”表示法把它写成192.168.36095,或者用“A类”表示法写成192.11046143。这里发生了什么呢?其实就是把地址末尾的字节合并,当作一个16位(B类)或24位(A类)的整数来处理了。
顺便说一句,这就是为什么像 ping 这样的工具能接受 127.1 这种看起来怪怪的地址,并将其解析为 127.0.0.1。这跟 IPv6 的 :: 省略零不同,它并不是简单地把缺失的部分当作零来扩展。127.1 实际上是采用了 A 类地址的表示逻辑,意思是“在 127 这个网络里,主机号为 1”,而这个“1”是被当作一个 24 位的整数来解读的。
最后,我们遇到了最后一点未明确规定的行为:IPv4地址的每个部分是否允许无限数量的前导零?还是说最多只能有3位数字?像 001.002.003.004 这样的地址,大家普遍认为是有效的。但如果是 0000000001.0000000002.0000000003.000000004 呢?
你可能还会想,既然前面提到前导零可能被解释为八进制,那这些带有前导零的数字是不是也应该按八进制读取呢?答案是:看情况!确实存在过同时支持两种解读方式的实现,但好在,如今大多数现代的实现都已经放弃了八进制和十六进制的特殊处理,统一将带有前导零的数字视为十进制来解析。
关于前导零的争论,在某种程度上也蔓延到了IPv6。比如,000001::00001.00002.00003.00004 是否算是一个有效的IPv6地址(其“常见”形式为 1::1.2.3.4,或 1::102:304)?目前看来,大多数现代的解析器似乎都允许在其表示中出现任意数量的前导零,这很可能是因为它们底层依赖的某些“整数解析”库本身就支持这种行为。
就这样,我们终于走到了这趟探索之旅的(有点令人无奈的)终点。如果你真的立志要编写一个能完美解析所有IP地址的程序,以上这些就是你不得不面对和处理的各种历史遗留和规范模糊之处。
目前,我那个作为参考的慢速解析器,已经主动舍弃了许多陈旧的特性,只支持我认为是这些可能性中比较合理的一部分。它能理解经典的v4点分十进制表示,允许任意数量的前导零。但它不处理A类/B类表示法,也不支持十六进制或八进制记法,同样不处理那种直接表示为32位无符号整数的形式。对于IPv6,它能理解标准的冒号十六进制格式,也支持::缩写以及末尾嵌入IPv4地址的风格(嵌入的IPv4遵循与前面v4相同的规则),并且每个字段都允许任意数量的前导零。
不过,对于最后一点,也就是‘IPv6中嵌入点分十进制IPv4’的格式,我其实还有些犹豫。虽然我的参考解析器(基于Go语言的 net.ParseIP)支持这种格式,但在当今的实际应用中,它的用处已经不大了。回想IPv6刚推出的时候,确实有过这样的设想:可以通过在IPv4地址前加上一对冒号(如 ::1.2.3.4)来将其‘升级’为IPv6地址。然而,现在的各种过渡技术已经不再提供如此简单明了的方案了,因此这种表示法在现实网络世界中也难觅踪迹。
数据加载中...BIU...BIU...BIU...