這章主要介紹了 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)
}
}