go源碼解析之TCP連接(一)——Listen

tcp連接的一生系列基于go源碼1.16.5

端口是如何監(jiān)聽的

首先奉上net文檔中第一個映入眼簾的example

ln, err := net.Listen("tcp", ":8080")
if err != nil {
        // handle error
}
for {
        conn, err := ln.Accept()
        if err != nil {
                // handle error
        }
        go handleConnection(conn)
}

下面我們通過逐行跟蹤源碼,來看開啟監(jiān)聽的過程:

1. net.Listen

src\net\dial.go

func Listen(network, address string) (Listener, error) {
    var lc ListenConfig
    return lc.Listen(context.Background(), network, address)
}

這個監(jiān)聽方法,其中network可以是tcp、tcp4、tcp6、unix、unixpacket,我們通常傳入tcp即代表監(jiān)聽tcp連接,包括ipv4和ipv6,其他類型不在我們的介紹范圍,包括udp本文也不討論。address是監(jiān)聽的地址,ip:port格式,如果不指定port,將由系統(tǒng)自動分配一個端口。

ListenConfig的struct體如下:
src\net\dial.go

type ListenConfig struct {
    Control func(network, address string, c syscall.RawConn) error
    KeepAlive time.Duration
}

其中Control是一個方法變量,根據(jù)注釋,這個方法會在連接創(chuàng)建之后并將連接綁定到操作系統(tǒng)之前調(diào)用,相當于是提供給用戶層的一個連接創(chuàng)建的回調(diào)方法,至于它的用處和調(diào)用時機,隨著后續(xù)更深層的代碼分析再做進一步介紹。
KeepAlive,應(yīng)該和內(nèi)核參數(shù)/proc/sys/net/ipv4/tcp_keepalive_time、tcp_keepalive_intvl、tcp_keepalive_probes是相同的作用,但是根據(jù)注釋說明,0是開啟,負數(shù)是關(guān)閉,沒有說明正數(shù)的作用。后續(xù)用到再研究。

2.ListenConfig的Listen方法

src\net\dial.go

func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (Listener, error) {
    addrs, err := DefaultResolver.resolveAddrList(ctx, "listen", network, address, nil)
    ...
        sl := &sysListener{
        ListenConfig: *lc,
        network:      network,
        address:      address,
    }
    var l Listener
    la := addrs.first(isIPv4)
    switch la := la.(type) {
    case *TCPAddr:
        l, err = sl.listenTCP(ctx, la)
        ...
    }
    ...
    return l, nil
}

其中...代表省略的一些細節(jié)處理或者是無關(guān)分支,后續(xù)也都會以這種方式貼代碼。

ListenConfig的Listen方法同樣是傳入了network和address,ctx是上層傳入的context.Background()。返回值是Listener類型和error,其中的Listener其實是一個接口類型,具體接口定義如下:
src\net\net.go

type Listener interface {
    Accept() (Conn, error) //等待并返回建立成功的連接
    Close() error //關(guān)閉監(jiān)聽
    Addr() Addr //監(jiān)聽地址
}

我們再看ListenConfig的Listen方法的邏輯,第一行對傳入的地址進行了解析,轉(zhuǎn)換成了下層可用的地址格式。緊接著生成了一個sysListener的變量,sysListener的作用很簡單,它的存在就是為了構(gòu)造各種類型的實現(xiàn)了Listener接口的監(jiān)聽器,因此它的所有的方法都是listenXXX,XXX則代表網(wǎng)絡(luò)協(xié)議類型,例如這里的listenTCP,還有l(wèi)istenUDP等等。

sysListener.listenTCP

繼續(xù)看代碼,下面的switch case我們不管,直接看case是TCPAddr的情況,調(diào)用了sysListener的listenTCP方法,方法中代碼如下:

src\net\tcpsock_posix.go

func (sl *sysListener) listenTCP(ctx context.Context, laddr *TCPAddr) (*TCPListener, error) {
    fd, err := internetSocket(ctx, sl.network, laddr, nil, syscall.SOCK_STREAM, 0, "listen", sl.ListenConfig.Control)
    if err != nil {
        return nil, err
    }
    return &TCPListener{fd: fd, lc: sl.ListenConfig}, nil
}

可見sysListener構(gòu)造了一個TCPListener并返回,看一下internetSocket,internetSocket的作用是創(chuàng)建一個socket,TCPListener將使用這個socket來監(jiān)聽端口接收連接,下面看具體代碼:

src\net\ipsock_posix.go

func internetSocket(ctx context.Context, net string, laddr, raddr sockaddr, sotype, proto int, mode string, ctrlFn func(string, string, syscall.RawConn) error) (fd *netFD, err error) {
    if (runtime.GOOS == "aix" || runtime.GOOS == "windows" || runtime.GOOS == "openbsd") && mode == "dial" && raddr.isWildcard() {
        raddr = raddr.toLocal(net)
    }
    family, ipv6only := favoriteAddrFamily(net, laddr, raddr, mode)
    return socket(ctx, net, family, sotype, proto, ipv6only, laddr, raddr, ctrlFn)
}

這個方法的參數(shù)可真長,我們對照方法調(diào)用一個個看一下:

  1. 參數(shù)1,ctx不說了
  2. 參數(shù)2,net,是我們最初傳入的network,即網(wǎng)絡(luò)協(xié)議類型,tcp、udp等
  3. 參數(shù)3,laddr是local address的縮寫,即本地地址。我們構(gòu)建Listener需要傳入本地地址
  4. 參數(shù)4,raddr是remoe address的縮寫,即遠端地址。構(gòu)建Listener不需要遠端地址,當連接到遠端時需要raddr
  5. 參數(shù)5,sotype,傳入了syscall.SOCK_STREAM即代表進行tcp監(jiān)聽,與之對應(yīng)的是SOCK_DGRAM
  6. 參數(shù)6,proto,默認0。
  7. 參數(shù)7,mode,傳入了listen,代表要建立的socket是監(jiān)聽socket
  8. 參數(shù)8,ctrlFn,這里就是上面ListenConfig的Controller屬性

方法的第一部分還是地址轉(zhuǎn)換,第二部分的favoriteAddrFamily方法則是返回了支持的協(xié)議簇(AF_INET或者AF_INET6,代表了ipv4和ipv6),第三部分則是socket方法的調(diào)用,它的入?yún)⒑蚷nternetSocket的基本一致,返回值是*netFD,而netFD則是對系統(tǒng)文件描述符(socket也有一個唯一的文件描述符fd與之對應(yīng))的包裝,下面我們看下socket方法中是怎么創(chuàng)建netFD的:

socket

src\net\sock_posix.go

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) {
    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
    }

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

我們從上到下介紹每個方法調(diào)用的作用:

  1. sysSocket,顧名思義,它的作用是創(chuàng)建系統(tǒng)socket
  2. setDefaultSockopts,設(shè)置了socket的一些屬性,例如是否只支持ipv6
  3. newFD,對返回的系統(tǒng)fd進行了包裝,生成了本方法要返回的netFD
  4. if laddr != nil && raddr == nil,如果傳入了本地地址,沒有傳入遠端地址,則認為新的socket是用來監(jiān)聽的,調(diào)用了netFD的listenStream進行端口綁定,可以看到這里將ctrlFn(ListenConfig的Controller屬性)又一次傳入,那么ListenConfig的Controller方法屬性是在socket創(chuàng)建之后執(zhí)行的,具體在什么操作之前,還需要進一步跟代碼。
  5. fd.dial,是傳入了遠端地址的情況,則認為新的socket是用來connect的,dial進行了連接。

一個tcp的監(jiān)聽socket創(chuàng)建完成、進行了端口綁定,并將此socket的fd包裝成了netFD返回給調(diào)用者,沿著調(diào)用鏈一直向上返回到sysListener的listenTCP方法,為方便大家查看,將上面貼過的代碼再次貼到這里:

src\net\tcpsock_posix.go

func (sl *sysListener) listenTCP(ctx context.Context, laddr *TCPAddr) (*TCPListener, error) {
    fd, err := internetSocket(ctx, sl.network, laddr, nil, syscall.SOCK_STREAM, 0, "listen", sl.ListenConfig.Control)
    if err != nil {
        return nil, err
    }
    return &TCPListener{fd: fd, lc: sl.ListenConfig}, nil
}

中場小結(jié)

在繼續(xù)深入sysSocket、setDefaultSockopts、newFD、listenStream幾個方法之前,我們現(xiàn)在通過一張圖來回顧一下前面的調(diào)用過程

8424B70F-2273-4AE4-B0D3-1C6F9A19509C.png

到此為止,整個邏輯除了最下層的socket方法中略顯復(fù)雜,其他每個方法體都很小,但是調(diào)用鏈路還是比較長,我們來簡單總結(jié)下每一層的代碼設(shè)計。

  1. net.Listen是整個鏈路的入口方法,它創(chuàng)建了一個空的ListenConfig,并調(diào)用了ListenConfig的Listen方法
  2. ListenConfig,它目前擁有兩個可選配置項:Control和KeepAlive。它將被作為配置數(shù)據(jù)傳遞給下游,設(shè)計成一個struct可以避免通過傳參的方式傳遞很多配置
  3. ListenConfig.Listen方法,將上層傳入的字符串類型的address轉(zhuǎn)換成下層使用的Addr數(shù)據(jù),并通過判斷network的類型調(diào)用sysListener的不同的listen方法(listenTCP、listenUDP等)
  4. sysListener將ListenConfig、address、network作為自己的屬性,并實現(xiàn)了各種network的listen方法
  5. sysListener.listenTCP方法,調(diào)用internetSocket方法,并使用返回的netFD創(chuàng)建TCPListener
  6. internetSocket方法,是一個創(chuàng)建監(jiān)聽socket和connect socket(dial方法主動發(fā)起連接)的共用方法
  7. socket方法,是unixsock和ipsock的共用方法,它首先創(chuàng)建了socket并為socket設(shè)置默認屬性,再將返回的fd包裝成netFD,最后使用此socket綁定端口或者進行連接。
  8. 最終將TCPListener返回給net.Listen的調(diào)用者,調(diào)用者可以調(diào)用TCPListener的Accept方法開始接受連接請求,這一部分將在下一篇中介紹。

下面繼續(xù)介紹sysSocket、setDefaultSockopts、newFD、listenStream幾個方法

sysSocket

老套路,先祭出代碼:

src\net\sock_cloexec.go

func sysSocket(family, sotype, proto int) (int, error) {
    s, err := socketFunc(family, sotype|syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC, proto)

    ...
    
    return s, nil
}

中間省略部分是socketFunc報錯后的容錯處理,老版本內(nèi)核由于不支持創(chuàng)建socket時設(shè)置SOCK_NONBLOCK或者SOCK_CLOEXEC,導致創(chuàng)建失敗。省略部分進行了容錯,先創(chuàng)建socket,再進行socket屬性的設(shè)置。

在跟入socketFunc之前先介紹一下它的參數(shù):

  1. family是AF_INET或者AF_INET6,即ipv4或者ipv6
  2. sotype是SOCK_STREAM或者SOCK_DGRAM,即tcp或者udp
  3. SOCK_NONBLOCK是將socket設(shè)置為非阻塞
  4. SOCK_CLOEXEC是將socket設(shè)置為close-on-exec
  5. proto默認0

socketFunc是一個全局的方法變量,它的值如下:

src\net\hook_unix.go

var (
    ...

    // Placeholders for socket system calls.
    socketFunc        func(int, int, int) (int, error)  = syscall.Socket
    connectFunc       func(int, syscall.Sockaddr) error = syscall.Connect
    listenFunc        func(int, int) error              = syscall.Listen
    getsockoptIntFunc func(int, int, int) (int, error)  = syscall.GetsockoptInt
)

可見除了socketFunc之外,還有connectFunc、listenFunc、getsockoptIntFunc,它們都是syscall包里的方法。

繼續(xù)跟入syscall.Socket:

src\syscall\syscall_unix.go

func Socket(domain, typ, proto int) (fd int, err error) {
    if domain == AF_INET6 && SocketDisableIPv6 {
        return -1, EAFNOSUPPORT
    }
    fd, err = socket(domain, typ, proto)
    return
}

src\syscall\zsyscall_linux_amd64.go

// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT

func socket(domain int, typ int, proto int) (fd int, err error) {
    r0, _, e1 := RawSyscall(SYS_SOCKET, uintptr(domain), uintptr(typ), uintptr(proto))
    fd = int(r0)
    if e1 != 0 {
        err = errnoErr(e1)
    }
    return
}

src\syscall\zsysnum_linux_amd64.go

const {
    ...
    SYS_SOCKET                 = 41
    ...
}

src\syscall\asm_linux_amd64.s

// func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2, err uintptr)
TEXT ·RawSyscall(SB),NOSPLIT,$0-56
    MOVQ    a1+8(FP), DI
    MOVQ    a2+16(FP), SI
    MOVQ    a3+24(FP), DX
    MOVQ    trap+0(FP), AX  // syscall entry
    SYSCALL
        ...

以上4段代碼邏輯都比較簡單,就是實現(xiàn)了一個socket的系統(tǒng)調(diào)用,最后的rawSyscall是使用匯編實現(xiàn)的一段系統(tǒng)調(diào)用方法,創(chuàng)建socket的系統(tǒng)調(diào)用號是SYS_SOCKET。

setDefaultSockopts

老規(guī)矩,上代碼:

src\net\sockopt_linux.go

func setDefaultSockopts(s, family, sotype int, ipv6only bool) error {
    if family == syscall.AF_INET6 && sotype != syscall.SOCK_RAW {
        syscall.SetsockoptInt(s, syscall.IPPROTO_IPV6, syscall.IPV6_V6ONLY, boolint(ipv6only))
    }
    if (sotype == syscall.SOCK_DGRAM || sotype == syscall.SOCK_RAW) && family != syscall.AF_UNIX {
        // Allow broadcast.
        return os.NewSyscallError("setsockopt", syscall.SetsockoptInt(s, syscall.SOL_SOCKET, syscall.SO_BROADCAST, 1))
    }
    return nil
}

可見代碼在一定條件下設(shè)置了是否只允許ipv6。如果是udp的話,還將socket設(shè)置為允許廣播。
syscall.SetsockoptInt方法同syscall.Socket方法,都是syscall中的系統(tǒng)調(diào)用。

newFD

廢話不多說,上代碼:

src\net\fd_unix.go

func newFD(sysfd, family, sotype int, net string) (*netFD, error) {
    ret := &netFD{
        pfd: poll.FD{
            Sysfd:         sysfd,
            IsStream:      sotype == syscall.SOCK_STREAM,
            ZeroReadIsEOF: sotype != syscall.SOCK_DGRAM && sotype != syscall.SOCK_RAW,
        },
        family: family,
        sotype: sotype,
        net:    net,
    }
    return ret, nil
}

newFD方法將創(chuàng)建成功的系統(tǒng)fd包裝成了netFD,下面挑選幾個netFD的重要方法來了解它:

func (fd *netFD) Read(p []byte) (n int, err error)
func (fd *netFD) Write(p []byte) (nn int, err error)
func (fd *netFD) SetDeadline(t time.Time)
func (fd *netFD) SetReadDeadline(t time.Time)
func (fd *netFD) SetWriteDeadline(t time.Time)
func (fd *netFD) connect(ctx context.Context, la, ra syscall.Sockaddr) (rsa syscall.Sockaddr, ret error)
func (fd *netFD) accept() (netfd *netFD, err error)
func (fd *netFD) dial(ctx context.Context, laddr, raddr sockaddr, ctrlFn func(string, string, syscall.RawConn) error) error
func (fd *netFD) listenStream(laddr sockaddr, backlog int, ctrlFn func(string, string, syscall.RawConn) error) error
func (fd *netFD) listenDatagram(laddr sockaddr, ctrlFn func(string, string, syscall.RawConn) error) error

netFD除了具有讀寫socket的方法,還實現(xiàn)了listen、accept及dial方法。

fd.listenStream

socket創(chuàng)建成功后,進而就是進行端口綁定和監(jiān)聽,看代碼:

src\net\sock_posix.go

func (fd *netFD) listenStream(laddr sockaddr, backlog int, ctrlFn func(string, string, syscall.RawConn) error) error {
    
        ...
    
    if ctrlFn != nil {
        c, err := newRawConn(fd)
        if err != nil {
            return err
        }
        if err := ctrlFn(fd.ctrlNetwork(), laddr.String(), c); err != nil {
            return err
        }
    }
    if err = syscall.Bind(fd.pfd.Sysfd, lsa); err != nil {
        return os.NewSyscallError("bind", err)
    }
    if err = listenFunc(fd.pfd.Sysfd, backlog); err != nil {
        return os.NewSyscallError("listen", err)
    }
    
        ...
    
    return nil
}

省略去了一些初始化和地址轉(zhuǎn)換的代碼。

syscall.Bind又一個系統(tǒng)調(diào)用,注意fd.pfd.Sysfd就是我們新創(chuàng)建的socket的fd,lsa則是我們最初傳入的ip:port經(jīng)過轉(zhuǎn)換后的地址,Bind將這個地址綁定到我們創(chuàng)建的socket上。
listenFunc是一個方法變量,存儲各種操作系統(tǒng)的Listen方法:

src\net\hook_unix.go

listenFunc        func(int, int) error              = syscall.Listen

經(jīng)過Listen系統(tǒng)調(diào)用,我們的socket就被激活了,內(nèi)核將接收連接到此socket的連接請求。下一步調(diào)用accept就可以取到連接請求的socket了。

呼呼??,終于把端口綁定和監(jiān)聽的大體代碼流程捋完了??聪旅孢@張圖,本文對應(yīng)到了TCP Server的監(jiān)聽socket創(chuàng)建和bind、listen,下一章將繼續(xù)介紹accept。

socket.png

最后將開頭ListenConfig的Controller屬性的調(diào)用時機補上,netFD.listenStream方法中的ctrlFn就是這個屬性,可見它是在監(jiān)聽socket創(chuàng)建后,bind調(diào)用之前被回調(diào)的。應(yīng)該是開放給應(yīng)用層個性化設(shè)置socket的屬性的。

最最后再把backlog說一下??,在netFD.listenStream方法中的listenFunc(fd.pfd.Sysfd, backlog)這一行中的backlog參數(shù)控制著待處理連接隊列的長度,如果隊列已滿,新的連接請求將被忽略。backlog的值取自系統(tǒng)參數(shù)(linux系統(tǒng))/proc/sys/net/core/somaxconn,如果讀取失敗,默認設(shè)置為128。如果值超過backlog可以存儲的最大值(內(nèi)核版本4.1以下backlog使用uint16存儲,高版本使用uint32存儲),將被設(shè)置為可存儲的最大值。

最后編輯于
?著作權(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)容