最近新做了个功能,在华为服务器上运行go的http服务时,看到监听的端口是ipv6
而在阿里云机器上,则是ipv4
恰好该端口telnet不通,误以为是这个原因(其实是华为服务器默认有安全组,只开放5901-5910段),顺便就研究了一下go在Linux上对IPv4/IPv6服务的方式。
先说结论
Go的 net.Listen() 函数,如果不强行指定 IPv4 或 IPv6 ,在双栈系统上(VPS 同时支持 IPv4 和 IPv6)默认只会监听 IPv6 地址。这不影响客户端使用 IPv4 地址来访问。
Linux相关
使用man 7 ipv6来查看如下,"The port space of IPv6 is shared with IPv4",ipv4地址在ipv6上有map ::FFF:<ipv4 address>。
IPv4 connections can be handled with the v6 API by using the v4-mapped-on-v6 address type;thus a program needs to support only this API type to support both protocols.This is handled transparently by the address handling functions in the C library. IPv4 and IPv6 share the local port space.When you get an IPv4 connection or packet to a IPv6 socket,its source address will be mapped to v6 and it will be mapped to v6. The address notation for IPv6 is a group of 8 4-digit hexadecimal numbers, separated with a ':'."::" stands for a string of 0 bits.Special addresses are ::1 for loopback and ::FFFF:<IPv4 address> for IPv4-mapped-on-IPv6.
在 linux 上有个内核参数 net.ipv6.bindv6only ,默认为关闭状态,这样 IPv6 的 socket 也就可以解析映射到同一个网卡的 IPv4 请求了。如果服务需要同时提供 IPv4 和 IPv6 的访问能力,只需要监听一个 IPv6 的 socket 即可。如果不希望 IPv4 可以访问 IPv6 的服务,就把 net.ipv6.bindv6only 置为 1:
$ cat /proc/sys/net/ipv6/bindv6only 1
但是,开启了这个参数后,go服务依然可以使用 IPv4 的地址来访问。
Go语言
内核参数没有生效,问题多半是发生在 syscall 的调用上,祭出 strace大杀器:
可以看到在 220 行 Golang 把准备 listen 的 socket 选项置为了 0。查看代码src/net/ipsock_posix.go :
func favoriteAddrFamily(network string, laddr, raddr sockaddr, mode string) (family int, ipv6only bool) { switch network[len(network)-1] { case '4': return syscall.AF_INET, false case '6': return syscall.AF_INET6, true } if mode == "listen" && (laddr == nil || laddr.isWildcard()) { if supportsIPv4map() || !supportsIPv4() { return syscall.AF_INET6, false } if laddr == nil { return syscall.AF_INET, false } return laddr.family(), false } if (laddr == nil || laddr.family() == syscall.AF_INET) && (raddr == nil || raddr.family() == syscall.AF_INET) { return syscall.AF_INET, false } return syscall.AF_INET6, false}
Go自己定义了 IPV6_V6ONLY 这个行为,至于这么做的原因在官方 Github 也有一些讨论:net: Listen is unfriendly to multiple address families, endpoints and subflows。
如何更准确地控制
在上层可以使用 http.ListenAndServe来选择,如:
http.ListenAndServe(":8083", nil) // tcp http.ListenAndServe("[2604:180:3:dd3::276e]:8083", nil) // 具体指定 tcp6
如果觉得具体指定 IPv6地址太麻烦,可以用 net.Listen 重构 ListenAndServe ,在该函数里指定 network ,可选参数:
"tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only), "udp", "udp4" (IPv4-only), "udp6" (IPv6-only), "ip", "ip4" (IPv4-only), "ip6" (IPv6-only), "unix", "unixgram" and "unixpacket"
常用:
tcp 自动适配,优先IPv6 tcp4 仅使用IPv4 tcp6 仅使用IPv6
示例如下:
package main import ( "fmt" "net" "net/http") type helloHandler struct{} func (h *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello, world!"))} func main() { var err error http.Handle("/", &helloHandler{}) //err = http.ListenAndServe(":8083", nil) // IPv4 或 IPv6 //err = http.ListenAndServe("[2604:180:3:dd3::276e]:8083", nil) // 具体指定,仅 IPv6 err = ListenAndServe(":8083", nil) // 重构 ListenAndServe 函数 if err != nil { fmt.Println(err) } } type tcpKeepAliveListener struct { *net.TCPListener} func ListenAndServe(addr string, handler http.Handler) error { srv := &http.Server{Addr: addr, Handler: handler} addr = srv.Addr if addr == "" { addr = ":http" } ln, err := net.Listen("tcp6", addr) // 仅指定 IPv6 if err != nil { return err } return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})}
客户端可以用下面的命令行检测 IPv6 服务:
curl "http://[2604:180:3:dd3::276e]:8083" curl -g -6 'http://[2604:180:3:dd3::276e]:8083/' telnet -6 2604:180:3:dd3::276e 8083
扫码二维码 获取免费视频学习资料
- 本文固定链接: http://phpxs.com/post/6875/
- 转载请注明:转载必须在正文中标注并保留原文链接
- 扫码: 扫上方二维码获取免费视频资料