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)用容握,相當(dāng)于是提供給用戶層的一個連接創(chuàng)建的回調(diào)方法宣脉,至于它的用處和調(diào)用時機(jī),隨著后續(xù)更深層的代碼分析再做進(jìn)一步介紹剔氏。
KeepAlive塑猖,應(yīng)該和內(nèi)核參數(shù)/proc/sys/net/ipv4/tcp_keepalive_time竹祷、tcp_keepalive_intvl、tcp_keepalive_probes是相同的作用羊苟,但是根據(jù)注釋說明塑陵,0是開啟,負(fù)數(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
}
其中...代表省略的一些細(xì)節(jié)處理或者是無關(guān)分支凉倚,后續(xù)也都會以這種方式貼代碼兼都。
ListenConfig的Listen方法同樣是傳入了network和address,ctx是上層傳入的context.Background()稽寒。返回值是Listener類型和error扮碧,其中的Listener其實(shí)是一個接口類型,具體接口定義如下:
src\net\net.go
type Listener interface {
Accept() (Conn, error) //等待并返回建立成功的連接
Close() error //關(guān)閉監(jiān)聽
Addr() Addr //監(jiān)聽地址
}
我們再看ListenConfig的Listen方法的邏輯杏糙,第一行對傳入的地址進(jìn)行了解析慎王,轉(zhuǎn)換成了下層可用的地址格式。緊接著生成了一個sysListener的變量宏侍,sysListener的作用很簡單赖淤,它的存在就是為了構(gòu)造各種類型的實(shí)現(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的縮寫斑司,即遠(yuǎn)端地址。構(gòu)建Listener不需要遠(yuǎn)端地址但汞,當(dāng)連接到遠(yuǎn)端時需要raddr
- 參數(shù)5宿刮,sotype,傳入了syscall.SOCK_STREAM即代表進(jìn)行tcp監(jiān)聽私蕾,與之對應(yīng)的是SOCK_DGRAM
- 參數(shù)6糙置,proto,默認(rèn)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進(jìn)行了包裝衬以,生成了本方法要返回的netFD
- if laddr != nil && raddr == nil缓艳,如果傳入了本地地址,沒有傳入遠(yuǎn)端地址看峻,則認(rèn)為新的socket是用來監(jiān)聽的阶淘,調(diào)用了netFD的listenStream進(jìn)行端口綁定,可以看到這里將ctrlFn(ListenConfig的Controller屬性)又一次傳入互妓,那么ListenConfig的Controller方法屬性是在socket創(chuàng)建之后執(zhí)行的溪窒,具體在什么操作之前分井,還需要進(jìn)一步跟代碼。
- fd.dial霉猛,是傳入了遠(yuǎn)端地址的情況,則認(rèn)為新的socket是用來connect的珠闰,dial進(jìn)行了連接惜浅。
一個tcp的監(jiān)聽socket創(chuàng)建完成、進(jìn)行了端口綁定伏嗜,并將此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或衡,它目前擁有兩個可選配置項(xiàng):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作為自己的屬性,并實(shí)現(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è)置默認(rèn)屬性,再將返回的fd包裝成netFD详囤,最后使用此socket綁定端口或者進(jìn)行連接财骨。
- 最終將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理澎,導(dǎo)致創(chuàng)建失敗逞力。省略部分進(jìn)行了容錯,先創(chuàng)建socket糠爬,再進(jìn)行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默認(rèn)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段代碼邏輯都比較簡單寻仗,就是實(shí)現(xiàn)了一個socket的系統(tǒng)調(diào)用,最后的rawSyscall是使用匯編實(shí)現(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的方法串稀,還實(shí)現(xiàn)了listen、accept及dial方法狮杨。
fd.listenStream
socket創(chuàng)建成功后母截,進(jìn)而就是進(jìn)行端口綁定和監(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)用時機(jī)補(bǔ)上,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ù)控制著待處理連接隊(duì)列的長度,如果隊(duì)列已滿火架,新的連接請求將被忽略鉴象。backlog的值取自系統(tǒng)參數(shù)(linux系統(tǒng))/proc/sys/net/core/somaxconn
,如果讀取失敗何鸡,默認(rèn)設(shè)置為128纺弊。如果值超過backlog可以存儲的最大值(內(nèi)核版本4.1以下backlog使用uint16存儲,高版本使用uint32存儲)骡男,將被設(shè)置為可存儲的最大值淆游。