logo

IP

入门。

很多语言都内置了很多功能用来处理 IP,比如 Java 中就有InetAddress

jshell> java.net.InetAddress.getByName("192.168.0.1")
$1 ==> /192.168.0.1

注意如果数字前有占位的0,这个 IP 也是合法的:

jshell> java.net.InetAddress.getByName("18.032.4.011")
$2 ==> /18.32.4.11

放弃?

用 C/C++试试?毕竟很多低层的东西是用 C 写的。inet_aton 可以把一个struct in_addr转换成 dots-and-numbers 格式,而inet_ntoa则正好相反,所以连用这两个转换应该得到原来的 IP:

#include <iostream>
#include <arpa/inet.h>

int main() {
  struct in_addr a;
  inet_aton("18.032.4.011", &a);
  std::cout << inet_ntoa(a) << std::endl;
  return 0;
}

打印的结果竟然是18.26.4.9,而不是18.32.4.11

进阶!

这里的关键问题在于,C 看到数字前面的0的时候,会把这个数当成八进制 (octal number),所以八进制的032就是十进制的26,八进制的011就是十进制的9.

在 Go 当中也有相应的函数net.ParseIP()

package main

import (
	"fmt"
	"net"
)

func main() {
	fmt.Println(net.ParseIP("18.032.4.011"))
}

有趣的是,即使都是 Go1(理论上应该兼容),1.16 之前和 1.17 之后的结果却是不同的。假设你已经有了一个更现代版本的 go,你可以再下载一个 1.16 版:

$ go install golang.org/dl/go1.16@latest
$ go1.16 download

这时你运行同样的上面的代码:

go1.16 run ip.go
18.32.4.11

$ go run ip.go
<nil>

1.16 之前是可以解析的,但和 Java 结果一致,和 C 结果不同。而 1.17 之后则缴械投降返回了 nil。而这个改变正式因为 Go 团队发现了 Go 的结果与 C 的结果不一致,与其导致一些隐秘的 bug,干脆不允许带 0 了。(具体可参考后附的 GopherCon 2022 的视频。)

另一个躺平的语言是 Python,如果带 0,直接返回ValueError: '18.032.4.011' does not appear to be an IPv4 or IPv6 address:

>>> import ipaddress
>>> ipaddress.ip_address('192.168.0.1')
IPv4Address('192.168.0.1')
>>> ipaddress.ip_address('18.032.4.011')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/ipaddress.py", line 53, in ip_address
    raise ValueError('%r does not appear to be an IPv4 or IPv6 address' %
ValueError: '18.032.4.011' does not appear to be an IPv4 or IPv6 address

参考

GopherCon 2022 https://www.youtube.com/watch?v=v24wrd3RwGo