Go Web編程--深入學習解析HTTP請求
摘要:type Request struct { Method string URL *url.URL Proto string // "HTTP/1.0" ProtoMajor int // 1 ProtoMinor int // 0 Header Header Body io.ReadCloser GetBody func() (io.ReadCloser, error) ContentLength int64 TransferEncoding []string Close bool Host string Form url.Values PostForm url.Values MultipartForm *multipart.Form Trailer Header RemoteAddr string RequestURI string TLS *tls.ConnectionState Cancel
之前這個系列的文章一直在講用 Go
語言 怎麼編寫HTTP服務器 來提供服務,如何給服務器 配置路由 來匹配請求到對應的處理程序,如何 添加中間件 把一些通用的處理任務從具體的Handler中解耦出來,以及如何更規範地在項目中 應用數據庫 。不過一直漏掉了一個環節是服務器接收到請求後如何解析請求拿到想要的數據, Go
語言使用 net/http
包中的 Request
結構體對象來表示 HTTP
請求,通過 Request
結構對象上定義的方法和數據字段,應用程序能夠便捷地訪問和設置 HTTP
請求中的數據。
一般服務端解析請求的需求有如下幾種
Form JSON
今天這篇文章我們就按照這幾種常見的服務端對 HTTP
請求的操作來說一下服務器應用程序如何通過 Request
對象解析請求頭和請求體。
Request 結構定義
在說具體操作的使用方法之前我們先來看看 net/http
包中 Request
結構體的定義,瞭解一下 Request
擁有什麼樣的數據結構。 Request
結構在源碼中的定義如下。
type Request struct { Method string URL *url.URL Proto string // "HTTP/1.0" ProtoMajor int // 1 ProtoMinor int // 0 Header Header Body io.ReadCloser GetBody func() (io.ReadCloser, error) ContentLength int64 TransferEncoding []string Close bool Host string Form url.Values PostForm url.Values MultipartForm *multipart.Form Trailer Header RemoteAddr string RequestURI string TLS *tls.ConnectionState Cancel <-chan struct{} Response *Response ctx context.Context } 複製代碼
我們快速地瞭解一下每個字段大致的含義,瞭解了每個字段的含義在不同的應用場景下需要讀取訪問 HTTP
請求的不同部分時就能夠有的放矢了。
Method
指定HTTP方法(GET,POST,PUT等)。
URL
URL指定要請求的URI(對於服務器請求)或要訪問的URL(用於客戶請求)。它是一個表示 URL
的類型 url.URL
的指針, url.URL
的結構定義如下:
type URL struct { Scheme string Opaque string User *Useri Host string Path string RawPath string ForceQuery bool RawQuery string Fragment string } 複製代碼
Proto
Proto
, ProtoMajor
, ProtoMinor
三個字段表示傳入服務器請求的協議版本。對於客戶請求,這些字段將被忽略。 HTTP
客戶端代碼始終使用 HTTP / 1.1
或 HTTP / 2
。
Header
Header
包含服務端收到或者由客戶端發送的 HTTP
請求頭,該字段是一個 http.Header
類型的指針, http.Header
類型的聲明如下:
type Header map[string][]string 複製代碼
是 map[string][]string
類型的別名, http.Header
類型實現了 GET
, SET
, Add
等方法用於存取請求頭。如果服務端收到帶有如下請求頭的請求:
Host: example.com accept-encoding: gzip, deflate Accept-Language: en-us fOO: Bar foo: two 複製代碼
那麼 Header
的值爲:
Header = map[string][]string{ "Accept-Encoding": {"gzip, deflate"}, "Accept-Language": {"en-us"}, "Foo": {"Bar", "two"}, } 複製代碼
對於傳入的請求, Host
標頭被提升爲 Request.Host
字段,並將其從 Header
對象中刪除。 HTTP
定義頭部的名稱是不區分大小寫的。 Go
使用 CanonicalHeaderKey
實現的請求解析器使得請求頭名稱第一個字母以及跟隨在短橫線後的第一個字母大寫其他都爲小寫形式,比如: Content-Length
。對於客戶端請求,某些標頭,例如 Content-Length
和 Connection
會在需要時自動寫入,並且標頭中的值可能會被忽略。
Body
這個字段的類型是 io.ReadCloser
, Body
是請求的主體。對於客戶端發出的請求, nil
主體表示該請求沒有 Body
,例如 GET
請求。 HTTP
客戶端的傳輸會負責調用 Close
方法。對於服務器接收的請求,請求主體始終爲非 nil
,但如果請求沒有主體,則將立即返回 EOF
。服務器將自動關閉請求主體。服務器端的處理程序不需要關心此操作。
GetBody
客戶端使用的方法的類型,其聲明爲:
GetBody func() (io.ReadCloser, error) 複製代碼
ContentLength
ContentLength
記錄請求關聯內容的長度。值-1表示長度未知。值>=0表示從 Body
中讀取到的字節數。對於客戶請求,值爲0且非 nil
的 Body
也會被視爲長度未知。
####TransferEncoding
TransferEncoding
爲字符串切片,其中會列出從最外層到最內層的傳輸編碼, TransferEncoding
通常可以忽略;在發送和接收請求時,分塊編碼會在需要時自動被添加或者刪除。
Close
Close
表示在服務端回覆請求或者客戶端讀取到響應後是否要關閉連接。對於服務器請求,HTTP服務器會自動處理 並且處理程序不需要此字段。對於客戶請求,設置此字段爲 true
可防止重複使用到相同主機的請求之間的TCP連接,就像已設置 Transport.DisableKeepAlives
一樣。
Host
對於服務器請求, Host
指定URL所在的主機,爲防止DNS重新綁定攻擊,服務器處理程序應驗證 Host
標頭具有的值。 http
庫中的 ServeMux
(複用器)支持註冊到特定 Host
的模式,從而保護其註冊的處理程序。對於客戶端請求, Host
可以用來選擇性地覆蓋請求頭中的 Host
,如果不設置, Request.Write
使用 URL.Host
來設置請求頭中的 Host
。
Form
Form
包含已解析的表單數據,包括 URL
字段的查詢參數以及 PATCH
, POST
或 PUT
表單數據。此字段僅在調用 Request.ParseForm
之後可用。 HTTP
客戶端會忽略 Form
並改用 Body
。 Form
字段的類型是 url.Values
類型的指針。 url.Values
類型的聲明如下:
type Values map[string][]string 複製代碼
也是 map[string][]string
類型的別名。 url.Values
類型實現了 GET
, SET
, Add
, Del
等方法用於存取表單數據。
PostForm
PostForm
類型與 Form
字段一樣,包含來自 PATCH
, POST
的已解析表單數據或PUT主體參數。此字段僅在調用 ParseForm
之後可用。 HTTP
客戶端會忽略 PostForm
並改用 Body
。
####MultipartForm
MultipartForm
是已解析的多部分表單數據,包括文件上傳。僅在調用 Request.ParseMultipartForm
之後,此字段纔可用。 HTTP
客戶端會忽略 MultipartForm
並改用 Body
。該字段的類型是 *multipart.Form
。
RemoteAddr
RemoteAddr
允許 HTTP
服務器和其他軟件記錄發送請求的網絡地址,通常用於記錄。 net/http
包中的HTTP服務器在調用處理程序之前將 RemoteAddr
設置爲“ IP:端口”, HTTP客戶端會忽略此字段。
RequestURI
RequestURI
是未修改的 request-target
客戶端發送的請求行(RFC 7230,第3.1.1節)。在服務器端,通常應改用URL字段。在HTTP客戶端請求中設置此字段是錯誤的。
Response
Response
字段類型爲 *Response
,它指定了導致此請求被創建的重定向響應,此字段僅在客戶端發生重定向時被填充。
ctx
ctx
是客戶端上下文或服務器上下文。它應該只通過使用 WithContext
複製整個 Request
進行修改。這個字段未導出以防止人們錯誤使用 Context
並更改同一請求的調用方所擁有的上下文。
讀取請求頭
上面分析了 Go
將 HTTP
請求頭存儲在 Request
結構體對象的 Header
字段裏, Header
字段實質上是一個 Map
,請求頭的名稱爲Map key
, Map Value
的類型爲字符串切片,有的請求頭像 Accept
會有多個值,在切片中就對應多個元素。
Header
類型的 Get
方法可以獲取請求頭的第一個值,
func exampleHandler(w http.ResponseWriter, r *http.Request) { ua := r.Header.Get("User-Agent") ... } 複製代碼
或者是獲取值時直接通過 key
獲取對應的切片值就好,比如將上面的改爲:
ua := r.Header["User-Agent"] 複製代碼
下面我們寫個遍歷請求頭信息的示例程序,同時也會通上面介紹的 Request
結構中定義的 Method
, URL
, Host
, RemoteAddr
等字段把請求的通用信息打印出來。在我們一直使用的 http_demo
項目中增加一個 DisplayHeadersHandler
,其源碼如下:
package handler import ( "fmt" "net/http" ) func DisplayHeadersHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Method: %s URL: %s Protocol: %s \n", r.Method, r.URL, r.Proto) // 遍歷所有請求頭 for k, v := range r.Header { fmt.Fprintf(w, "Header field %q, Value %q\n", k, v) } fmt.Fprintf(w, "Host = %q\n", r.Host) fmt.Fprintf(w, "RemoteAddr= %q\n", r.RemoteAddr) // 通過 Key 獲取指定請求頭的值 fmt.Fprintf(w, "\n\nFinding value of \"Accept\" %q", r.Header["Accept"]) } 複製代碼
將其處理程序綁定到 /index/display_headers
路由上:
indexRouter.HandleFunc("/display_headers", handler.DisplayHeadersHandler) 複製代碼
然後啓動項目,打開瀏覽器訪問:
http://localhost:8000/index/display_headers 複製代碼
可以看到如下輸出:
http_demo
項目中已經添加了本文中所有示例的源碼,關注文末公衆號回覆 gohttp06
可以獲取源碼的下載鏈接。
獲取URL參數值
GET
請求中的 URL
查詢字符串中的參數可以通過 url.Query()
,我們來看一下啊 url.Query()
函數的源碼:
func (u *URL) Query() Values { v, _ := ParseQuery(u.RawQuery) return v } 複製代碼
它通過 ParseQuery
函數解析 URL
參數然後返回一個 url.Values
類型的值。 url.Values
類型上面我們已經介紹過了是 map[string][]string
類型的別名,實現了 GET
, SET
, Add
, Del
等方法用於存取數據。
所以我們可以使用 r.URL.Query().Get("ParamName")
獲取參數值,也可以使用 r.URL.Query()["ParamName"]
。兩者的區別是 Get
只返回切片中的第一個值,如果參數對應多個值時(比如複選框表單那種請求就是一個 name
對應多個值),記住要使用第二種方式。
我們通過運行一個示例程序 display_url_params.go
來看一下兩種獲取 URL
參數的區別
package handler import ( "fmt" "net/http" ) func DisplayUrlParamsHandler(w http.ResponseWriter, r *http.Request) { for k, v := range r.URL.Query() { fmt.Fprintf(w, "ParamName %q, Value %q\n", k, v) fmt.Fprintf(w, "ParamName %q, Get Value %q\n", k, r.URL.Query().Get(k)) } } 複製代碼
將其處理程序綁定到 /index/display_url_params
路由上:
indexRouter.HandleFunc("/display_url_params", handler.DisplayUrlParamsHandler) 複製代碼
打開瀏覽器訪問
http://localhost:8000/index/display_url_params?a=b&c=d&a=c 複製代碼
瀏覽器會輸出:
ParamName "a", Value ["b" "c"] ParamName "a", Get Value "b" ParamName "c", Value ["d"] ParamName "c", Get Value "d" 複製代碼
我們爲參數 a
傳遞了兩個參數值,可以看到通過 url.Query.Get()
只能讀取到第一個參數值。
獲取表單中的參數值
Request
結構的 Form
字段包含已解析的表單數據,包括 URL
字段的查詢參數以及 PATCH
, POST
或 PUT
表單數據。此字段僅在調用 Request.ParseForm
之後可用。不過 Request
對象提供一個 FormValue
方法來獲取指定名稱的表單數據, FormValue
方法會根據 Form
字段是否有設置來自動執行 ParseForm
方法。
func (r *Request) FormValue(key string) string { if r.Form == nil { r.ParseMultipartForm(defaultMaxMemory) } if vs := r.Form[key]; len(vs) > 0 { return vs[0] } return "" } 複製代碼
可以看到 FormValue
方法也是隻返回切片中的第一個值。如果需要獲取字段對應的所有值,那麼需要通過字段名訪問 Form
字段。如下:
獲取表單字段的單個值
r.FormValue(key) 複製代碼
獲取表單字段的多個值
r.ParseForm() r.Form["key"] 複製代碼
下面是我們的示例程序,以及對應的路由:
//handler/display_form_data.go package handler import ( "fmt" "net/http" ) func DisplayFormDataHandler(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { panic(err) } for key, values := range r.Form { fmt.Fprintf(w, "Form field %q, Values %q\n", key, values) fmt.Fprintf(w, "Form field %q, Value %q\n", key, r.FormValue(key)) } } //router.go indexRouter.HandleFunc("/display_form_data", handler.DisplayFormDataHandler) 複製代碼
我們在命令行中使用 cURL
命令發送表單數據到處理程序,看看效果。
curl -X POST -d 'username=James&password=123' \ http://localhost:8000/index/display_form_data 複製代碼
返回的響應如下:
Form field "username", Values ["James"] Form field "username", Value "James" Form field "password", Values ["123"] Form field "password", Value "123" 複製代碼
獲取 Cookie
Request
對象專門提供了一個 Cookie
方法用來訪問請求中攜帶的 Cookie
數據,方法會返回一個 *Cookie
類型的值以及 error
。 Cookie
類型的定義如下:
type Cookie struct { Name string Value string Path string // optional Domain string // optional Expires time.Time // optional RawExpires string // for reading cookies only MaxAge int Secure bool HttpOnly bool SameSite SameSite Raw string Unparsed []string } 複製代碼
所以要讀取請求中指定名稱的 Cookie
值,只需要
cookie, err := r.Cookie(name) // 錯誤檢查 ... value := cookie.Value 複製代碼
Request.Cookies()
方法會返回 []*Cookie
切片,其中會包含請求中所有的 Cookie
下面的示例程序,會打印請求中所有的 Cookie
// handler/read_cookie.go package handler import ( "fmt" "net/http" ) func ReadCookieHandler(w http.ResponseWriter, r *http.Request) { for _, cookie := range r.Cookies() { fmt.Fprintf(w, "Cookie field %q, Value %q\n", cookie.Name, cookie.Value) } } //router/router.go indexRouter.HandleFunc("/read_cookie", handler.ReadCookieHandler) 複製代碼
我們通過 cURL
在命令行請求 http://localhost:8000/index/read_cookie
curl --cookie "USER_TOKEN=Yes" http://localhost:8000/index/read_cookie 複製代碼
執行命令後會返回:
Cookie field "USER_TOKEN", Value "Yes" 複製代碼
解析請求體中的JSON數據
現在前端都傾向於把請求數據以 JSON
格式放到請求主體中傳給服務器,針對這個使用場景,我們需要把請求體作爲 json.NewDecoder()
的輸入流,然後將請求體中攜帶的 JSON
格式的數據解析到聲明的結構體變量中
//handler/parse_json_request package handler import ( "encoding/json" "fmt" "net/http" ) type Person struct { Name string Age int } func DisplayPersonHandler(w http.ResponseWriter, r *http.Request) { var p Person // 將請求體中的 JSON 數據解析到結構體中 // 發生錯誤,返回400 錯誤碼 err := json.NewDecoder(r.Body).Decode(&p) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } fmt.Fprintf(w, "Person: %+v", p) } // router/router.go indexRouter.HandleFunc("/parse_json_request", handler.ParseJsonRequestHandler) 複製代碼
在命令行裏用 cURL
命令測試我們的程序:
curl -X POST -d '{"name": "James", "age": 18}' \ -H "Content-Type: application/json" \ http://localhost:8000/index/parse_json_request 複製代碼
返回響應如下:
Person: {Name:James Age:18}% 複製代碼
讀取上傳文件
服務器接收客戶端上傳的文件,使用 Request
定義的 FormFile()
方法。該方法會自動調用 r.ParseMultipartForm(32 << 20)
方法解析請求多部表單中的上傳文件,並把文件可讀入內存的大小設置爲 32M
(32向左位移20位),如果內存大小需要單獨設置,就要在程序裏單獨調用 ParseMultipartForm()
方法纔行。
func ReceiveFile(w http.ResponseWriter, r *http.Request) { r.ParseMultipartForm(32 << 20) var buf bytes.Buffer file, header, err := r.FormFile("file") if err != nil { panic(err) } defer file.Close() name := strings.Split(header.Filename, ".") fmt.Printf("File name %s\n", name[0]) io.Copy(&buf, file) contents := buf.String() fmt.Println(contents) buf.Reset() return } 複製代碼
Go語言解析 HTTP
請求比較常用的方法我們都介紹的差不多了。因爲想總結全一點,篇幅還是有點長,不過整體不難懂,而且也可以下載程序中的源碼自己運行調試,動手實踐一下更有助於理解吸收。 HTTP
客戶端發送請求要設置的內容也只今天講的 Request
結構體的字段, Request
對象也提供了一些設置相關的方法供開發人員使用,今天就先說這麼多了。
關注下方公衆號回覆 gohttp06
可以下載文章中項目的源碼,趕快下載下來自己試一試吧。