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_INET和AF_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ù))