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)用一個個看一下:
- 參數(shù)1,ctx不說了
- 參數(shù)2,net,是我們最初傳入的network,即網(wǎng)絡(luò)協(xié)議類型,tcp、udp等
- 參數(shù)3,laddr是local address的縮寫,即本地地址。我們構(gòu)建Listener需要傳入本地地址
- 參數(shù)4,raddr是remoe address的縮寫,即遠端地址。構(gòu)建Listener不需要遠端地址,當連接到遠端時需要raddr
- 參數(shù)5,sotype,傳入了syscall.SOCK_STREAM即代表進行tcp監(jiān)聽,與之對應(yīng)的是SOCK_DGRAM
- 參數(shù)6,proto,默認0。
- 參數(shù)7,mode,傳入了listen,代表要建立的socket是監(jiān)聽socket
- 參數(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)用的作用:
- sysSocket,顧名思義,它的作用是創(chuàng)建系統(tǒng)socket
- setDefaultSockopts,設(shè)置了socket的一些屬性,例如是否只支持ipv6
- newFD,對返回的系統(tǒng)fd進行了包裝,生成了本方法要返回的netFD
- if laddr != nil && raddr == nil,如果傳入了本地地址,沒有傳入遠端地址,則認為新的socket是用來監(jiān)聽的,調(diào)用了netFD的listenStream進行端口綁定,可以看到這里將ctrlFn(ListenConfig的Controller屬性)又一次傳入,那么ListenConfig的Controller方法屬性是在socket創(chuàng)建之后執(zhí)行的,具體在什么操作之前,還需要進一步跟代碼。
- 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)用過程

到此為止,整個邏輯除了最下層的socket方法中略顯復(fù)雜,其他每個方法體都很小,但是調(diào)用鏈路還是比較長,我們來簡單總結(jié)下每一層的代碼設(shè)計。
- net.Listen是整個鏈路的入口方法,它創(chuàng)建了一個空的ListenConfig,并調(diào)用了ListenConfig的Listen方法
- ListenConfig,它目前擁有兩個可選配置項:Control和KeepAlive。它將被作為配置數(shù)據(jù)傳遞給下游,設(shè)計成一個struct可以避免通過傳參的方式傳遞很多配置
- ListenConfig.Listen方法,將上層傳入的字符串類型的address轉(zhuǎn)換成下層使用的Addr數(shù)據(jù),并通過判斷network的類型調(diào)用sysListener的不同的listen方法(listenTCP、listenUDP等)
- sysListener將ListenConfig、address、network作為自己的屬性,并實現(xiàn)了各種network的listen方法
- sysListener.listenTCP方法,調(diào)用internetSocket方法,并使用返回的netFD創(chuàng)建TCPListener
- internetSocket方法,是一個創(chuàng)建監(jiān)聽socket和connect socket(dial方法主動發(fā)起連接)的共用方法
- socket方法,是unixsock和ipsock的共用方法,它首先創(chuàng)建了socket并為socket設(shè)置默認屬性,再將返回的fd包裝成netFD,最后使用此socket綁定端口或者進行連接。
- 最終將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ù):
- family是AF_INET或者AF_INET6,即ipv4或者ipv6
- sotype是SOCK_STREAM或者SOCK_DGRAM,即tcp或者udp
- SOCK_NONBLOCK是將socket設(shè)置為非阻塞
- SOCK_CLOEXEC是將socket設(shè)置為close-on-exec
- 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。

最后將開頭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è)置為可存儲的最大值。