摘要: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

ProtoProtoMajorProtoMinor 三個字段表示傳入服務器請求的協議版本。對於客戶請求,這些字段將被忽略。 HTTP 客戶端代碼始終使用 HTTP / 1.1HTTP / 2

Header

Header 包含服務端收到或者由客戶端發送的 HTTP 請求頭,該字段是一個 http.Header 類型的指針, http.Header 類型的聲明如下:

type Header map[string][]string
複製代碼

map[string][]string 類型的別名, http.Header 類型實現了 GETSETAdd 等方法用於存取請求頭。如果服務端收到帶有如下請求頭的請求:

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-LengthConnection 會在需要時自動寫入,並且標頭中的值可能會被忽略。

Body

這個字段的類型是 io.ReadCloserBody 是請求的主體。對於客戶端發出的請求, nil 主體表示該請求沒有 Body ,例如 GET 請求。 HTTP 客戶端的傳輸會負責調用 Close 方法。對於服務器接收的請求,請求主體始終爲非 nil ,但如果請求沒有主體,則將立即返回 EOF 。服務器將自動關閉請求主體。服務器端的處理程序不需要關心此操作。

GetBody

客戶端使用的方法的類型,其聲明爲:

GetBody func() (io.ReadCloser, error)
複製代碼

ContentLength

ContentLength 記錄請求關聯內容的長度。值-1表示長度未知。值>=0表示從 Body 中讀取到的字節數。對於客戶請求,值爲0且非 nilBody 也會被視爲長度未知。

####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 字段的查詢參數以及 PATCHPOSTPUT 表單數據。此字段僅在調用 Request.ParseForm 之後可用。 HTTP 客戶端會忽略 Form 並改用 BodyForm 字段的類型是 url.Values 類型的指針。 url.Values 類型的聲明如下:

type Values map[string][]string
複製代碼

也是 map[string][]string 類型的別名。 url.Values 類型實現了 GETSETAddDel 等方法用於存取表單數據。

PostForm

PostForm 類型與 Form 字段一樣,包含來自 PATCHPOST 的已解析表單數據或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 並更改同一請求的調用方所擁有的上下文。

讀取請求頭

上面分析了 GoHTTP 請求頭存儲在 Request 結構體對象的 Header 字段裏, Header 字段實質上是一個 Map ,請求頭的名稱爲Map keyMap 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 結構中定義的 MethodURLHostRemoteAddr 等字段把請求的通用信息打印出來。在我們一直使用的 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 類型的別名,實現了 GETSETAddDel 等方法用於存取數據。

所以我們可以使用 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 字段的查詢參數以及 PATCHPOSTPUT 表單數據。此字段僅在調用 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 類型的值以及 errorCookie 類型的定義如下:

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 可以下載文章中項目的源碼,趕快下載下來自己試一試吧。

相關文章