Go net/dial.go 阅读笔记(二)

Go net/dial.go 阅读笔记(二)

上一篇文章 我们大致分析了dial.go中的代码,起主要的功能就是为真正发起连接做一些准备,起到了应用层的作用(DNS解析等)。但是一个连接完整的连接还需要更深层次的网络协议来完成协作,所以我们接着上篇来分析,由于篇(懒)幅原因,只将dialTcp作为传输层的例子。。。话不多说,上代码:

func dialTCP(ctx context.Context, net string, laddr, raddr *TCPAddr) (*TCPConn, error) {
    if testHookDialTCP != nil { //testHookDialTCP 是语言开发者为了测试留的钩子函数,不用管
        return testHookDialTCP(ctx, net, laddr, raddr)
    }
    return doDialTCP(ctx, net, laddr, raddr)
}

注意现在所在文件是在tcpsock_posix.go 这部分是传输层的内容了。

来看doDialTCP:

func doDialTCP(ctx context.Context, net string, laddr, raddr *TCPAddr) (*TCPConn, error) {
    fd, err := internetSocket(ctx, net, laddr, raddr, syscall.SOCK_STREAM, 0, "dial")

    for i := 0; i < 2 && (laddr == nil || laddr.Port == 0) && (selfConnect(fd, err) || spuriousENOTAVAIL(err)); i++ {
        if err == nil {
            fd.Close()
        }
        fd, err = internetSocket(ctx, net, laddr, raddr, syscall.SOCK_STREAM, 0, "dial")
    }

    if err != nil {
        return nil, err
    }
    return newTCPConn(fd), nil
}

参数里的ctx自然不言而喻了,是为了控制请求超时取消请求释放资源的;laddr是 local address , raddr是指 remote address;返回值这里会得到 TCPConn。代码不长,就是调用了 internetSocket得到一个文件描述符,并用其新建一个conn返回。但这里我想多说几句,因为不难发现, internetSocket可能会被调用多次,为什么呢?

首先我们需要知道 Tcp 有一个极少使用的机制,叫simultaneous connection(同时连接)。正常的连接是:A主机 dial B主机,B主机 listen。 而同时连接则是: A 向 B dial 同时 B 向 A dial,那么 A 和 B 都不需要监听。

我们知道,当 传入 dial 函数的参数laddr==raddr时,内核会拒绝dial。但如果传入的laddr为nil,kernel 会自动选择一个本机端口,这时候有可能会使得新的laddr==raddr,这个时候,kernel不会拒绝dial,并且这个dial会成功,原因是就simultaneous connection,这可能是kernel的bug。所以会判断是否是 selfConnect或者spuriousENOTAVAIL(spurious error not avail)来判断上一次调用internetSocket返回的 err 类型,在特定的情况下重新尝试internetSocket.关于这个问题的讨论参见这里。

好了,我们接下来看看internetSocket,该函数在ipsock_posix.go文件,到了网络层的范围了。

func internetSocket(ctx context.Context, net string, laddr, raddr sockaddr, sotype, proto int, mode string) (fd *netFD, err error) {
    if (runtime.GOOS == "windows" || runtime.GOOS == "openbsd" || runtime.GOOS == "nacl") && mode == "dial" && raddr.isWildcard() {
        raddr = raddr.toLocal(net) 
      // 如果 raddr 是零地址,把它转化成当前系统对应的零地址格式(local system address 127.0.0.1 or ::1)
    }
    family, ipv6only := favoriteAddrFamily(net, laddr, raddr, mode)
    return socket(ctx, net, family, sotype, proto, ipv6only, laddr, raddr)
}

(sotype 和 proto 是生成 socket 文件d的系统调用时用的)首先判断了运行系统的类型,favoriteAddrFamily返回了当前 dial 最合适的地址族,主要是判断应该用ipv4还是ipv6或者都用,其返回值 family 有两种可能值:AF_INETAF_INET6,都是int类型,感兴趣的朋友可以参见这里。

让我们接着关注socket,该函数在sock_posix.go文件,意味着接下来将是更加底层的系统调用了。

// socket returns a network file descriptor that is ready for
// asynchronous I/O using the network poller.
func socket(ctx context.Context, net string, family, sotype, proto int, ipv6only bool, laddr, raddr sockaddr) (fd *netFD, err error) {
    s, err := sysSocket(family, sotype, proto)
    if err != nil {
        return nil, err
    }
    if err = setDefaultSockopts(s, family, sotype, ipv6only); err != nil {
        poll.CloseFunc(s)
        return nil, err
    }
    if fd, err = newFD(s, family, sotype, net); err != nil {
        poll.CloseFunc(s)
        return nil, err
    }

    // This function makes a network file descriptor for the
    // following applications:
    //
    // - An endpoint holder that opens a passive stream
    //   connection, known as a stream listener
    //
    // - An endpoint holder that opens a destination-unspecific
    //   datagram connection, known as a datagram listener
    //
    // - An endpoint holder that opens an active stream or a
    //   destination-specific datagram connection, known as a
    //   dialer
    //
    // - An endpoint holder that opens the other connection, such
    //   as talking to the protocol stack inside the kernel
    //
    // For stream and datagram listeners, they will only require
    // named sockets, so we can assume that it's just a request
    // from stream or datagram listeners when laddr is not nil but
    // raddr is nil. Otherwise we assume it's just for dialers or
    // the other connection holders.

    if laddr != nil && raddr == nil {
        switch sotype {
        case syscall.SOCK_STREAM, syscall.SOCK_SEQPACKET:
            if err := fd.listenStream(laddr, listenerBacklog); err != nil {
                fd.Close()
                return nil, err
            }
            return fd, nil
        case syscall.SOCK_DGRAM:
            if err := fd.listenDatagram(laddr); err != nil {
                fd.Close()
                return nil, err
            }
            return fd, nil
        }
    }
    if err := fd.dial(ctx, laddr, raddr); err != nil {
        fd.Close()
        return nil, err
    }
    return fd, nil
}

这段代码隐含了大量细节,首先看最上面函数的注释,返回值是一个使用了network poller异步I/O的文件描述符。前面三个 if 里,先创建了一个 socket,然后设置基本参数,再 new 一个文件描述符,其中包含了大量的系统调用和底层细节,这里先跳过。我想说的在下面。

socket 这个函数可以为一下几种应用创建一个文件描述符:

  • 一个打开了 被动的、流式的 连接的终端,通常叫stream listener
  • 一个打开了 没有具体目的地的、数据报格式的 连接的终端,通常叫datagram listener
  • 一个打开了 主动的、有明确目的地的、数据报格式的 连接的终端,通常叫dialer
  • 一个打开了其他连接的终端,比如与内核中的协议栈通信

通常可以认为当 laddr不为空但raddr为空时的 request 是来自stream or datagram listeners。否则就是来自 dialers 或者其他系统连接。

所以一个dialer和listener的区别就是 laddr, 也就是dialer在一定情况下可以当做listener,到这里就可以解释之前tcp的simultaneous connection同时连接了。

接下来调用了fd的dial函数,这里才真正通过socket开始发送连接请求。

(待续)

你可能感兴趣的:(Go net/dial.go 阅读笔记(二))