Go36-47-基於HTTP協議的網路服務(net/http)
基於HTTP協議的網路服務
HTTP協議是基於TCP/IP協議棧的,並且是一個面向普通文字的協議。原則上,使用任何一個文字編輯器,都可以寫出一個完整的HTTP請求報文。只要搞清楚了請求報文的頭部(header、請求頭 )和主體(body、請求體 )應該包含的內容。
如果只是訪問基於HTTP協議的網路服務,那麼使用net/http包中的程式實體會非常方便。
http.Get函式
呼叫http.Get函式,只需要傳遞給它一個URL即可:
package main import ( "fmt" "net/http" "os" ) func main() { resp, err := http.Get("http://baidu.com") if err != nil { fmt.Fprintf(os.Stderr, "request sending error: %v\n", err) return } defer resp.Body.Close() line := resp.Proto + " " + resp.Status fmt.Println("返回的第一行的內容:", line) }
http.Get函式會返回兩個結果:
- (resp *Response): 網路服務返回的響應內容的結構化表示
- (err error): 建立和傳送HTTP請求,以及接收和解析HTTP響應的過程中可能發生的錯誤
http.Get函式會在內部使用預設的HTTP客戶端,並且呼叫它的Get方法來完成功能。這個預設的HTTP客戶端就是net/http包中的公開變數DefaultClient,原始碼中是這樣的:
// 原始碼中提供的預設的客戶端 var DefaultClient = &Client{} // 使用預設的客戶端呼叫Get方法 func Get(url string) () { return DefaultClient.Get(url) }
所以下面的這兩行程式碼:
var httpClient http.Client resp, err := httpClient.Get(utl)
與示例中的這一行程式碼:
resp, err := http.Get(url)
是等價的。這裡只是不使用DefaultClient而是自己建立了一個客戶端。
http.Client型別
http.Client是一個結構體,並且它包含的欄位都是公開的:
type Client struct { Transport RoundTripper CheckRedirect func(req *Request, via []*Request) error Jar CookieJar Timeout time.Duration }
該型別是開箱即用的,因為它的所有欄位,要麼存在相應的預設值,要麼其零值直接就可以使用,並且代表著特定的含義。
Transport欄位
主要看下Transport欄位,該欄位向網路服務傳送HTTP請求,並從網路服務接收HTTP響應。該欄位的方法RoundTrip應該實現單次HTTP事務(或者說基於HTTP協議的單次互動)需要的所有步驟。這個欄位是一個介面:
type RoundTripper interface { RoundTrip(*Request) (*Response, error) }
並且該欄位有一個由http.DefaultTransport變數的預設值:
func (c *Client) transport() RoundTripper { if c.Transport != nil { return c.Transport } return DefaultTransport }
在初始化http.Client型別的時候,如果沒有顯式的為該欄位賦值,這個Client欄位就會直接使用DefaultTransport。
Timeout欄位
該欄位是單次HTTP事務的超時時間,它是time.Duration型別。它的零值是可用的,用於表示沒有設定超時時間。
http.Transport型別
http.Transport型別是一個結構體,該型別包含的欄位很多。這裡通過http.Client結構體中的Transport欄位的預設值DefaultTransport,來深入瞭解一下。DefaultTransport是一個*http.Transport的結構體,做了一些預設的設定:
var DefaultTransport RoundTripper = &Transport{ Proxy: ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout:30 * time.Second, KeepAlive: 30 * time.Second, DualStack: true, }).DialContext, MaxIdleConns:100, IdleConnTimeout:90 * time.Second, TLSHandshakeTimeout:10 * time.Second, ExpectContinueTimeout: 1 * time.Second, }
這裡Transport結構體的指標就是就是RoundTripper介面的預設實現:
func (t *Transport) RoundTrip(req *Request) (*Response, error) { return t.roundTrip(req) }
這個型別是可以被複用的,並且也推薦被複用。同時它也是併發安全的。所以http.Client型別也是一樣,推薦複用,並且併發安全。
看上面的預設設定,http.Transport型別,內部的DialContext欄位會使用net.Dialer型別的值,並且把Timeout設定為30秒。仔細看,該值是一個方法,這裡把Dialer值的DialContext方法賦值給了DefaultTransport裡的同名欄位,並且已經設定好了呼叫該方法時的結構體。
操作超時相關欄位
http.Transport型別還包含了很多其他的欄位,其中有一些欄位是關於操作超時的:
- IdleConnTimeout: 空閒的連線在多久之後就應該被關閉。DefaultTransport把該值設定為90秒。如果是0,表示不關閉空閒的連線,注意,這樣很可能會造成資源的洩露。
- ResponseHeaderTimeout: 從客戶端把請求完全遞交給作業系統到從作業系統那裡接收到響應報文的最長時長。DefaultTransport沒有設定該欄位的值。
- ExpectContinueTimeout: 在客戶端遞交了請求報文頭之後,等待接收第一個響應報文頭的最長時間。在客戶端想要使用POST傳送一個很大的請求給服務端的時候,可以通過傳送一個包含了“Expect: 100-continue”的請求頭,來詢問服務端是否願意接收這個大請求體。這個欄位就是用於設定在這種情況下的超時時間的。如果該欄位的值不大於0,那麼就不詢問了,直接把請求體一併發出,無論多大。這樣可能會造成網路資源的浪費。DefaultTransport把該值設定為1秒。
- TLSHandshakeTimeout: 表示基於TLS協議的連線在被建立時的握手階段的超時時間。如果是0,則表示沒有超時限制。DefaultTransport把該值設定為10秒。
TLS 是 Transport Layer Security 的縮寫,可以被翻譯為傳輸層安全。
連線數限制相關欄位
此外,還有一些與IdleConnTimeout相關的欄位值也值得關注:
- MaxIdleConns
- MaxIdleConnsPerHost
- MaxConnsPerHost
MaxIdleConns無論當前訪問了多少個網路服務,MaxIdleConns欄位只會對空閒連線的總數做限定。
而MaxIdleConnsPerHost欄位限定的是,每一個網路服務的最大空閒連線數。每一個網路服務都有自己的網路地址,可能會使用不同的網路協議,對於一些HTTP請求也可能會用到代理。地址、協議、代理,通脫這三個方面的具體情況來鑑別不同的網路服務。
MaxIdleConnsPerHost是有預設值的,由常量http.DefaultMaxIdleConnsPerHost表示,值為2:
const DefaultMaxIdleConnsPerHost = 2 func (t *Transport) maxIdleConnsPerHost() int { if v := t.MaxIdleConnsPerHost; v != 0 { return v } return DefaultMaxIdleConnsPerHost }
在預設情況下,每一個網路服務,它的空閒連線數最多隻能由2個。
MaxConnsPerHostMaxConnsPerHost欄位限制針對每一個網路服務的最大連線數,不論這些連結是否是空閒的。並且,該欄位沒有相應的預設值,零值就是不做限制。
小結不限制連線數,預設也不限制每一個網路服務的連線數。要限制整體的空閒連線數以及嚴格限制對每一個網路服務的空閒連線數。
空閒的連線
簡單說明一下,為什麼會出現空閒的連線。
HTTP協議的請求頭裡有一個Connection。在HTTP協議的1.1版本中,預設值是“keep-alive”。在這種情況下的網路連線是持久連線的,它們會在當前的HTTP事務完成後仍然保持著連通性,因此是可以被複用的。
既然連線可以被複用,就會有兩種可能:
- 針對同一個網路服務,有新的HTTP請求被提交,該連線被再次使用。
- 不再對該網路服務提交HTTP請求,該連線被閒置。這樣就產生了空閒的連線。
另外,如果分配給某一個網路服務的連線過多的話,也可能會導致空閒連線的產生。因為沒一個HTTP請求只會使用一個空閒的連線。所以,在大多數情況下,都需要限制空閒連線數。
另外,請求頭的Connection還可以設定為“close”,這樣就徹徹底杜絕了空閒連線的生成。這會告訴網路服務,這個網路連線不必保持,當前的HTTP事務完成後就可以斷開它了。做法是在初始化Transport值的時候,將DisableKeepAlives欄位設定為true。
這麼做的話,每次提交HTTP請求,就會產生一個新的網路連線。這樣會明顯的加重網路服務以及客戶端的負載,並會讓每個HTTP事務都耗費更多的時間。所以預設不設定這個DisableKeepAlives欄位。
net.Dialer型別
http.Transport型別,內部的DialContext欄位會使用net.Dialer型別的值。在net.Dialer型別中,也有一個KeepAlive欄位。該欄位是直接作用在底層的socket上的。
它的背後是一種針對網路連線(更確切的是說,是TCP連線)的存活探測機制。它的值用於表示每間隔多長時間傳送一次探測包。當該值不大於0是,則表示不開啟這種機制。
DefaultTransport會把這個欄位設定為30秒。
Client示例
自定義Client和Transport使用的示例:
package main import ( "fmt" "io/ioutil" "net" "net/http" "strings" "sync" "time" ) var domains = []string{ "baidu.com", "sina.com.cn", "www.baidu.com", "www.sina.com.cn", "tieba.baidu.com", "news.baidu.com", "news.sina.com.cn", } func main() { myTransport := &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout:15 * time.Second, KeepAlive: 15 * time.Second, DualStack: true, }).DialContext, MaxConnsPerHost:2, MaxIdleConns:10, MaxIdleConnsPerHost:2, IdleConnTimeout:30 * time.Second, ResponseHeaderTimeout: 0, ExpectContinueTimeout: 1 * time.Second, TLSHandshakeTimeout:10 * time.Second, } myClient := http.Client{ Transport: myTransport, Timeout:20 * time.Second, } var wg sync.WaitGroup for _, domain := range domains { wg.Add(1) go func(domain string) { var logBuf strings.Builder var diff time.Duration defer func() { logBuf.WriteString(fmt.Sprintf("持續時間: %s\n", diff)) fmt.Println(logBuf.String()) wg.Done() }() url := "https://" + domain logBuf.WriteString(fmt.Sprintf("傳送請求: %s\n", url)) tStart := time.Now() resp, err := myClient.Get(url) diff = time.Now().Sub(tStart) if err != nil { logBuf.WriteString(fmt.Sprintf("request get error: %v\n", err)) return } defer resp.Body.Close() line := resp.Proto + " " + resp.Status logBuf.WriteString(fmt.Sprintf("response: %s\n", line)) data, err := ioutil.ReadAll(resp.Body) if err != nil { logBuf.WriteString(fmt.Sprintf("get data error: %v\n", err)) return } index1 := strings.Index(string(data), "<title>") index2 := strings.Index(string(data), "</title>") if index1 > 0 && index2 > 0 { logBuf.WriteString(fmt.Sprintf("title: %s\n", string(data)[index1+len("<title>"):index2])) } }(domain) } wg.Wait() fmt.Println("All Done") }
http.Server型別
http.Server型別與http.Client是相對應的。http.Server代表的是基於HTTP協議的服務端,或者說網路服務。
ListenAndServe方法
http.Server型別的ListenAndServe方法的功能是:監聽一個基於TCP協議的網路地址,並對接收到的HTTP請求進行處理。這個方法會預設開啟針對網路連線的存活探測機制,以保證連線是持久的。同時,該方法會一直執行,直到有嚴重的錯誤發生或者被外界關掉。當被外界關掉時,它會返回一個由http.ErrServerClosed變數代表的錯誤值。
這個ListenAndServe方法主要會做以下幾件事情:
- 檢查當前的http.Server型別的值的Addr欄位。Addr是當前的網路服務需要使用的網路地址,即:IP地址和埠號。如果這個欄位的值為空字串,那麼就用":http"代替。也就是說,使用任何可以代表本機的域名和IP地址,並且埠號為80。
- 通過呼叫net.Listen函式在已確定的網路地址上啟動基於TCP協議的監聽。
- 檢查net.Listen函式返回的錯誤值。如果該錯誤值不為nil,那麼就直接返回該錯誤值。否則,通過呼叫當前http.Server值的Serve方法準備接受和處理將要到來的HTP請求。
這裡又牽出兩個問題:
- net.Listen函式
- http.Server型別的Serve方法
net.Listen函式
net.Listen函式的作用:
- 解析引數值中包含的網路地址隱含的IP地址和埠號
- 根據給定的網路協議,確定監聽的方法,並開始進行監聽
再往下深入的話,就會涉及到net.socket函式以及相關的socket知識。就此打住。
http.Server型別的Serve方法
在一個for迴圈中,網路監聽器Accept方法會不斷地呼叫,該方法的原始碼如下:
type tcpKeepAliveListener struct { *net.TCPListener } func (ln tcpKeepAliveListener) Accept() (net.Conn, error) { tc, err := ln.AcceptTCP() if err != nil { return nil, err } tc.SetKeepAlive(true) tc.SetKeepAlivePeriod(3 * time.Minute) return tc, nil }
Accept方法會返回兩個結果值:
- net.Conn : 代表包含了新到來的HTTP請求的網路連線
- error : 代表了可能發生的錯誤的error的型別值
當錯誤值不為nil時,如果此時是一個暫時性的錯誤,那麼迴圈的下一次迭代將會在一段時間之後開始執行。否則,迴圈會被終止。
如果沒有錯誤,返回的錯誤值就是nil。那麼這裡的程式將會把它的第一個結果值包裝成一個*http.conn型別的值,然後通過在新的goroutine中呼叫這個conn值的serve方法,來對當前的HTTP請求進行處理。
上面最後說的處理的細節還是很多的:
- conn值的各種狀態,各狀態代表的處理階段
- 處理過程中會用到的讀取器和寫入器,及其作用
- 讓程式呼叫自定義的處理函式
這些都沒有一一說明,建議去看下原始碼。
Server示例
在下面的示例中,啟動了3個Server。啟動後,可以用瀏覽器訪問進行驗證:
package main import ( "fmt" "net/http" "os" "sync" ) var wg sync.WaitGroup // 一般沒有這麼用的,http.Server的Handler欄位 // 要麼是nil,就用包裡的http.DefaultServeMux // 要麼用NewServeMux()來建立一個*http.ServeMux // 我這裡按照http.Handler介面的要求實現了一個,賦值給Handler欄位 // 這個自定義的Handler不支援路由 func startServer1() { defer wg.Done() var httpServer http.Server httpServer.Addr = "127.0.0.1:8001" httpServer.Handler = http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { fmt.Println(*r) fmt.Fprint(w, "Hello World") }, ) fmt.Println("啟動服務,訪問: http://127.0.0.1:8001") if err := httpServer.ListenAndServe(); err != nil { if err == http.ErrServerClosed { fmt.Println("HTTP Server1 Closed.") } else { fmt.Fprintf(os.Stderr, "HTTP Server1 Error: %v\n", err) } } } // 這個最簡單,都是呼叫http包裡的函式。本質上還是要呼叫方法的,都會用預設的或是零值 func startServer2() { defer wg.Done() http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "Hello World\nThis is Server2") }) fmt.Println("啟動服務,訪問: http://127.0.0.1:8002") // 第二個引數傳nil,就是用包裡的http.DefaultServeMux,或者也可以自己建立一個傳給第二個引數 if err := http.ListenAndServe("127.0.0.1:8002", nil); err != nil { if err == http.ErrServerClosed { fmt.Println("HTTP Server2 Closed.") } else { fmt.Fprintf(os.Stderr, "HTTP Server2 Error: %v\n", err) } } } // 這個例子裡用到了解析Get請求的引數,並且還設定了2個路由 func startServer3() { defer wg.Done() mux := http.NewServeMux() mux.HandleFunc("/hi", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/hi" { // 這個分支應該是進不來的,因為要進入這個分支,路徑應該必須是"/hi" fmt.Println("Server3 hi 404") http.NotFound(w, r) return } name := r.FormValue("name") if name == "" { fmt.Fprint(w, "Hi!") } else { fmt.Fprintf(w, "Hi, %s!", name) } }) mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "Hello World\nThis is Server3") }) // 如果只是定義http.Server的下面2個欄位,完全可以使用http.ListenAndServe函式來啟動服務 // 這樣的用法可以對http.Server裡更多的欄位進行自定義 httpServer := http.Server{ Addr: "127.0.0.1:8003", Handler: mux, } fmt.Println("啟動服務,訪問: http://127.0.0.1:8003/hi?name=Adam") if err := httpServer.ListenAndServe(); err != nil { if err == http.ErrServerClosed { fmt.Println("HTTP Server3 Closed.") } else { fmt.Fprintf(os.Stderr, "HTTP Server3 Error: %v\n", err) } } } func main() { wg.Add(1) go startServer1() wg.Add(1) go startServer2() wg.Add(1) go startServer3() wg.Wait() }
補充-優雅的停止HTTP服務
包裡還提供了一個Shutdown方法,可以優雅的停止HTTP服務:
func (srv *Server) Shutdown(ctx context.Context) error { // 內容省略 }
我們要做的就是在需要的時候,可以呼叫該Shutdown方法。
這裡的問題是,呼叫了ListenAndServe方法之後,就進入了無限迴圈的流程。這裡最好是用一個goroutine來啟動ListenAndServe方法,在goroutine外宣告http.Server。然後在主執行緒裡等待一個訊號,比如是從通道接收值。這樣就可以在主執行緒裡呼叫這個Shutdown方法執行了。