在编写网络相关的程序代码时,我们经常需要处理 IP 地址。当同时需要对 IPv4/IPv6 双栈网络支持时,如何简洁、优雅地表示 IP 地址也是需要些技巧的。
这篇文章将分析 Go 库的 net/netip
包,学习 Go 标准库如何表示和处理 IP 地址/网段。
Addr 类型 netip.Addr
类型可以表示 IPv4 或者 IPv6 地址,它是整个 netip
包的核心类型。它的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type Addr struct { addr uint128 z unique.Handle[addrDetail] } type uint128 struct { hi uint64 lo uint64 } type addrDetail struct { isV6 bool zoneV6 string }
addr
字段是一个 uint128
类型的字段,uint128
是 netip
包内部定义的一个类型,可以用来存储 16 字节长度的整型数。Addr
类型使用该字段来存储 IPv4/IPv6 中的地址数据:它将 16 字节长的地址以大端序的方式存储在该 uint128
类型中,其中最高有效位在 uint128.hi
中、最低有效位在 uint128.lo
中。例如,对于 IPv6 地址 0011:2233:4455:6677:8899:aabb:ccdd:eeff
按照如下方式保存:
addr.hi = 0x0011223344556677
addr.lo = 0x8899aabbccddeeff
使用两个 uint64
类型的字段而不是 [16]byte
来保存地址数据,使得 IP 地址的绝大多数操作都可以变成 64 位寄存器的算术/位运算,这样比字节操作运算更快。
对于 IPv4 地址,addr
字段保存的是 IPv4-mapped 格式的 IPv6 地址 。此时为了快速判断一个 Addr
对象的 IP 地址类型,就需要依赖 z
字段了。它使用了 unique
包来实现 addrDetail
类型值的 intern
化,用于表示该 IP 地址的细节信息,例如区分 IPv4/IPv6 地址、保存 IPv6 地址的 zone 信息。使用 intern
技术来保存地址的详细信息可以节省内存,同时提高比较效率。netip
包预定义了如下 Addr.z
类型的变量,分别表示 Addr
零值、IPv4 地址、IPv6 地址(不含 zone 信息):
1 2 3 4 5 var ( z0 unique.Handle[addrDetail] z4 = unique.Make(addrDetail{}) z6noz = unique.Make(addrDetail{isV6: true }) )
接下来看 netip
包提供的两个构造 Addr
对象的函数实现,以便加深对 Addr
类型的理解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func AddrFrom4 (addr [4]byte ) Addr { return Addr{ addr: uint128{0 , 0xffff00000000 | uint64 (addr[0 ])<<24 | uint64 (addr[1 ])<<16 | uint64 (addr[2 ])<<8 | uint64 (addr[3 ])}, z: z4, } } func AddrFrom16 (addr [16]byte ) Addr { return Addr{ addr: uint128{ byteorder.BeUint64(addr[:8 ]), byteorder.BeUint64(addr[8 :]), }, z: z6noz, } }
可以看到,AddrFrom4
/AddrFrom16
函数以大端字节序的方式将代表 IP 地址的字节切片保存到到 uint128
中,同时根据地址类型的不同,设置 Addr.z
字段的值。
接下来简单列举与 netip.Addr
相关的函数及方法。
Addr 构造相关函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func AddrFrom4 (addr [4]byte ) Addrfunc AddrFrom16 (addr [16]byte ) Addrfunc AddrFromSlice (slice []byte ) (ip Addr, ok bool )func ParseAddr (s string ) (Addr, error )func MustParseAddr (s string ) Addr
Addr 判断相关函数/方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 func (ip Addr) IsValid() bool { return ip.z != z0 }func (ip Addr) BitLen() int { switch ip.z { case z0: return 0 case z4: return 32 } return 128 } func (ip Addr) Is4() bool { return ip.z == z4 } func (ip Addr) Is6() bool { return ip.z != z0 && ip.z != z4 } func (ip Addr) Is4In6() bool { return ip.Is6() && ip.addr.hi == 0 && ip.addr.lo>>32 == 0xffff }
尤其需要注意 Is4()/Is6()/Is4In6() 的判断,虽然在 Addr 中始终都是以 IPv4inIPv6
格式来存储 IPv4 地址数据,但是对于 IPv4 地址,其 z 字段始终都是 z4
。而对于真正的 IPv4-mapped
的 IPv6 地址,其 z 字段则不是 z4
。以下例子说明了这个区别:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport ( "fmt" "net/netip" ) func main () { a4 := netip.MustParseAddr("192.168.1.0" ) a46 := netip.MustParseAddr("::FFFF:192.168.1.0" ) a6 := netip.MustParseAddr("::FFFF:192:168:1:0" ) fmt.Println(a4.Is4(), a4.Is6(), a4.Is4In6()) fmt.Println(a46.Is4(), a46.Is6(), a46.Is4In6()) fmt.Println(a6.Is4(), a6.Is6(), a6.Is4In6()) }
以下函数则用来判断某个 Addr 是否属于某类特殊地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 func (ip Addr) IsLinkLocalUnicast() bool func (ip Addr) IsLoopback() bool func (ip Addr) IsMulticast() bool func (ip Addr) IsInterfaceLocalMulticast() bool func (ip Addr) IsLinkLocalMulticast() bool func (ip Addr) IsGlobalUnicast() bool func (ip Addr) IsPrivate() bool
另外,netip
库也提供了以下函数,直接生成某些特殊地址:
1 2 3 4 5 func IPv4Unspecified () Addrfunc IPv6Unspecified () Addrfunc IPv6LinkLocalAllNodes () Addrfunc IPv6LinkLocalAllRouters () Addrfunc IPv6Loopback () Addr
Addr 其他函数/方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 func (ip Addr) Zone() string func (ip Addr) WithZone(zone string ) Addrfunc (ip Addr) Compare(ip2 Addr) int func (ip Addr) Less(ip2 Addr) bool { return ip.Compare(ip2) == -1 }func (ip Addr) Unmap() Addrfunc (ip Addr) As16() (a16 [16 ]byte )func (ip Addr) As4() (a4 [4 ]byte )func (ip Addr) AsSlice() []byte func (ip Addr) Next() Addrfunc (ip Addr) Prev() Addrfunc (ip Addr) String() string func (ip Addr) AppendTo(b []byte ) []byte func (ip Addr) StringExpanded() string func (ip Addr) MarshalText() ([]byte , error )func (ip *Addr) UnmarshalText(text []byte ) error func (ip Addr) MarshalBinary() ([]byte , error )func (ip *Addr) UnmarshalBinary(b []byte ) error
AddrPort 类型 AddrPort
类型表示 IP 地址和端口的组合,它的定义如下:
1 2 3 4 type AddrPort struct { ip Addr port uint16 }
AddrPort
类型的定义非常简单,使用 Addr
表示 IP 地址,使用 uint16
表示端口。下面再简单列举 AddrPort
提供的相关函数及方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 func AddrPortFrom (ip Addr, port uint16 ) AddrPortfunc ParseAddrPort (s string ) (AddrPort, error )func MustParseAddrPort (s string ) AddrPortfunc (p AddrPort) Addr() Addr { return p.ip }func (p AddrPort) Port() uint16 { return p.port }func (p AddrPort) IsValid() bool { return p.ip.IsValid() }func (p AddrPort) Compare(p2 AddrPort) int func (p AddrPort) String() string func (p AddrPort) AppendTo(b []byte ) []byte func (p AddrPort) MarshalText() ([]byte , error )func (p *AddrPort) UnmarshalText(text []byte ) error func (p AddrPort) MarshalBinary() ([]byte , error )func (p *AddrPort) UnmarshalBinary(b []byte ) error
Prefix 类型 与网路地址息息相关的一个概念是网段,用于表示一个网络。netip
包定义了 Prefix
类型来表示一个网段,它的定义如下:
1 2 3 4 5 6 7 8 9 10 type Prefix struct { ip Addr bitsPlusOne uint8 }
注意 Prefix 中的 ip 可以是任意的 IP 地址,并不一定要真的是一个网络地址(即主机位全为 0 的 IP 地址) 。而且 Prefix 中的 ip 总是没有 zone 信息的。
下面同样列举与 Prefix 类型相关的函数及方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 func PrefixFrom (ip Addr, bits int ) Prefixfunc (ip Addr) Prefix(b int ) (Prefix, error )func ParsePrefix (s string ) (Prefix, error )func MustParsePrefix (s string ) Prefixfunc (p Prefix) Addr() Addr { return p.ip }func (p Prefix) Bits() int { return int (p.bitsPlusOne) - 1 }func (p Prefix) IsValid() bool { return p.bitsPlusOne > 0 }func (p Prefix) IsSingleIP() bool { return p.IsValid() && p.Bits() == p.ip.BitLen() }func (p Prefix) Masked() Prefixfunc (p Prefix) Contains(ip Addr) bool func (p Prefix) Overlaps(o Prefix) bool func (p Prefix) AppendTo(b []byte ) []byte func (p Prefix) MarshalText() ([]byte , error )func (p *Prefix) UnmarshalText(text []byte ) error func (p Prefix) MarshalBinary() ([]byte , error )func (p *Prefix) UnmarshalBinary(b []byte ) error func (p Prefix) String() string
net.IP
、net.IPMask
和 net.IPNet
类型除了 net/netip
包提供的 Addr
及相关类型外,标准库 net
包中还定义了 IP
、IPMask
和 IPNet
类型,分别用于表示 IP 地址、IP 掩码、IP 网段。相关的代码位于 src/net/ip.go
文件中。相比于 netip
包,net
包中的 IP 地址实现更加简单,但是是不可比较的(即不支持 == 或者作为 map 的 key)。而且 net
包的部分实现也依赖于 netip
包。
如下展示了 net
包中这些类型的定义:
1 2 3 4 5 6 7 8 9 10 11 package nettype IP []byte type IPMask []byte type IPNet struct { IP IP Mask IPMask }
可以看到,IP 和 IPMask 都是基于字节切片类型实现的,因此它们都是不可比较类型。无论对于 IPv4 还是 IPv6 地址,内部实现总是使用 16
字节来保存 IP 地址数据。即对于 IPv4 地址,总是保存其 IPv4-mapped
格式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var v4InV6Prefix = []byte {0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0xff , 0xff }func IPv4 (a, b, c, d byte ) IP { p := make (IP, IPv6len) copy (p, v4InV6Prefix) p[12 ] = a p[13 ] = b p[14 ] = c p[15 ] = d return p }
而 IPMask 则是根据实际的地址类型,分配对应的字节切片长度:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const ( IPv4len = 4 IPv6len = 16 ) func IPv4Mask (a, b, c, d byte ) IPMask { p := make (IPMask, IPv4len) p[0 ] = a p[1 ] = b p[2 ] = c p[3 ] = d return p }
在使用 net
包提供的 IP 相关功能时,一定要注意上述实现细节。不能简单的根据字节切片长度来区分是否是 IPv4 还是 IPv6:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport "net" func main () { ip := net.IPv4(1 , 1 , 1 , 1 ) m := net.IPv4Mask(1 , 1 , 1 , 1 ) println (len (ip), len (m)) ip = ip.To4() println (len (ip), len (m)) }
下面简单列举 net
包提供的 IP 地址/网络相关的函数及方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 func IPv4 (a, b, c, d byte ) IPfunc ParseIP (s string ) IPfunc (ip IP) To4() IPfunc (ip IP) To16() IPfunc (ip IP) DefaultMask() IPMaskfunc (ip IP) Mask(mask IPMask) IPfunc (ip IP) String() string func (ip IP) MarshalText() ([]byte , error )func (ip *IP) UnmarshalText(text []byte ) error func (ip IP) Equal(x IP) bool
1 2 3 4 5 6 7 8 9 10 func IPv4Mask (a, b, c, d byte ) IPMaskfunc CIDRMask (ones, bits int ) IPMaskfunc (m IPMask) Size() (ones, bits int )func (m IPMask) String() string
1 2 3 4 5 6 7 8 func ParseCIDR (s string ) (IP, *IPNet, error )func (n *IPNet) Contains(ip IP) bool func (n *IPNet) String() string
小结 文本介绍了 Go 标准库为 IP 地址/网络等功能提供的基础设施。net/netip
和 net
包都提供了 IP 地址相关的 API,我们可以根据实际需要选择使用对应的工具。但是需要注意,这些包中提供的地址类型被用于同时表示 IPv4 或 IPv6,而 IPv4-mapped
的 IPv6 地址又使情况更加复杂,因此在使用相关接口时需要额外小心。
Reference