Go Web編程--深刻學習解析HTTP請求

以前這個系列的文章一直在講用Go語言怎麼編寫HTTP服務器來提供服務,如何給服務器配置路由來匹配請求到對應的處理程序,如何添加中間件把一些通用的處理任務從具體的Handler中解耦出來,以及如何更規範地在項目中應用數據庫。不過一直漏掉了一個環節是服務器接收到請求後如何解析請求拿到想要的數據,Go語言使用net/http包中的Request結構體對象來表示HTTP請求,經過Request結構對象上定義的方法和數據字段,應用程序可以便捷地訪問和設置HTTP請求中的數據。前端

通常服務端解析請求的需求有以下幾種shell

  • 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請求的不一樣部分時就可以有的放矢了。json

Method

指定HTTP方法(GET,POST,PUT等)。瀏覽器

URL

URL指定要請求的URI(對於服務器請求)或要訪問的URL(用於客戶請求)。它是一個表示URL的類型url.URL的指針,url.URL的結構定義以下:bash

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類型的聲明以下:cookie

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,請求頭的名稱爲MapkeyMap 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
複製代碼

能夠看到以下輸出:

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能夠下載文章中項目的源碼,趕快下載下來本身試一試吧。

前文回顧

深刻學習用Go編寫HTTP服務器

Web服務器路由

十分鐘學會用Go編寫Web中間件

Go Web編程--應用ORM

相關文章
相關標籤/搜索