Golang 學習筆記(二) - HTTP 客戶端 - 使用 Client 型別

broqiang發表於2018-06-17

這章主要介紹了 Client 型別以及 Do 和 Head 的使用。

Client型別代表HTTP客戶端。它的零值( DefaultClient )是一個可用的使用 DefaultTransport 的客戶端。

Client 的 Transport 欄位一般會含有內部狀態(快取 TCP 連線),因此 Client 型別值應儘量被重用而不是每次需要都建立新的。 Client 型別值可以安全的被多個go程同時使用。

Client 型別的層次比 RoundTripper 介面(如 Transport )高,還會管理 HTTP 的 cookie 和重定向等細節。

Client 型別的結構體

type Client struct {
    // Transport 指定執行獨立、單次 HTTP 請求的機制。
    // 如果 Transport 為 nil,則使用 DefaultTransport 。
    Transport RoundTripper

    // CheckRedirect 指定處理重定向的策略。
    // 如果 CheckRedirect 不為 nil,客戶端會在執行重定向之前呼叫本函式欄位。
    // 引數 req 和 via 是將要執行的請求和已經執行的請求(切片,越新的請求越靠後)。
    // 如果 CheckRedirect 返回一個錯誤,本型別的 Get 方法不會傳送請求 req,
    // 而是返回之前得到的最後一個回覆和該錯誤。(包裝進 url.Error 型別裡)
    //
    // 如果CheckRedirect為nil,會採用預設策略:連續10此請求後停止。
    CheckRedirect func(req *Request, via []*Request) error

    // Jar 指定 cookie 管理器。
    // 如果Jar為nil,請求中不會傳送 cookie ,回覆中的 cookie 會被忽略。
    Jar CookieJar

    // Timeout 指定本型別的值執行請求的時間限制。
    // 該超時限制包括連線時間、重定向和讀取回復主體的時間。
    // 計時器會在 Head 、 Get 、 Post 或 Do 方法返回後繼續運作並在超時後中斷回覆主體的讀取。
    //
    // Timeout 為零值表示不設定超時。
    //
    // Client 例項的 Transport 欄位必須支援 CancelRequest 方法,
    // 否則 Client 會在試圖用 Head 、 Get 、 Post 或 Do 方法執行請求時返回錯誤。
    // 本型別的 Transport 欄位預設值( DefaultTransport )支援 CancelRequest 方法。
    Timeout time.Duration
}

在此目錄下初始化了一個 Client ,用來配合後面測試使用。

package myclient

import (
    "net/http"
)

var Client = &http.Client{}

Do方法傳送請求,返回HTTP回覆。它會遵守客戶端c設定的策略(如重定向、cookie、認證)。

如果客戶端的策略(如重定向)返回錯誤或存在HTTP協議錯誤時,本方法將返回該錯誤;如果回應的狀態碼不是2xx,本方法並不會返回錯誤。

如果返回值err為nil,resp.Body總是非nil的,呼叫者應該在讀取完resp.Body後關閉它。如果返回值resp的主體未關閉,c下層的RoundTripper介面(一般為Transport型別)可能無法重用resp主體下層保持的TCP連線去執行之後的請求。

請求的主體,如果非nil,會在執行後被c.Transport關閉,即使出現錯誤。

一般應使用 Get 、 Post 或 PostForm 方法就可以代替Do方法,其實它們最終執行的也是 Do ,只不過做了一些包裝。

當有一些比較特殊的,上面的三種方式不能滿足時,就要自己初始化 Request ,然後呼叫 Do 方法。

語法:

func (c *Client) Do(req *Request) (resp *Response, err error)

引數:

  • *Request 可以通過這個引數來自定義 Request

返回值:

  • *Response 如果獲取到了資料,會將資料儲存在 Response 中

  • error 如果請求資料的時候出現錯誤,會返回一個 error ,並將具體的錯誤記錄到 error 中

示例:

詳細的使用請看示例程式碼,已經在裡面寫了詳細的註釋。

myclient.Client 是在上一級目錄的 client.go 檔案中初始化的。

伺服器端

server.go

package main

import (
    "fmt"
    "io"
    "log"
    "net/http"
    "strings"
    "time"
)

func main() {
    http.HandleFunc("/", TestDo)

    log.Fatalf("%v", http.ListenAndServe("localhost:8080", nil))
}

func TestDo(w http.ResponseWriter, req *http.Request) {
    // 休眠 2 秒再處理,一會來測試超時
    time.Sleep(2e9)

    // 驗證 csrf
    if req.Header.Get("_csrf") != "123456" {
        http.Error(w, "無效的 csrf token", 400)
        return
    }

    // 獲取 cookie
    cookie := req.Header.Get("Cookie")
    if cookie == "" {
        http.Error(w, "請登陸後再操作", 401)
        return
    }

    // 獲取使用者名稱,簡單的利用 strings 去擷取字串,只是個簡單示例,沒有考慮那麼多可能性。
    fmt.Printf("%v\n", cookie)
    index := strings.Index(cookie, "=")
    name := cookie[index+1:]
    fmt.Printf("%v\n", name)
    if name != "BroQiang" {
        http.Error(w, "當前使用者沒有許可權操作", 401)
        return
    }

    io.WriteString(w, "Hello "+name)
}

客戶端

client.go

package main

import (
    "log"
    "net/http"
    "os"

    "io/ioutil"

    "fmt"
    "io"

    "github.com/broqiang/go-packages-study/packages/net/http/client"
)

func main() {
    // 測試自定義的 Get 函式
    GetData()

    // 當然,我們不是為了自定義 Get 、 Post 方法,才去實現 Do 方法的呼叫
    // 是因為有一些特殊的需求,比如要攜帶 cooki , 連線超時時間設定等。
    MyConnection()
}

func MyConnection() {
    log.Println("--------------- 開始執行自定義的 Do 方法 ----------------")
    // 正常執行自定義的請求
    myDo()

    log.Println("--------------- 開始執行超時後的自定義的 Do 方法 ----------------")
    // 給 Client 設定一個超時,然後測試效果
    // 這裡只是簡單的驗證超時,沒有管 Transport 的設定
    myclient.Client.Timeout = 1e9
    myDo()
    // 可以看到會有一個連結超時的錯誤,如下面這樣
    // Get http://localhost:8080: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
}

func myDo() {
    // 先自定義一個 Request
    req, err := http.NewRequest("GET", "http://localhost:8080", nil)
    ErrPrint(err)

    // 設定一個 csrf token, 伺服器端會去驗證這個
    req.Header.Set("_csrf", "123456")
    // 設定一個 Cookie, 伺服器端會去驗證這個
    req.Header.Set("Cookie", "name=BroQiang")
    // 一會寫完伺服器端可以嘗試分別將上面兩行註釋去測試

    resp, err := myclient.Client.Do(req)
    ErrPrint(err)
    defer resp.Body.Close()

    DataPrint(resp.Body)
    // 列印下狀態碼,看下效果
    fmt.Printf("返回的狀態碼是: %v\n", resp.StatusCode)
    fmt.Printf("返回的資訊是: %v\n", resp.StatusCode)
}

func GetData() {
    log.Println("------------ 自定義的 Get 方法獲取資料 ----------")
    url := "https://broqiang.com"
    resp, err := MyGet(url)
    ErrPrint(err)
    defer resp.Body.Close()

    DataPrint(resp.Body)

}

// 實現和 Get 函式一樣的功能,其實這個就是原始碼改的
// Post 和 PostForm 就演示了,實現原理差不多,最終夜都是要呼叫 client.Do
func MyGet(url string) (resp *http.Response, err error) {
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        return nil, err
    }
    return myclient.Client.Do(req)
}

func DataPrint(body io.ReadCloser) {
    // 拿到資料
    bytes, err := ioutil.ReadAll(body)
    ErrPrint(err)

    // 這裡要格式化再輸出,因為 ReadAll 返回的是位元組切片
    fmt.Printf("%s\n", bytes)
}

func ErrPrint(err error) {
    if err != nil {
        log.Fatalln(err)
        os.Exit(1)
    }
}

Head 向指定的 URL 發出一個 HEAD 請求,如果回應的狀態碼如下,Head會在呼叫 client.CheckRedirect 後執行重定向。

就是 Head 只會請求 Head 部分,可以快速的返回。比如你寫了一個爬蟲,可以先通過 Head 測試下,是否是 200 的,就根據返回的狀態碼來做處理,Head 請求沒有 Body 部分,速度會快很多。

除了沒有返回的 Body ,基本上用起來和 Get 差不多。

Head 函式

語法:

Head(url string) (resp *Response, err error)

引數:

  • 字串型別的 url 地址,需要注意的是這裡要是完整地址,要加上 http://https:// 的地址

返回值:

  • *Response 如果獲取到了資料,會將資料儲存在 Response 中

  • error 如果請求資料的時候出現錯誤,會返回一個 error ,並將具體的錯誤記錄到 error 中

另外一種方式 Head 方法

可以通過 client 結構體的 Head() 方法獲取資料,其實兩種方式是一樣的,Head() 函式也是呼叫的結構體中的 Head() 方法。詳細的使用可以見示例中的用法

示例:

client.go

package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "os"
)

func main() {
    fmt.Println("----------訪問一個正常的 URL ----------")
    MyHead("https://broqiang.com")
    // 結果:返回的狀態碼是: 200

    fmt.Println("----------訪問一個正常的 URL ----------")
    // 可以將之前測試 Do 用的伺服器端啟動,因為這個伺服器端有限制,
    // 可以通過返回的 code 來直到訪問的狀況。
    MyHead("http://localhost:8080")
    // 結果: 返回的狀態碼是: 400
}

func MyHead(url string) {
    resp, err := http.Head(url)
    ErrPrint(err)

    defer resp.Body.Close()

    bytes, err := ioutil.ReadAll(resp.Body)
    ErrPrint(err)

    fmt.Printf("%q\n", bytes)
    fmt.Printf("返回的狀態碼是: %d\n", resp.StatusCode)
}

func ErrPrint(err error) {
    if err != nil {
        log.Fatalln(err)
        os.Exit(1)
    }
}

相關文章