天天看點

Go語言核心36講(Go語言實戰與應用二十五)--學習筆記

我們在上一篇文章中簡單地讨論了網絡程式設計和 socket,并由此提及了 Go 語言标準庫中的syscall代碼包和net代碼包。

我還重點講述了net.Dial函數和syscall.Socket函數的參數含義。前者間接地調用了後者,是以正确了解後者,會對用好前者有很大裨益。

之後,我們把視線轉移到了net.DialTimeout函數以及它對操作逾時的處理上,這又涉及了net.Dialer類型。實際上,這個類型正是net包中這兩個“撥号”函數的底層實作。

我們像上一篇文章的示例代碼那樣用net.Dial或net.DialTimeout函數來通路基于 HTTP 協定的網絡服務是完全沒有問題的。HTTP 協定是基于 TCP/IP 協定棧的,并且它也是一個面向普通文本的協定。

原則上,我們使用任何一個文本編輯器,都可以輕易地寫出一個完整的 HTTP 請求封包。隻要你搞清楚了請求封包的頭部(header)和主體(body)應該包含的内容,這樣做就會很容易。是以,在這種情況下,即便直接使用net.Dial函數,你應該也不會感覺到困難。

不過,不困難并不意味着很友善。如果我們隻是通路基于 HTTP 協定的網絡服務的話,那麼使用net/http代碼包中的程式實體來做,顯然會更加便捷。

其中,最便捷的是使用http.Get函數。我們在調用它的時候隻需要傳給它一個 URL 就可以了,比如像下面這樣:

http.Get函數會傳回兩個結果值。第一個結果值的類型是*http.Response,它是網絡服務給我們傳回來的響應内容的結構化表示。

第二個結果值是error類型的,它代表了在建立和發送 HTTP 請求,以及接收和解析 HTTP 響應的過程中可能發生的錯誤。

http.Get函數會在内部使用預設的 HTTP 用戶端,并且調用它的Get方法以完成功能。這個預設的 HTTP 用戶端是由net/http包中的公開變量DefaultClient代表的,其類型是*http.Client。它的基本類型也是可以被拿來使用的,甚至它還是開箱即用的。下面的這兩行代碼:

與前面的這一行代碼

是等價的。

http.Client是一個結構體類型,并且它包含的字段都是公開的。之是以該類型的零值仍然可用,是因為它的這些字段要麼存在着相應的預設值,要麼其零值直接就可以使用,且代表着特定的含義。

現在,我問你一個問題,是關于這個類型中的最重要的一個字段的。

今天的問題是:http.Client類型中的Transport字段代表着什麼?

這道題的典型回答是這樣的。

http.Client類型中的Transport字段代表着:向網絡服務發送 HTTP 請求,并從網絡服務接收 HTTP 響應的操作過程。也就是說,該字段的方法RoundTrip應該實作單次 HTTP 事務(或者說基于 HTTP 協定的單次互動)需要的所有步驟。

這個字段是http.RoundTripper接口類型的,它有一個由http.DefaultTransport變量代表的預設值(以下簡稱DefaultTransport)。當我們在初始化一個http.Client類型的值(以下簡稱Client值)的時候,如果沒有顯式地為該字段指派,那麼這個Client值就會直接使用DefaultTransport。

順便說一下,http.Client類型的Timeout字段,代表的正是前面所說的單次 HTTP 事務的逾時時間,它是time.Duration類型的。它的零值是可用的,用于表示沒有設定逾時時間。

下面,我們再通過該字段的預設值DefaultTransport,來深入地了解一下這個Transport字段。

DefaultTransport的實際類型是*http.Transport,後者即為http.RoundTripper接口的預設實作。這個類型是可以被複用的,也推薦被複用,同時,它也是并發安全的。正因為如此,http.Client類型也擁有着同樣的特質。

http.Transport類型,會在内部使用一個net.Dialer類型的值(以下簡稱Dialer值),并且,它會把該值的Timeout字段的值,設定為30秒。

也就是說,這個Dialer值如果在 30 秒内還沒有建立好網絡連接配接,那麼就會被判定為操作逾時。在DefaultTransport的值被初始化的時候,這樣的Dialer值的DialContext方法會被賦給前者的DialContext字段。

http.Transport類型還包含了很多其他的字段,其中有一些字段是關于操作逾時的。

IdleConnTimeout:含義是空閑的連接配接在多久之後就應該被關閉。

DefaultTransport會把該字段的值設定為90秒。如果該值為0,那麼就表示不關閉空閑的連接配接。注意,這樣很可能會造成資源的洩露。

ResponseHeaderTimeout:含義是,從用戶端把請求完全遞交給作業系統到從作業系統那裡接收到響應封包頭的最大時長。DefaultTransport并沒有設定該字段的值。

ExpectContinueTimeout:含義是,在用戶端遞交了請求封包頭之後,等待接收第一個響應封包頭的最長時間。在用戶端想要使用 HTTP 的“POST”方法把一個很大的封包體發送給服務端的時候,它可以先通過發送一個包含了“Expect: 100-continue”的請求封包頭,來詢問服務端是否願意接收這個大封包體。這個字段就是用于設定在這種情況下的逾時時間的。注意,如果該字段的值不大于0,那麼無論多大的請- 求封包體都将會被立即發送出去。這樣可能會造成網絡資源的浪費。DefaultTransport把該字段的值設定為了1秒。

TLSHandshakeTimeout:TLS 是 Transport Layer Security 的縮寫,可以被翻譯為傳輸層安全。這個字段代表了基于 TLS 協定的連接配接在被建立時的握手階段的逾時時間。若該值為0,則表示對這個時間不設限。DefaultTransport把該字段的值設定為了10秒。

此外,還有一些與IdleConnTimeout相關的字段值得我們關注,即:MaxIdleConns、MaxIdleConnsPerHost以及MaxConnsPerHost。

無論目前的http.Transport類型的值(以下簡稱Transport值)通路了多少個網絡服務,MaxIdleConns字段都隻會對空閑連接配接的總數做出限定。而MaxIdleConnsPerHost字段限定的則是,該Transport值通路的每一個網絡服務的最大空閑連接配接數。

每一個網絡服務都會有自己的網絡位址,可能會使用不同的網絡協定,對于一些 HTTP 請求也可能會用到代理。Transport值正是通過這三個方面的具體情況,來鑒别不同的網絡服務的。

MaxIdleConnsPerHost字段的預設值,由http.DefaultMaxIdleConnsPerHost變量代表,值為2。也就是說,在預設情況下,對于某一個Transport值通路的每一個網絡服務,它的空閑連接配接數都最多隻能有兩個。

與MaxIdleConnsPerHost字段的含義相似的,是MaxConnsPerHost字段。不過,後者限制的是,針對某一個Transport值通路的每一個網絡服務的最大連接配接數,不論這些連接配接是否是空閑的。并且,該字段沒有相應的預設值,它的零值表示不對此設限。

DefaultTransport并沒有顯式地為MaxIdleConnsPerHost和MaxConnsPerHost這兩個字段指派,但是它卻把MaxIdleConns字段的值設定為了100。

換句話說,在預設情況下,空閑連接配接的總數最大為100,而針對每個網絡服務的最大空閑連接配接數為2。注意,上述兩個與空閑連接配接數有關的字段的值應該是關聯的,是以,你有時候需要根據實際情況來定制它們。

當然了,這首先需要我們在初始化Client值的時候,定制它的Transport字段的值。定制這個值的方式,可以參看DefaultTransport變量的聲明。

最後,我簡單說一下為什麼會出現空閑的連接配接。我們都知道,HTTP 協定有一個請求封包頭叫做“Connection”。在 HTTP 協定的 1.1 版本中,這個封包頭的值預設是“keep-alive”。

在這種情況下的網絡連接配接都是持久連接配接,它們會在目前的 HTTP 事務完成後仍然保持着連通性,是以是可以被複用的。

既然連接配接可以被複用,那麼就會有兩種可能。一種可能是,針對于同一個網絡服務,有新的 HTTP 請求被遞交,該連接配接被再次使用。另一種可能是,不再有對該網絡服務的 HTTP 請求,該連接配接被閑置。

顯然,後一種可能就産生了空閑的連接配接。另外,如果配置設定給某一個網絡服務的連接配接過多的話,也可能會導緻空閑連接配接的産生,因為每一個新遞交的 HTTP 請求,都隻會征用一個空閑的連接配接。是以,為空閑連接配接設定限制,在大多數情況下都是很有必要的,也是需要斟酌的。

如果我們想徹底地杜絕空閑連接配接的産生,那麼可以在初始化Transport值的時候把它的DisableKeepAlives字段的值設定為true。這時,HTTP 請求的“Connection”封包頭的值就會被設定為“close”。這會告訴網絡服務,這個網絡連接配接不必保持,目前的 HTTP 事務完成後就可以斷開它了。

如此一來,每當一個 HTTP 請求被遞交時,就都會産生一個新的網絡連接配接。這樣做會明顯地加重網絡服務以及用戶端的負載,并會讓每個 HTTP 事務都耗費更多的時間。是以,在一般情況下,我們都不要去設定這個DisableKeepAlives字段。

順便說一句,在net.Dialer類型中,也有一個看起來很相似的字段KeepAlive。不過,它與前面所說的 HTTP 持久連接配接并不是一個概念,KeepAlive是直接作用在底層的 socket 上的。

它的背後是一種針對網絡連接配接(更确切地說,是 TCP 連接配接)的存活探測機制。它的值用于表示每間隔多長時間發送一次探測包。當該值不大于0時,則表示不開啟這種機制。DefaultTransport會把這個字段的值設定為30秒。

好了,以上這些内容闡述的就是,http.Client類型中的Transport字段的含義,以及它的值的定制方式。這涉及了http.RoundTripper接口、http.DefaultTransport變量、http.Transport類型,以及net.Dialer類型。

http.Server類型與http.Client是相對應的。http.Server代表的是基于 HTTP 協定的服務端,或者說網絡服務。

http.Server類型的ListenAndServe方法的功能是:監聽一個基于 TCP 協定的網絡位址,并對接收到的 HTTP 請求進行處理。這個方法會預設開啟針對網絡連接配接的存活探測機制,以保證連接配接是持久的。同時,該方法會一直執行,直到有嚴重的錯誤發生或者被外界關掉。當被外界關掉時,它會傳回一個由http.ErrServerClosed變量代表的錯誤值。

對于本問題,典型回答可以像下面這樣。

這個ListenAndServe方法主要會做下面這幾件事情。

1、檢查目前的http.Server類型的值(以下簡稱目前值)的Addr字段。該字段的值代表了目前的網絡服務需要使用的網絡位址,即:IP 位址和端口号. 如果這個字段的值為空字元串,那麼就用":http"代替。也就是說,使用任何可以代表本機的域名和 IP 位址,并且端口号為80。

2、通過調用net.Listen函數在已确定的網絡位址上啟動基于 TCP 協定的監聽。

3、檢查net.Listen函數傳回的錯誤值。如果該錯誤值不為nil,那麼就直接傳回該值。否則,通過調用目前值的Serve方法準備接受和處理将要到來的 HTTP 請求。

可以從目前問題直接衍生出的問題一般有兩個,一個是“net.Listen函數都做了哪些事情”,另一個是“http.Server類型的Serve方法是怎樣接受和處理 HTTP 請求的”。

對于第一個直接的衍生問題,如果概括地說,回答可以是:

1、解析參數值中包含的網絡位址隐含的 IP 位址和端口号;

2、根據給定的網絡協定,确定監聽的方法,并開始進行監聽。

從這裡的第二個步驟出發,我們還可以繼續提出一些間接的衍生問題。這往往會涉及net.socket函數以及相關的 socket 知識。

對于第二個直接的衍生問題,我們可以這樣回答:

在一個for循環中,網絡監聽器的Accept方法會被不斷地調用,該方法會傳回兩個結果值;第一個結果值是net.Conn類型的,它會代表包含了新到來的 HTTP 請求的網絡連接配接;第二個結果值是代表了可能發生的錯誤的error類型值。

如果這個錯誤值不為nil,除非它代表了一個暫時性的錯誤,否則循環都會被終止。如果是暫時性的錯誤,那麼循環的下一次疊代将會在一段時間之後開始執行。

如果這裡的Accept方法沒有傳回非nil的錯誤值,那麼這裡的程式将會先把它的第一個結果值包裝成一個*http.conn類型的值(以下簡稱conn值),然後通過在新的 goroutine 中調用這個conn值的serve方法,來對目前的 HTTP 請求進行處理。

這個處理的細節還是很多的,是以我們依然可以找出不少的間接的衍生問題。比如,這個conn值的狀态有幾種,分别代表着處理的哪個階段?又比如,處理過程中會用到哪些讀取器和寫入器,它們的作用分别是什麼?再比如,這裡的程式是怎樣調用我們自定義的處理函數的,等等。

諸如此類的問題很多,我就不在這裡一一列舉和說明了。你隻需要記住一句話:“源碼之前了無秘密”。上面這些問題的答案都可以在 Go 語言标準庫的源碼中找到。如果你想對本問題進行深入的探索,那麼一定要去看net/http代碼包的源碼。

今天,我們主要講的是基于 HTTP 協定的網絡服務,側重點仍然在用戶端。

我們在讨論了http.Get函數和http.Client類型的簡單使用方式之後,把目光聚焦在了後者的Transport字段。

這個字段代表着單次 HTTP 事務的操作過程。它是http.RoundTripper接口類型的。它的預設值由http.DefaultTransport變量代表,其實際類型是*http.Transport。

http.Transport包含的字段非常多。我們先講了DefaultTransport中的DialContext字段會被賦予什麼樣的值,又詳細說明了一些關于操作逾時的字段。

比如IdleConnTimeout和ExpectContinueTimeout,以及相關的MaxIdleConns和MaxIdleConnsPerHost等等。之後,我又簡單地解釋了出現空閑連接配接的原因,以及相關的定制方式。

最後,作為擴充,我還為你簡要地梳理了http.Server類型的ListenAndServe方法,執行的主要流程。不過,由于篇幅原因,我沒有做深入講述。但是,這并不意味着沒有必要深入下去。相反,這個方法很重要,值得我們認真地去探索一番。

在你需要或者有興趣的時候,我希望你能去好好地看一看net/http包中的相關源碼。一切秘密都在其中。

我今天留給你的思考題比較簡單,即:怎樣優雅地停止基于 HTTP 協定的網絡服務程式?

https://github.com/MingsonZheng/go-core-demo

Go語言核心36講(Go語言實戰與應用二十五)--學習筆記

本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協定進行許可。

歡迎轉載、使用、重新釋出,但務必保留文章署名 鄭子銘 (包含連結: http://www.cnblogs.com/MingsonZheng/ ),不得用于商業目的,基于本文修改後的作品務必以相同的許可釋出。