GoWeb 處理請求

go語言的 net/http包提供了一系列用於表示HTTP報文的結構,可以使用它處理請求發送響應,其中Request結構代表了客戶端發送的請求報文

type Request

type Request struct {
    // Method指定HTTP方法(GET、POST、PUT等)。對客戶端,""代表GET。
    Method string
    // URL在服務端表示被請求的URI,在客戶端表示要訪問的URL。
    //
    // 在服務端,URL字段是解析請求行的URI(保存在RequestURI字段)得到的,
    // 對大多數請求來說,除了Path和RawQuery之外的字段都是空字符串。
    // (參見RFC 2616, Section 5.1.2)
    //
    // 在客戶端,URL的Host字段指定了要連接的服務器,
    // 而Request的Host字段(可選地)指定要發送的HTTP請求的Host頭的值。
    URL *url.URL
    // 接收到的請求的協議版本。本包生產的Request總是使用HTTP/1.1
    Proto      string // "HTTP/1.0"
    ProtoMajor int    // 1
    ProtoMinor int    // 0
    // Header字段用來表示HTTP請求的頭域。如果頭域(多行鍵值對格式)爲:
    //	accept-encoding: gzip, deflate
    //	Accept-Language: en-us
    //	Connection: keep-alive
    // 則:
    //	Header = map[string][]string{
    //		"Accept-Encoding": {"gzip, deflate"},
    //		"Accept-Language": {"en-us"},
    //		"Connection": {"keep-alive"},
    //	}
    // HTTP規定頭域的鍵名(頭名)是大小寫敏感的,請求的解析器通過規範化頭域的鍵名來實現這點。
    // 在客戶端的請求,可能會被自動添加或重寫Header中的特定的頭,參見Request.Write方法。
    Header Header
    // Body是請求的主體。
    //
    // 在客戶端,如果Body是nil表示該請求沒有主體買入GET請求。
    // Client的Transport字段會負責調用Body的Close方法。
    //
    // 在服務端,Body字段總是非nil的;但在沒有主體時,讀取Body會立刻返回EOF。
    // Server會關閉請求的主體,ServeHTTP處理器不需要關閉Body字段。
    Body io.ReadCloser
    // ContentLength記錄相關內容的長度。
    // 如果爲-1,表示長度未知,如果>=0,表示可以從Body字段讀取ContentLength字節數據。
    // 在客戶端,如果Body非nil而該字段爲0,表示不知道Body的長度。
    ContentLength int64
    // TransferEncoding按從最外到最裏的順序列出傳輸編碼,空切片表示"identity"編碼。
    // 本字段一般會被忽略。當發送或接受請求時,會自動添加或移除"chunked"傳輸編碼。
    TransferEncoding []string
    // Close在服務端指定是否在回覆請求後關閉連接,在客戶端指定是否在發送請求後關閉連接。
    Close bool
    // 在服務端,Host指定URL會在其上尋找資源的主機。
    // 根據RFC 2616,該值可以是Host頭的值,或者URL自身提供的主機名。
    // Host的格式可以是"host:port"。
    //
    // 在客戶端,請求的Host字段(可選地)用來重寫請求的Host頭。
    // 如過該字段爲"",Request.Write方法會使用URL字段的Host。
    Host string
    // Form是解析好的表單數據,包括URL字段的query參數和POST或PUT的表單數據。
    // 本字段只有在調用ParseForm後纔有效。在客戶端,會忽略請求中的本字段而使用Body替代。
    Form url.Values
    // PostForm是解析好的POST或PUT的表單數據。
    // 本字段只有在調用ParseForm後纔有效。在客戶端,會忽略請求中的本字段而使用Body替代。
    PostForm url.Values
    // MultipartForm是解析好的多部件表單,包括上傳的文件。
    // 本字段只有在調用ParseMultipartForm後纔有效。
    // 在客戶端,會忽略請求中的本字段而使用Body替代。
    MultipartForm *multipart.Form
    // Trailer指定了會在請求主體之後發送的額外的頭域。
    //
    // 在服務端,Trailer字段必須初始化爲只有trailer鍵,所有鍵都對應nil值。
    // (客戶端會聲明哪些trailer會發送)
    // 在處理器從Body讀取時,不能使用本字段。
    // 在從Body的讀取返回EOF後,Trailer字段會被更新完畢幷包含非nil的值。
    // (如果客戶端發送了這些鍵值對),此時纔可以訪問本字段。
    //
    // 在客戶端,Trail必須初始化爲一個包含將要發送的鍵值對的映射。(值可以是nil或其終值)
    // ContentLength字段必須是0或-1,以啓用"chunked"傳輸編碼發送請求。
    // 在開始發送請求後,Trailer可以在讀取請求主體期間被修改,
    // 一旦請求主體返回EOF,調用者就不可再修改Trailer。
    //
    // 很少有HTTP客戶端、服務端或代理支持HTTP trailer。
    Trailer Header
    // RemoteAddr允許HTTP服務器和其他軟件記錄該請求的來源地址,一般用於日誌。
    // 本字段不是ReadRequest函數填寫的,也沒有定義格式。
    // 本包的HTTP服務器會在調用處理器之前設置RemoteAddr爲"IP:port"格式的地址。
    // 客戶端會忽略請求中的RemoteAddr字段。
    RemoteAddr string
    // RequestURI是被客戶端發送到服務端的請求的請求行中未修改的請求URI
    // (參見RFC 2616, Section 5.1)
    // 一般應使用URI字段,在客戶端設置請求的本字段會導致錯誤。
    RequestURI string
    // TLS字段允許HTTP服務器和其他軟件記錄接收到該請求的TLS連接的信息
    // 本字段不是ReadRequest函數填寫的。
    // 對啓用了TLS的連接,本包的HTTP服務器會在調用處理器之前設置TLS字段,否則將設TLS爲nil。
    // 客戶端會忽略請求中的TLS字段。
    TLS *tls.ConnectionState
}

Request類型代表一個服務端接受到的或者客戶端發送出去的HTTP請求。

Request各字段的意義和用途在服務端和客戶端是不同的。除了字段本身上方文檔,還可參見Request.Write方法和RoundTripper接口的文檔

獲取請求URL

Request結構中的URL字段用於表示請求行中包含的URL,改字段是一個指向url.URL結構的指針

type URL

type URL struct {
    Scheme     string
    Opaque     string    //編碼的不透明數據
    User       *Userinfo //用戶名和密碼信息
    Host       string    //主機或主機:端口
    Path       string    //路徑(相對路徑可能省略前斜槓)
    RawPath    string    //編碼路徑提示(請參閱EscapedPath方法)
    ForceQuery bool      //追加查詢('?'),即使RawQuery爲空
    RawQuery   string    //編碼的查詢值,不帶'?'
    Fragment   string    //用於引用的片段,不帶'#'
}

表示的一般形式是

[scheme:][//[userinfo@]host][/]path[?query][#fragment]

scheme 後 不以斜槓開頭的URL解釋爲

scheme:opaque[?query][#fragment]

請注意,Path字段以解碼形式存儲:/%47%6f%2f變爲/ Go /。結果是,無法確定路徑中的斜線是原始URL中的斜線,以及哪個是%2f。這種區別很少很重要,但是在這種情況下,代碼應使用RawPath,這是一個可選字段,僅當默認編碼與Path不同時才設置該字段

  1. Path字段

通過 r.URL.Path 只能得到 /hello

  1. RawQuery字段

通過 r.URL.RawQuery 得到的是 username=admin&password=123456

package main

import (
	"fmt"
	"net/http"
)

//創建處理器函數
func handler(w http.ResponseWriter, r *http.Request)  {
	fmt.Fprintln(w, "你發送的請求的請求地址是:", r.URL.Path)
	fmt.Fprintln(w, "你發送的請求的請求地址後的查詢字符串是:", r.URL.RawQuery)
}
func main() {
	http.HandleFunc("/hello", handler)
	http.ListenAndServe(":8080", nil)
}

獲取請求頭中的信息

通過Request結果中的Header字段用來獲取請求頭中的所有信息,Header字段的類型Header類型,而Header類型是一個map[string][]string, string類型的key,string切片類型的值

  • type Header
type Header map[string][]string

Header代表HTTP頭域的鍵值對。

  • func (Header) Get
func (h Header) Get(key string) string

Get返回鍵對應的第一個值,如果鍵不存在會返回""。如要獲取該鍵對應的值切片,請直接用規範格式的鍵訪問map。

  • func (Header) Set
func (h Header) Set(key, value string)

Set添加鍵值對到h,如鍵已存在則會用只有新值一個元素的切片取代舊值切片。

  • func (Header) Add
func (h Header) Add(key, value string)

Add添加鍵值對到h,如鍵已存在則會將新的值附加到舊值切片後面。

  • func (Header) Del
func (h Header) Del(key string)

Del刪除鍵值對。

  • func (Header) Write
func (h Header) Write(w io.Writer) error

Write以有線格式將頭域寫入w

  1. 獲取請求頭中的所有信息
    r.Header
    結果
map[Connection:[keep-alive] Cache-Control:[max-age=0] User-Agent:[Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36] Accept:[text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8] Accept-Encoding:[gzip, deflate, br] Upgrade-Insecure-Requests:[1] Accept-Language:[zh-CN,zh;q=0.9]]
  1. 獲取請求頭中的某個具體屬性的值,如 獲取 Accept-Encoding的值
  • 方式1: r.Header[“Accept-Encoding”]
    得到的是一個字符串切片
    結果
[gzip, deflate, br]
  • 方式2: r.Header.Get(“Accept-Encoding”)
    得到的是字符串形式的值,多個值使用逗號分隔
    結果
gzip, deflate, br
package main

import (
	"fmt"
	"net/http"
)

//創建處理器函數
func handler(w http.ResponseWriter, r *http.Request)  {
	fmt.Fprintln(w, "你發送的請求的請求地址是:", r.URL.Path)
	fmt.Fprintln(w, "你發送的請求的請求地址後的查詢字符串是:", r.URL.RawQuery)
	fmt.Fprintln(w, "請求頭中的所有信息:", r.Header)
	fmt.Fprintln(w, "請求頭中Accept-Encoding的信息:", r.Header["Accept-Encoding"])
	fmt.Fprintln(w, "請求頭中Accept-Encoding的屬性值是:", r.Header.Get("Accept-Encoding"))

}
func main() {
	http.HandleFunc("/hello", handler)
	http.ListenAndServe(":8080", nil)
}

獲取請求體中的信息

請求和響應的主體都是有Request結構中的Body字段表示,這個字段的類型是 io.ReadCloser接口,該接口包含了Reader接口和Closer接口,Reader接口擁有Read方法,Closer接口擁有Close方法

  • type ReadCloser
type ReadCloser interface {
    Reader
    Closer
}

ReadCloser 接口組合了基本的 Read 和 Close 方法

  • type Reader
type Reader interface {
    Read(p []byte) (n int, err error)
}

Reader 接口包裝了基本的 Read 方法。

Read 將 len§ 個字節讀取到 p 中。它返回讀取的字節數 n(0 <= n <= len§) 以及任何遇到的錯誤。即使 Read 返回的 n < len§,它也會在調用過程中使用 p 的全部作爲暫存空間。若一些數據可用但不到 len§ 個字節,Read 會照例返回可用的東西, 而不是等待更多。

當 Read 在成功讀取 n > 0 個字節後遇到一個錯誤或 EOF 情況,它就會返回讀取的字節數。 它會從相同的調用中返回(非nil的)錯誤或從隨後的調用中返回錯誤(和 n == 0)。 這種一般情況的一個例子就是 Reader 在輸入流結束時會返回一個非零的字節數, 可能的返回不是 err == EOF 就是 err == nil。無論如何,下一個 Read 都應當返回 0, EOF。

調用者應當總在考慮到錯誤 err 前處理 n > 0 的字節。這樣做可以在讀取一些字節, 以及允許的 EOF 行爲後正確地處理I/O錯誤。

Read 的實現在 len§ == 0 以外的情況下會阻止返回零字節的計數和 nil 錯誤, 調用者應將返回 0 和 nil 視作什麼也沒有發生;特別是它並不表示 EOF

  • type Closer
type Closer interface {
    Close() error
}

Closer 接口包裝了基本的 Close 方法。

Close 的行爲在第一次調用後沒有定義。具體實現可能有自己的行爲描述

1.由於GET請求沒有請求體,所以需要在HTML頁面中創建一個form表單,通過指定 method=“post” 來發送一個POST請求

表單

<html>
    <head>
        <meta charset="UTF-8" />
    </head>
    <body>
        <form action="http://localhost:8080/hello" method="POST">
            用戶名: <input type="text" name="username" /><br/>
            密碼: <input type="password" name="password" /><br/>
            <input type="submit" />
        </form>
    </body>
</html>

服務器處理請求代碼

package main

import (
	"fmt"
	"net/http"
)

//創建處理器函數
func handler(w http.ResponseWriter, r *http.Request)  {
	fmt.Fprintln(w, "你發送的請求的請求地址是:", r.URL.Path)
	fmt.Fprintln(w, "你發送的請求的請求地址後的查詢字符串是:", r.URL.RawQuery)
	fmt.Fprintln(w, "請求頭中的所有信息:", r.Header)
	fmt.Fprintln(w, "請求頭中Accept-Encoding的信息:", r.Header["Accept-Encoding"])
	fmt.Fprintln(w, "請求頭中Accept-Encoding的屬性值是:", r.Header.Get("Accept-Encoding"))

	//獲取請求體內容的長度
	len := r.ContentLength
	//創建byte切片
	body := make([]byte, len)
	//將請求體中的內容讀到body中
	r.Body.Read(body)
	//在瀏覽器中顯示請求體中的內容
	fmt.Fprintln(w, "請求體中的內容有: ", string(body))

}
func main() {
	http.HandleFunc("/hello", handler)
	http.ListenAndServe(":8080", nil)
}

瀏覽器顯示結果

請求體中的內容有:  username=admin&password=123456

獲取請求參數

form字段

  1. 類型是 url.Values類型,form是解析好的表單數據,包括URL字段的query參數和POST或PUT的表單數據
  • type Values
type Values map[string][]string

值將字符串鍵映射到值列表。它通常用於查詢參數和表單值。與http.Header映射不同,Values映射中的鍵區分大小寫

  1. form字段只有在調用Request的ParseForm方法後纔有效。在客戶端,會忽略請求中的本字段而使用Body替代
  • func (*Request) ParseForm
func (r *Request) ParseForm() error

ParseForm解析URL中的查詢字符串,並將解析結果更新到r.Form字段。

對於POST或PUT請求,ParseForm還會將body當作表單解析,並將結果既更新到r.PostForm也更新到r.Form。解析結果中,POST或PUT請求主體要優先於URL查詢字符串(同名變量,主體的值在查詢字符串的值前面)

如果請求的主體的大小沒有被MaxBytesReader函數設定限制,其大小默認限制爲開頭10MB

ParseMultipartForm會自動調用ParseForm。重複調用本方法是無意義的

  1. 獲取表單中提交的請求參數(username和password)
package main

import (
	"fmt"
	"net/http"
)

//創建處理器函數
func handler(w http.ResponseWriter, r *http.Request)  {
	//解析表單,在調用r.Form之前必須執行該操作
	r.ParseForm()
	//獲取請求參數
	fmt.Fprintln(w, "請求參數有:", r.Form)

}
func main() {
	http.HandleFunc("/hello", handler)
	http.ListenAndServe(":8080", nil)
}

表單

<html>
    <head>
        <meta charset="UTF-8" />
    </head>
    <body>
        <form action="http://localhost:8080/hello?username=admin&password=123456" method="POST">
            用戶名: <input type="text" name="username"><br/>
            密碼: <input type="password" name="password"><br/>
            <input type="submit" />
        </form>
    </body>
</html>

運行結果:

請求參數有: map[username:[linux admin] password:[888 123456]]

發現: 表單中的請求參數username和URL中的請求參數username都獲取到了,而且表單中的請求參數的值排在URL請求參數值的前面

PostForm字段

  1. 類型也是 url.Values 類型,用來獲取表單中的請求參數
package main

import (
	"fmt"
	"net/http"
)

//創建處理器函數
func handler(w http.ResponseWriter, r *http.Request)  {
	//解析表單,在調用r.Form之前必須執行該操作
	r.ParseForm()
	//獲取請求參數
	fmt.Fprintln(w, "請求參數有:", r.Form)
	fmt.Fprintln(w, "POST請求的form表單中的請求參數有:", r.PostForm)

}
func main() {
	http.HandleFunc("/hello", handler)
	http.ListenAndServe(":8080", nil)
}

運行結果:

請求參數有: map[username:[admin admin] password:[123 123456]]
POST請求的form表單中的請求參數有: map[username:[admin] password:[123]]
  1. 但是PostForm字段只支持 application/x-www-form-urlencoded 編碼,如果form表單的enctype屬性值爲 multipart/form-data, 那麼使用PostForm字段無法獲取表單中的數據,此時需要使用 MultipartForm 字段

說明: form表單的 enctype 屬性的默認值是 application/x-www-form-urlencoded 編碼,實現上傳文件時需要將該屬性的值設置爲 multipart/form-data 編碼格式

FormValue方法和 PostFormValue方法

  1. FormValue方法
    可以通過FormValue 方法快速獲取某一個請求參數,該方法調用之前會自動調用 ParseMultipartFormParseForm方法對錶單進行解析
  • func (*Request) FormValue
func (r *Request) FormValue(key string) string

FormValue返回key爲鍵查詢r.Form字段得到結果[]string切片的第一個值。POST和PUT主體中的同名參數優先於URL查詢字符串。如果必要,本函數會隱式調用ParseMultipartForm和ParseForm

  1. PostFormValue方法
    可以通過PostFormValue方法快速獲取表單中的某一個請求參數,該方法調用之前會自動調用ParseMultipartFormParseForm方法對錶單進行解析
  • func (*Request) PostFormValue
func (r *Request) PostFormValue(key string) string

PostFormValue返回key爲鍵查詢r.PostForm字段得到結果[]string切片的第一個值。如果必要,本函數會隱式調用ParseMultipartForm和ParseForm

代碼

package main

import (
	"fmt"
	"net/http"
)

//創建處理器函數
func handler(w http.ResponseWriter, r *http.Request)  {
	//通過直接調用FormValue方法和PostFormValue方法直接獲取請求參數的值
	fmt.Fprintln(w, "URL中的user請求參數的值:", r.FormValue("user"))
	fmt.Fprintln(w, "Form表單中的username請求參數的值:", r.PostFormValue("username"))
	
}
func main() {
	http.HandleFunc("/hello", handler)
	http.ListenAndServe(":8080", nil)
}

表單

<html>
    <head>
        <meta charset="UTF-8" />
    </head>
    <body>
        <form action="http://localhost:8080/hello?user=wuxing&password=666" method="POST" enctype="application/x-www-form-urlencoded">
            用戶名: <input type="text" name="username"><br/>
            密碼: <input type="password" name="password"><br/>
            <input type="submit" />
        </form>
    </body>
</html>

運行結果:

URL中的user請求參數的值: wuxing
Form表單中的username請求參數的值: admin

MultipartForm字段

爲了取得 multipart/form-data編碼的表單數據,需要用到Request結構的 ParseMultipartForm方法和MultipartForm字段,通常上傳文件時會將form表單的 enctype屬性值設置爲 multipart/form-data

  • func (*Request) ParseMultipartForm
func (r *Request) ParseMultipartForm(maxMemory int64) error

ParseMultipartForm將請求的主體作爲multipart/form-data解析。請求的整個主體都會被解析,得到的文件記錄最多maxMemery字節保存在內存,其餘部分保存在硬盤的temp文件裏。如果必要,ParseMultipartForm會自行調用ParseForm。重複調用本方法是無意義的

給客戶端響應

  • type ResponseWriter
type ResponseWriter interface {
    // Header返回一個Header類型值,該值會被WriteHeader方法發送。
    // 在調用WriteHeader或Write方法後再改變該對象是沒有意義的。
    Header() Header
    // WriteHeader該方法發送HTTP回覆的頭域和狀態碼。
    // 如果沒有被顯式調用,第一次調用Write時會觸發隱式調用WriteHeader(http.StatusOK)
    // WriterHeader的顯式調用主要用於發送錯誤碼。
    WriteHeader(int)
    // Write向連接中寫入作爲HTTP的一部分回覆的數據。
    // 如果被調用時還未調用WriteHeader,本方法會先調用WriteHeader(http.StatusOK)
    // 如果Header中沒有"Content-Type"鍵,
    // 本方法會使用包函數DetectContentType檢查數據的前512字節,將返回值作爲該鍵的值。
    Write([]byte) (int, error)
}

ResponseWriter接口被HTTP處理器用於構造HTTP回覆

  1. 給客戶端響應一個字符串
  • 處理器中的代碼
package main

import (
	"net/http"
)

//創建處理器函數
func testString(w http.ResponseWriter, r *http.Request)  {
	w.Write([]byte("你的請求我已經收到!"))
}


func main() {
	http.HandleFunc("/testStr", testString)
	http.ListenAndServe(":8080", nil)
}
  • 瀏覽器中的結果
你的請求我已經收到!
  • 響應報文中的內容
HTTP/1.1 200 OK
Date: Thu, 02 Apr 2020 05:18:15 GMT
Content-Length: 30
Content-Type: text/plain; charset=utf-8
  1. 給客戶端響應一個HTML頁面
  • 處理器中的代碼
package main

import (
	"net/http"
)

//創建處理器函數
func testHtml(w http.ResponseWriter, r *http.Request)  {
	html := `
	<html>
		<head>
			<title>測試響應內容爲網頁</title>
			<meta charset="utf-8"/>
		</head>
		<body>
			我是以網頁的形式響應過來的!
		</body>
	</html>`
	w.Write([]byte(html))
}


func main() {
	http.HandleFunc("/html", testHtml)
	http.ListenAndServe(":8080", nil)
}
  • 瀏覽器中的結果
我是以網頁的形式響應過來的!
  • 響應報文中的內容
HTTP/1.1 200 OK
Date: Thu, 02 Apr 2020 05:25:31 GMT
Content-Length: 174
Content-Type: text/html; charset=utf-8
  1. 給客戶端響應JSON格式數據
  • 處理器端代碼

user.go

package model

//User 結構體
type User struct {
	ID int
	Username string
	Password string
	Email string
}

main.go

package main

import (
	"encoding/json"
	"fmt"
	"goweb/web02_req/model"
	"net/http"
)

//創建處理器函數
func testJsonRes(w http.ResponseWriter, r *http.Request)  {
	//設置響應內容的類型
	w.Header().Set("Content-Type", "application/json")
	//創建User
	user := model.User{
		ID: 1,
		Username: "admin",
		Password: "123456",
		Email: "[email protected]",
	}
	//將User轉換爲Json格式
	json, _ := json.Marshal(user)
	//將json格式的數據響應給客戶端
	w.Write(json)
}

func main() {
	http.HandleFunc("/testJson", testJsonRes)
	http.ListenAndServe(":8080", nil)
}
  • 瀏覽器中的結果
{"ID":1,"Username":"admin","Password":"123456","Email":"[email protected]"}
  • 響應報文中的內容
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 02 Apr 2020 04:17:34 GMT
Content-Length: 74
  1. 讓客戶端重定向
  • 處理器端代碼
package main

import (
	"net/http"
)

//創建處理器函數
func testRedire(w http.ResponseWriter, r *http.Request)  {
	//設置響應頭中的Location屬性
	w.Header().Set("Location", "https://www.baidu.com")
	//設置響應的狀態碼
	w.WriteHeader(302)
}


func main() {
	http.HandleFunc("/testRedirect", testRedire)
	http.ListenAndServe(":8080", nil)
}
  • 響應報文中的內容
HTTP/1.1 302 Found
Location: https://www.baidu.com
Date: Thu, 02 Apr 2020 04:30:42 GMT
Content-Length: 0
Content-Type: text/plain; charset=utf-8
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章