Golang net.Listen 绑定到已被使用的端口

7

我的(OS X 10.13.5)系统上端口8888已经被运行在docker容器中的进程绑定:

$ netstat -an | grep 8888
tcp6       0      0  ::1.8888               *.*                    LISTEN
tcp4       0      0  *.8888                 *.*                    LISTEN

这是一个关于IT技术的翻译内容:一个Python程序试图绑定到该端口(使用尽可能接近golang套接字选项的方式),但以我预期的方式失败了:

import socket
import fcntl
import os


def main():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    flag = fcntl.fcntl(sock.fileno(), fcntl.F_GETFL)
    fcntl.fcntl(sock.fileno(), fcntl.F_SETFL, flag | os.O_NONBLOCK)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    sock.bind(("0.0.0.0", 8888))
    sock.listen(5)


main()

出现以下错误:

$ python test.py
Traceback (most recent call last):
  File "test.py", line 15, in <module>
    main()
  File "test.py", line 11, in main
    sock.bind(("0.0.0.0", 8888))
OSError: [Errno 48] Address already in use

但是,通过 net.Listen 创建连接的 Go 程序并不会像我预期的那样失败:

package main

import (
    "fmt"
    "net"
)

func main() {
    _, err := net.Listen("tcp", "0.0.0.0:8888")
    if err != nil {
        fmt.Printf("Connection error: %s\n", err)
    } else {
        fmt.Println("Listening")
    }
}

成功的要素包括:

$ go run test.go
Listening

一位同事报告说,在相同的设置下,他的Ubuntu系统正确地使go程序失败。

为什么Mac上会成功,我如何让net.Listen在绑定到端口8888时显示错误?

编辑:如果我使用一个简单的go程序占用端口8888:

package main

import (
    "log"
    "net/http"
)

func main() {
    log.Fatal(http.ListenAndServe("0.0.0.0:8888", nil))
}

然后test.go无法绑定端口。但是运行基本上就是那个 ^^^ 的docker进程并没有导致它失败。

编辑2:如果我指定“tcp4”,那么程序确实会像我期望的那样失败。如果我指定“tcp6”,它会成功,但netstat会说它绑定到*而不是::1

$ netstat -an | grep 8888
tcp6       0      0  *.8888                 *.*                    LISTEN
tcp6       0      0  ::1.8888               *.*                    LISTEN
tcp4       0      0  *.8888                 *.*                    LISTEN

因此,指定"tcp4"将解决我的实际问题,但我真的想了解"tcp46"连接类型到底是怎么回事,但我找不到任何文档资料。求助!


你确定你没有运行一个旧版本的构建,或者端口不同之类的吗?http.ListenAndServe也会在内部调用net.Listen("tcp", addr):https://github.com/golang/go/blob/master/src/net/http/server.go#L2728 - Emile Pels
在您的Go程序末尾添加time.Sleep(10 * time.Second)调用并在其运行时获取netstat输出可能会很有用。 - chuckx
优秀的点子:https://gist.github.com/llimllib/9f86f61a9436e9f59edf39d97c9b8dca。它绑定到`tcp46`而不是`tcp4`或`tcp6`,但我不知道这真正意味着什么? - llimllib
当Go程序正在运行时,你能提供lsof -i -T f | grep 8888的输出吗? - chuckx
@chuckx lsof -i -T f | grep 8888 在程序运行前和运行期间都是空的,你确定这些是正确的Mac lsof选项吗? - llimllib
显示剩余3条评论
2个回答

5

好的,我想我有一个关于这种情况发生的故事:

  1. 在Mac上,当映射端口时,Docker会绑定IPv4端口0.0.0.0:<port>和IPv6端口[::1]:<port>。请注意,在IPv6上,它将映射到等效于localhost而不是0.0.0.0,后者将是[::]
  2. Golang在打开要监听的套接字时,默认情况下会打开一个IPv6套接字,该套接字以某种方式映射到IPv4并进行监听。(我仍然不完全理解这个tcp46类型,如果您有好的文档,请告诉我!)
  3. 因此,我的golang程序在[::]:8888上打开了一个IPv6套接字,相当于IPv6中的0.0.0.0:8888。这成功了,因为docker正在侦听[::1](等效于127.0.0.1),而不是[::]
  4. 就是这样!因此,尽管只能通过从非回环地址连接的IPv6客户端连接,但golang程序仍然成功了(我认为,我太累了,无法测试这一点,请原谅)

我的同事报告说,在Ubuntu上,Docker侦听[::],这就是他无法复现我遇到的问题的原因。这似乎是明智的行为!但我不知道为什么在Mac上没有这样做。

我也认为令人惊讶且可能有点错误的是,尽管创建了一个非常难以访问的套接字,但Go在这种情况下仍然可以成功?但我不能确定这绝对是一个bug,我肯定不想试图向go项目报告这个问题。


1
我也遇到了这个问题,感谢您的解释。这种行为非常不直观,在文档中也没有提到。我猜如果你想强制出错,你必须手动监听 tcp4 而不是使用 listenAndServe 包装器?就像 https://github.com/shazow/ssh-chat/issues/139 - 1110101001

1
关于从netstat中获取的tcp46输出,我没有找到任何文档,但我找到了相关源代码。
来自network_cmds-543/netstat.proj/inet.c
void
protopr(uint32_t proto,     /* for sysctl version we pass proto # */
        char *name, int af)
{

...

    struct xinpcb_n *inp = NULL;

...

            const char *vchar;

#ifdef INET6
            if ((inp->inp_vflag & INP_IPV6) != 0)
                vchar = ((inp->inp_vflag & INP_IPV4) != 0)
                ? "46" : "6 ";
            else
#endif
                vchar = ((inp->inp_vflag & INP_IPV4) != 0)
                ? "4 " : "  ";

xinpcb_nbsd/netinet/in_pcb.h 中被定义。

 * struct inpcb captures the network layer state for TCP, UDP and raw IPv6
 * and IPv6 sockets.

在该文件的其他地方,inp_vflag 被记录为 /* INP_IPV4 or INP_IPV6 */。这些又被定义为:

#define INP_IPV4    0x1
#define INP_IPV6    0x2

基本上,当套接字同时设置了v4和v6位时,协议列将显示46

关于Go语言,net包中有一个socket()函数

func socket(ctx context.Context, net string, family, sotype, proto int, ipv6only bool, laddr, raddr sockaddr, ctrlFn func(string, string, syscall.RawConn) error) (fd *netFD, err error) {

...

if err = setDefaultSockopts(s, family, sotype, ipv6only); err != nil {

setDefaultSockopts()有针对每个平台的定义,这是BSD变体的摘录:

func setDefaultSockopts(s, family, sotype int, ipv6only bool) error {

...

    if supportsIPv4map() && family == syscall.AF_INET6 && sotype != syscall.SOCK_RAW {
        // Allow both IP versions even if the OS default
        // is otherwise. Note that some operating systems
        // never admit this option.
        syscall.SetsockoptInt(s, syscall.IPPROTO_IPV6, syscall.IPV6_V6ONLY, boolint(ipv6only))

因此,在套接字上允许两个IP版本是由ipv6Only布尔值驱动的。经过更深入的挖掘,我发现决定这一点的位置,其中包括逻辑的详细解释:
// favoriteAddrFamily returns the appropriate address family for the
// given network, laddr, raddr and mode.
//
// If mode indicates "listen" and laddr is a wildcard, we assume that
// the user wants to make a passive-open connection with a wildcard
// address family, both AF_INET and AF_INET6, and a wildcard address
// like the following:
//
//  - A listen for a wildcard communication domain, "tcp" or
//    "udp", with a wildcard address: If the platform supports
//    both IPv6 and IPv4-mapped IPv6 communication capabilities,
//    or does not support IPv4, we use a dual stack, AF_INET6 and
//    IPV6_V6ONLY=0, wildcard address listen. The dual stack
//    wildcard address listen may fall back to an IPv6-only,
//    AF_INET6 and IPV6_V6ONLY=1, wildcard address listen.
//    Otherwise we prefer an IPv4-only, AF_INET, wildcard address
//    listen.
//
//  - A listen for a wildcard communication domain, "tcp" or
//    "udp", with an IPv4 wildcard address: same as above.
//
//  - A listen for a wildcard communication domain, "tcp" or
//    "udp", with an IPv6 wildcard address: same as above.
//
//  - A listen for an IPv4 communication domain, "tcp4" or "udp4",
//    with an IPv4 wildcard address: We use an IPv4-only, AF_INET,
//    wildcard address listen.
//
//  - A listen for an IPv6 communication domain, "tcp6" or "udp6",
//    with an IPv6 wildcard address: We use an IPv6-only, AF_INET6
//    and IPV6_V6ONLY=1, wildcard address listen.
//
// Otherwise guess: If the addresses are IPv4 then returns AF_INET,
// or else returns AF_INET6. It also returns a boolean value what
// designates IPV6_V6ONLY option.
//
// Note that the latest DragonFly BSD and OpenBSD kernels allow
// neither "net.inet6.ip6.v6only=1" change nor IPPROTO_IPV6 level
// IPV6_V6ONLY socket option setting.

1
谢谢您的帮助!如果没有lsof,我认为我们不可能解决它。这是有关Unix网络中这个奇怪角落的有价值信息 :) - llimllib

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接