小白學標準庫之 http
1. 前言
標準庫是工具,是手段,是拿來用的。一味的學標準庫就忽視了語言的核心,關鍵。語言層面的特性,記憶體管理,垃圾回收。資料結構,設計模式。這些是程式的核心,要熟練,乃至精通它們,而不是精通標準庫。
標準庫是需要掌握的,瞭解的。可以通過標準庫深挖語言的特性,但不能只學標準庫,學所謂的表面的東西。
基於這個目的,這裡不會深入介紹 http 標準庫,因為它內容太廣,想深亦難。當然不是說不要,是要的,部分內容留作後續研究。
2. net/http 介紹
http 是超文字傳輸協議,是基於 TCP/IP 協議之上的應用層協議。HTTP 協議入門 清晰的介紹了 HTTP 協議。
Go 中實現 http 協議的包是 net/http。實現 http 協議需要 HTTP request 請求和 HTTP response 響應,請求和響應分別對應 Request 和 Response 結構體,如下:
type Request struct { Method string URL *url.URL Proto string // "HTTP/1.0" ProtoMajor int // 1 ProtoMinor int // 0 Header Header ... } type Response struct { Status string // e.g. "200 OK" StatusCode int // e.g. 200 Proto string // e.g. "HTTP/1.0" ProtoMajor int // e.g. 1 ProtoMinor int // e.g. 0 Header Header Body io.ReadCloser ... }
抓住了結構體就抓住了例項對應的屬性和方法。
這裡構造 server 端實現 http response 響應:
package main import ( "io" "log" "net/http" ) func main() { helloHandler := func(w http.ResponseWriter, req *http.Request) { sr := "hello, world with request " + req.Method io.WriteString(w, sr) } http.HandleFunc("/hello", helloHandler) log.Fatal(http.ListenAndServe(":8082", nil)) }
其中:
-
ListenAndServe 呼叫 net 包的 Listen 方法實現 tcp 地址 (ip + port) 的偵聽,Go 標準庫 net 介紹了 net 包相關內容。
-
http.ResponseWriter 是介面,它實現了 Header,Write,WriteHeader 方法向響應新增 header 和 body 內容。如定義當呼叫 /hello api 時返回 404 狀態碼,可呼叫 WriterHeader 方法如下:
w.WriteHeader(404) sr := "hello, world with request " + req.Method io.WriteString(w, sr)
注意狀態碼不能重複寫,如將 WriteHeader(404) 置於 WriteString 後會報錯
http: superfluous response.WriteHeader call from
。 -
http.Request 是客戶端發來的請求,在 Handler 中可使用該請求組合生成響應資訊。這裡將返回字元和請求方法結合作為響應發給客戶端。
繼續構造客戶端實現 HTTP request 請求:
func main() {
response, err := http.Get("http://127.0.0.1:8082/hello")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
defer response.Body.Close()
body, _ := ioutil.ReadAll(response.Body)
fmt.Println(string(body))
fmt.Println(response)
fmt.Println(*response.Request)
}
執行 server 和 client:
// run server
[chunqiu@test http]$ go run server/server.go
// run client
[chunqiu@test http]$ go run main.go
hello, world with request GET
&{404 Not Found 404 HTTP/1.1 1 1 map[Content-Length:[29] Content-Type:[text/plain; charset=utf-8] Date:[Mon, 06 Dec 2021 02:18:11 GMT]] 0xc00009c040 29 [] false false map[] 0xc000140000 <nil>}
{GET http://127.0.0.1:8082/hello HTTP/1.1 1 1 map[] <nil> <nil> 0 [] false 127.0.0.1:8082 map[] map[] <nil> map[] <nil> <nil> <nil> 0xc0000160a0}
從列印返回值可以看到:
- server response 為前面寫入的狀態碼 404;server 和 client 通訊使用的 HTTP 協議為 HTTP/1.1;response 的 Header 頭資訊包括 Content-Length,Content-Type 和 Date 資訊,其中 Content-Length 表示文字,或其它型別的“長度”,如對於 zip 型別,返回的 Content-Length 是 zip 的大小:
Length: 3116622545 (2.9G) [application/zip]
。 - server 的 response 也包括了 request 的資訊,request 是 response 的屬性,可通過 response.Request 呼叫 Request 資訊。
不僅是返回值頭資訊,在 Request 也可以定義頭資訊,如 Content-Type 定義接收型別,Accept 定義接收資料格式等。
3. 程式示例
看一段程式碼:
req, err := http.NewRequest(method, url, data)
if err != nil {
return nil, false, err
}
req.Header.Add("Content-Type", "application/json; charset=utf-8")
resp, err := client.Do(req)
if err != nil {
return nil, false, err
}
defer resp.Body.Close()
out, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, false, err
}
這裡使用了 NewRequest 函式建立 req 例項,通過 client 呼叫 req 的 url 和相應的方法,並且在 req 的頭資訊新增 Content-Type 宣告請求的 body 資訊。有一點需要注意的是 ioutil 包的 ReadAll 方法,它的函式原型為:
// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll is
// defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.
//
// As of Go 1.16, this function simply calls io.ReadAll.
func ReadAll(r io.Reader) ([]byte, error)
相關的描述資訊見原始碼註釋。其中 ReadAll 函式引數為 io.Reader,它是一個實現了 Read 方法的介面。而 resp.Body 是 io.ReadCloser 介面的例項,io.ReadCloser 實現了 Reader 和 Closer 方法。看到了嗎,這裡發生了介面的賦值,關於介面設計與實現及介面賦值部分留作後續研究。
還有一部分內容有待後續研究的是:http 是基於 TCP/IP 之上的應用層協議,它的實現不需要關心底層 TCP/IP 的實現,這是好處又是不好的地方,底層做了什麼, TCP/IP 怎麼處理 http 包,從 client 到 server 經過了什麼,具體流程是什麼樣的。這部分是不明確的,如果不掌握這部分內容 http 傳輸出現問題很難 debug。
芝蘭生於空谷,不以無人而不芳。