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

时间:2018-06-27 20:35:45

标签: python macos sockets go networking

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

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

试图绑定到该端口的python程序(使用我可以管理的与golang尽可能接近的socket选项),以我期望的方式失败:

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上这会成功,如何获取网络。听一听显示绑定到端口8888时出现错误?

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

package main

import (
    "log"
    "net/http"
)

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

然后test.go无法正确绑定到端口。但是docker进程(基本上在运行^^^)不会导致它失败。

edit 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”连接类型到底是怎么回事,而我找不到任何文档。救命!

2 个答案:

答案 0 :(得分:1)

好的,我想我有一个故事来讲述为什么发生这种情况:

  1. 在Mac上的Docker映射端口时,它绑定到IPv4 0.0.0.0:<port>和IPv6 [::1]:<port>。请注意,在IPv6上,它映射到localhost的等效项,而不是0.0.0.0的{​​{1}}!
  2. Golang在打开套接字进行侦听时,默认情况下会打开一个IPv6套接字,该套接字以某种方式映射为也可以侦听IPv4。 (我仍然不完全理解这种[::]类型,因此,如果您有很好的文档,请指向我!)。
  3. 因此我的golang程序正在tcp46上打开一个IPv6套接字,相当于IPv6中的[::]:8888。之所以成功,是因为docker正在监听0.0.0.0:8888(相当于127.0.0.1),不是 [::1]
  4. 就是这样!因此,即使golang程序仅通过从非环回地址在IPv6上连接的客户端进行连接(我认为,我也太累了,无法测试它,让我休息一下),该程序还是成功的。

我的同事报告说,在Ubuntu上,docker监听[::],这就是为什么他无法重现我看到的问题的原因。这似乎是明智的行为!而且我不知道为什么在Mac上不这样做。

我还认为,即使Go创建的套接字实际上很难访问,它在这种情况下成功地获得成功还是令人惊讶的,甚至可能有点错误。但是我不能说这绝对是一个错误,而且我绝对不想尝试将其报告给go项目。

答案 1 :(得分:1)

关于tcp46的{​​{1}}输出,我找不到任何文档,但确实找到了相关来源。

来自network_cmds-543/netstat.proj/inet.c

netstat

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 " : " "; bsd/netinet/in_pcb.h中定义。

xinpcb_n

在该文件的其他位置, * struct inpcb captures the network layer state for TCP, UDP and raw IPv6 * and IPv6 sockets. 被记录为inp_vflag。依次将它们定义为:

/* INP_IPV4 or INP_IPV6 */

因此,基本上,当套接字同时设置了v4和v6位时,它将在协议列中显示#define INP_IPV4 0x1 #define INP_IPV6 0x2


关于Go,有this socket() function in the net package

46

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 { 具有每个平台的定义,以下是the BSD variant的摘录:

setDefaultSockopts()

因此,套接字上允许同时使用两个IP版本都是由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)) 布尔值驱动的。经过进一步的挖掘,我发现了where this is decided并提供了有关逻辑的详细说明:

ipv6Only