美文网首页Go常用库Golang与区块链
Go net/dial.go 阅读笔记(二)

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

作者: 小小小超子 | 来源:发表于2018-03-05 16:16 被阅读641次

    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 阅读笔记(二)

        本文链接:https://www.haomeiwen.com/subject/fuphfftx.html