1. 程式人生 > >Go36-47-基於HTTP協議的網絡服務(net/http)

Go36-47-基於HTTP協議的網絡服務(net/http)

使用 沒有 turn 示例 cep cookie 解析 context 針對

基於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
而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個。

MaxConnsPerHost
MaxConnsPerHost字段限制針對每一個網絡服務的最大連接數,不論這些鏈接是否是空閑的。並且,該字段沒有相應的缺省值,零值就是不做限制。

小結
不限制連接數,默認也不限制每一個網絡服務的連接數。要限制整體的空閑連接數以及嚴格限制對每一個網絡服務的空閑連接數。

空閑的連接

簡單說明一下,為什麽會出現空閑的連接。
HTTP協議的請求頭裏有一個Connection。在HTTP協議的1.1版本中,默認值是“keep-alive”。在這種情況下的網絡連接是持久連接的,它們會在當前的HTTP事務完成後仍然保持著連通性,因此是可以被復用的。
既然連接可以被復用,就會有兩種可能:

  1. 針對同一個網絡服務,有新的HTTP請求被提交,該連接被再次使用。
  2. 不再對該網絡服務提交HTTP請求,該連接被閑置。這樣就產生了空閑的連接。

另外,如果分配給某一個網絡服務的連接過多的話,也可能會導致空閑連接的產生。因為沒一個HTTP請求只會使用一個空閑的連接。所以,在大多數情況下,都需要限制空閑連接數。

關閉keep-alive
另外,請求頭的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方法主要會做以下幾件事情:

  1. 檢查當前的http.Server類型的值的Addr字段。Addr是當前的網絡服務需要使用的網絡地址,即:IP地址和端口號。如果這個字段的值為空字符串,那麽就用":http"代替。也就是說,使用任何可以代表本機的域名和IP地址,並且端口號為80。
  2. 通過調用net.Listen函數在已確定的網絡地址上啟動基於TCP協議的監聽。
  3. 檢查net.Listen函數返回的錯誤值。如果該錯誤值不為nil,那麽就直接返回該錯誤值。否則,通過調用當前http.Server值的Serve方法準備接受和處理將要到來的HTP請求。

這裏又牽出兩個問題:

  1. net.Listen函數
  2. 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方法執行了。

Go36-47-基於HTTP協議的網絡服務(net/http)