Go net/dial.go 閱讀筆記(二)

Go net/dial.go 閱讀筆記(二)

上一篇文章 我們大致分析了dial.go中的代碼,起主要的功能就是為真正發(fā)起連接做一些準(zhǔn)備,起到了應(yīng)用層的作用(DNS解析等)。但是一個連接完整的連接還需要更深層次的網(wǎng)絡(luò)協(xié)議來完成協(xié)作,所以我們接著上篇來分析,由于篇(懶)幅原因,只將dialTcp作為傳輸層的例子。。。話不多說,上代碼:

func dialTCP(ctx context.Context, net string, laddr, raddr *TCPAddr) (*TCPConn, error) {
    if testHookDialTCP != nil { //testHookDialTCP 是語言開發(fā)者為了測試留的鉤子函數(shù),不用管
        return testHookDialTCP(ctx, net, laddr, raddr)
    }
    return doDialTCP(ctx, net, laddr, raddr)
}

注意現(xiàn)在所在文件是在tcpsock_posix.go 這部分是傳輸層的內(nèi)容了。

來看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
}

參數(shù)里的ctx自然不言而喻了,是為了控制請求超時取消請求釋放資源的;laddr是 local address , raddr是指 remote address;返回值這里會得到 TCPConn。代碼不長,就是調(diào)用了 internetSocket得到一個文件描述符,并用其新建一個conn返回。但這里我想多說幾句,因為不難發(fā)現(xiàn), internetSocket可能會被調(diào)用多次,為什么呢?

首先我們需要知道 Tcp 有一個極少使用的機制,叫simultaneous connection(同時連接)。正常的連接是:A主機 dial B主機,B主機 listen。 而同時連接則是: A 向 B dial 同時 B 向 A dial,那么 A 和 B 都不需要監(jiān)聽。

我們知道,當(dāng) 傳入 dial 函數(shù)的參數(shù)laddr==raddr時,內(nèi)核會拒絕dial。但如果傳入的laddr為nil,kernel 會自動選擇一個本機端口,這時候有可能會使得新的laddr==raddr,這個時候,kernel不會拒絕dial,并且這個dial會成功,原因是就simultaneous connection,這可能是kernel的bug。所以會判斷是否是 selfConnect或者spuriousENOTAVAIL(spurious error not avail)來判斷上一次調(diào)用internetSocket返回的 err 類型,在特定的情況下重新嘗試internetSocket.關(guān)于這個問題的討論參見這里。

好了,我們接下來看看internetSocket,該函數(shù)在ipsock_posix.go文件,到了網(wǎng)絡(luò)層的范圍了。

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 是零地址,把它轉(zhuǎn)化成當(dāng)前系統(tǒng)對應(yīng)的零地址格式(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的系統(tǒng)調(diào)用時用的)首先判斷了運行系統(tǒng)的類型,favoriteAddrFamily返回了當(dāng)前 dial 最合適的地址族,主要是判斷應(yīng)該用ipv4還是ipv6或者都用,其返回值 family 有兩種可能值:AF_INETAF_INET6,都是int類型,感興趣的朋友可以參見這里

讓我們接著關(guān)注socket,該函數(shù)在sock_posix.go文件,意味著接下來將是更加底層的系統(tǒng)調(diào)用了。

// 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
}

這段代碼隱含了大量細(xì)節(jié),首先看最上面函數(shù)的注釋,返回值是一個使用了network poller異步I/O的文件描述符。前面三個 if 里,先創(chuàng)建了一個 socket,然后設(shè)置基本參數(shù),再 new 一個文件描述符,其中包含了大量的系統(tǒng)調(diào)用和底層細(xì)節(jié),這里先跳過。我想說的在下面。

socket 這個函數(shù)可以為一下幾種應(yīng)用創(chuàng)建一個文件描述符:

  • 一個打開了 被動的、流式的 連接的終端,通常叫stream listener
  • 一個打開了 沒有具體目的地的、數(shù)據(jù)報格式的 連接的終端,通常叫datagram listener
  • 一個打開了 主動的、有明確目的地的、數(shù)據(jù)報格式的 連接的終端,通常叫dialer
  • 一個打開了其他連接的終端,比如與內(nèi)核中的協(xié)議棧通信

通??梢哉J(rèn)為當(dāng) laddr不為空但raddr為空時的 request 是來自stream or datagram listeners。否則就是來自 dialers 或者其他系統(tǒng)連接。

所以一個dialer和listener的區(qū)別就是 laddr, 也就是dialer在一定情況下可以當(dāng)做listener,到這里就可以解釋之前tcp的simultaneous connection同時連接了。

接下來調(diào)用了fd的dial函數(shù),這里才真正通過socket開始發(fā)送連接請求。

(待續(xù))

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容