之前這個系列的文章一直在講用Go
語言怎麼編寫HTTP伺服器來提供服務,如何給伺服器配置路由來匹配請求到對應的處理程式,如何新增中介軟體把一些通用的處理任務從具體的Handler中解耦出來,以及如何更規範地在專案中應用資料庫。不過一直漏掉了一個環節是伺服器接收到請求後如何解析請求拿到想要的資料,Go
語言使用net/http
包中的Request
結構體物件來表示HTTP
請求,通過Request
結構物件上定義的方法和資料欄位,應用程式能夠便捷地訪問和設定HTTP
請求中的資料。
一般服務端解析請求的需求有如下幾種
- HTTP請求頭中的欄位值
- URL 查詢字串中的欄位值
- 請求體中的
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
,請求頭的名稱為Mapkey
,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
可以下載文章中專案的原始碼,趕快下載下來自己試一試吧。
前文回顧
本作品採用《CC 協議》,轉載必須註明作者和本文連結