Go Web 編程之 請求

概述

前面咱們學習了處理器和處理器函數,如何編寫和註冊處理器。本文咱們將學習如何從請求中獲取信息。html

請求的結構

經過前面的學習,咱們知道處理器函數須要符合下面的簽名:git

func (w http.ResponseWriter, r *http.Request)

其中,http.Request就是請求的類型。客戶端傳遞的數據均可以經過這個結構來獲取。結構Request定義在包 net/http 中:github

// src/net/http/request.go

type Request struct {
    Method          string    
    URL             *url.URL
    Proto           string
    ProtoMajor      int
    ProtoMinor      int
    Header          Header
    Body            io.ReadCloser
    ContentLength   int
    // 省略一些字段...
}

咱們來看一下幾個重要的字段。golang

Method

請求中的Method字段表示客戶端想要調用服務器的哪一個方法。在第一篇文章中,咱們提到過 HTTP 協議方法。其取值有GET/POST/PUT/DELETE等。服務器根據請求方法的不一樣會進行不一樣的處理,例如GET方法只是獲取信息(用戶基本信息,商品信息等),POST方法建立新的資源(註冊新用戶,上架新商品等)。web

URL

Tim Berners-Lee 在建立萬維網的同時,也引入了使用字符串來表示互聯網資源的概念。他稱該字符串爲統一資源標識符(URI,Uniform Resource Identifier)。URI 由兩部分組成。一部分表示資源的名稱,即統一資源名稱(URN,Uniform Resource Name)。另外一部分表示資源的位置,即統一資源定位符(URL,Uniform Resource Location)。ajax

在 HTTP 請求中,使用 URL 來對要操做的資源位置進行描述。URL 的通常格式爲:編程

[scheme:][//[userinfo@]host][/]path[?query][#fragment]
  • scheme:協議名,常見的有httphttpsftp
  • userInfo:如有,則表示用戶信息,如用戶名和密碼可寫做dj:password
  • host:表示主機域名或地址,和一個可選的端口信息。若端口未指定,則默認爲 80。例如www.example.comwww.example.com:8080127.0.0.1:8080
  • path:資源在主機上的路徑,以/分隔,如/posts
  • query:可選的查詢字符串,客戶端傳輸過來的鍵值對參數,鍵值直接用=,多個鍵值對之間用&鏈接,如page=1&count=10
  • fragment:片斷,又叫錨點。表示一個頁面中的位置信息。由瀏覽器發起的請求 URL 中,一般沒有這部分信息。可是能夠經過ajax等代碼的方式發送這個數據;

咱們來看一個完整的 URL:json

http://dj:password@www.example.com/posts?page=1&count=10#fmt

Go 中的 URL 結構定義在net/url包中:瀏覽器

// net/url/url.go
type URL struct {
    Scheme      string
    Opaque      string
    User        *Userinfo
    Host        string
    Path        string
    RawPath     string
    RawQuery    string
    Fragment    string
}

能夠經過請求對象中的URL字段獲取這些信息。接下來,咱們編寫一個程序來具體看看(使用上一篇文章講的 Web 程序基本結構,只須要增長處理器函數和註冊便可):安全

func urlHandler(w http.ResponseWriter, r *http.Request) {
    URL := r.URL
    
    fmt.Fprintf(w, "Scheme: %s\n", URL.Scheme)
    fmt.Fprintf(w, "Host: %s\n", URL.Host)
    fmt.Fprintf(w, "Path: %s\n", URL.Path)
    fmt.Fprintf(w, "RawPath: %s\n", URL.RawPath)
    fmt.Fprintf(w, "RawQuery: %s\n", URL.RawQuery)
    fmt.Fprintf(w, "Fragment: %s\n", URL.Fragment)
}

// 註冊
mux.HandleFunc("/url", urlHandler)

運行服務器,經過瀏覽器訪問localhost:8080/url/posts?page=1&count=10#main

Scheme: 
Host: 
Path: /url/posts
RawPath: 
RawQuery: page=1&count=10
Fragment:

爲何會出現空字段?注意到源碼Request結構中URL字段上有一段註釋:

// URL specifies either the URI being requested (for server
// requests) or the URL to access (for client requests).
//
// For server requests, the URL is parsed from the URI
// supplied on the Request-Line as stored in RequestURI.  For
// most requests, fields other than Path and RawQuery will be
// empty. (See RFC 7230, Section 5.3)
//
// For client requests, the URL's Host specifies the server to
// connect to, while the Request's Host field optionally
// specifies the Host header value to send in the HTTP
// request.

大意是做爲服務器收到的請求時,URL中除了PathRawQuery,其它字段大多爲空。對於這個問題,Go 的 Github 倉庫上Issue 28940有過討論。

咱們還能夠經過URL結構獲得一個 URL 字符串:

URL := &net.URL {
    Scheme:     "http",
    Host:       "example.com",
    Path:       "/posts",
    RawQuery:   "page=1&count=10",
    Fragment:   "main",
}
fmt.Println(URL.String())

上面程序運行輸出字符串:

http://example.com/posts?page=1&count=10#main

Proto/ProtoMajor/ProtoMinor

Proto表示 HTTP 協議版本,如HTTP/1.1ProtoMajor表示大版本,ProtoMinor表示小版本。

func protoFunc(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Proto: %s\n", r.Proto)
    fmt.Fprintf(w, "ProtoMajor: %d\n", r.ProtoMajor)
    fmt.Fprintf(w, "ProtoMinor: %d\n", r.ProtoMinor)
}

mux.HandleFunc("/proto", protoFunc)

啓動服務器,瀏覽器請求localhost:8080返回:

Proto: HTTP/1.1
ProtoMajor: 1
ProtoMinor: 1

當前 HTTP/1.1 是主流的版本。

Header

Header中存放的客戶端發送過來的首部信息,鍵-值對的形式。Header類型底層實際上是map[string][]string

// src/net/http/header.go
type Header map[string][]string

每一個首部的鍵和值都是字符串,能夠設置多個相同的鍵。注意到Header值爲[]string類型,存放相同的鍵的多個值。瀏覽器發起 HTTP 請求的時候,會自動添加一些首部。咱們編寫一個程序來看看:

func headerHandler(w http.ResponseWriter, r *http.Request) {
    for key, value := range r.Header {
        fmt.Fprintf(w, "%s: %v\n", key, value)
    }
}

mux.HandleFunc("/header", headerHandler)

啓動服務器,瀏覽器請求localhost:8080/header返回:

Accept-Encoding: [gzip, deflate, br]
Sec-Fetch-Site: [none]
Sec-Fetch-Mode: [navigate]
Connection: [keep-alive]
Upgrade-Insecure-Requests: [1]
User-Agent: [Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36]
Sec-Fetch-User: [?1]
Accept: [text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3]
Accept-Language: [zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7]

我使用的是 Chrome 瀏覽器,不一樣的瀏覽器添加的首部不徹底相同。

常見的首部有:

  • Accept:客戶端想要服務器發送的內容類型;
  • Accept-Charset:表示客戶端能接受的字符編碼;
  • Content-Length:請求主體的字節長度,通常在 POST/PUT 請求中較多;
  • Content-Type:當包含請求主體的時候,這個首部用於記錄主體內容的類型。在發送 POST 或 PUT 請求時,內容的類型默認爲x-www-form-urlecoded。可是在上傳文件時,應該設置類型爲multipart/form-data
  • User-Agent:用於描述發起請求的客戶端信息,如什麼瀏覽器。

Content-Length/Body

Content-Length表示請求體的字節長度,請求體的內容能夠從Body字段中讀取。細心的朋友可能發現了Body字段是一個io.ReadCloser接口。在讀取以後要關閉它,不然會有資源泄露。可使用defer簡化代碼編寫

func bodyHandler(w http.ResponseWriter, r *http.Request) {
    data := make([]byte, r.ContentLength)
    r.Body.Read(data) // 忽略錯誤處理
    defer r.Body.Close()
    
    fmt.Fprintln(w, string(data))
}

mux.HandleFunc("/body", bodyHandler)

上面代碼將客戶端傳來的請求體內容回傳給客戶端。還可使用io/ioutil包簡化讀取操做:

data, _ := ioutil.ReadAll(r.Body)

直接在瀏覽器中輸入 URL 發起的是GET請求,沒法攜帶請求體。有不少種方式能夠發起帶請求體的請求,下面介紹兩種:

使用表單

經過 HTML 的表單咱們能夠向服務器發送 POST 請求,將表單中的內容做爲請求體發送。

func indexHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, `
<html>
    <head>
        <title>Go Web 編程之 request</title>
    </head>
    <body>
        <form method="post" action="/body">
            <label for="username">用戶名:</label>
            <input type="text" id="username" name="username">
            <label for="email">郵箱:</label>
            <input type="text" id="email" name="email">
            <button type="submit">提交</button>
        </form>
    </body>
</html>
`)
}

mux.HandleFunc("/", indexHandler)

在 HTML 中使用form來顯示一個表單。點擊提交按鈕後,瀏覽器會發送一個 POST 請求到路徑/body上,將用戶名和郵箱做爲請求包體。

啓動服務器,進入主頁localhost:8080/,顯示錶單。填寫信息,點擊提交:

瀏覽器向服務器發送 POST 請求,URL 爲/bodybodyHandler處理完成後將包體回傳給客戶端。最後客戶端顯示:

上面的數據使用了x-www-form-urlencoded編碼,這是表單的默認編碼。後文還有詳述。

使用 Postman

Postman 是一款功能很是強大的 API 測試工具。

  • 支持 HTTP 協議的全部方法請求(GET/POST/PUT/DELETE)。
  • 能夠在請求中攜帶首部信息,請求體的內容;
  • 支持json/xml/http等各類格式的內容;
  • 界面友好。

接下來咱們看看如何使用 PostMan 測試咱們的bodyHandler

  • 黑色部分:選擇 HTTP 協議方法,這裏選擇 POST 以即可以攜帶請求體;
  • 綠色部分:請求的 URL;
  • 藍色部分:能夠設置請求的首部,請求體;
  • 淡紅色部分:請求體支持多種格式,這裏選擇原始格式;
  • 灰色部分:請求體的具體內容;
  • 紅色部分:發送以後顯示的響應信息,能夠查看響應首部,Cookie,響應體等。能夠看到是原樣返回。

獲取請求參數

上面咱們分析了 Go 中 HTTP 請求的常見字段。在實際開發中,客戶端一般須要在請求中傳遞一些參數。參數傳遞的方式通常有兩種方式:

  • URL 中的鍵值對,又叫查詢字符串,即 query string;
  • 表單。

下面依次來介紹。

URL 鍵值對

前文中介紹 URL 的通常格式時提到過,URL 的後面能夠跟一個可選的查詢字符串,以?與路徑分隔,形如key1=value1&key2=value2

URL 結構中有一個RawQuery字段。這個字段就是查詢字符串。

func queryHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, r.URL.RawQuery)
}

mux.HandleFunc("/query", queryHandler)

若是咱們以localhost:8080/query?name=dj&age=20請求,查詢字符串name=dj&age=20會原樣傳回客戶端。可是RawQuery是字符串類型的,使用字符串方法解析也能用,可是太麻煩了!!!

表單

表單狹義上說是經過表單發送請求,廣義上說能夠將數據放在請求體中發送到服務器。接下來咱們簡單編寫一個 HTML 頁面,經過頁面表單發送 HTTP 請求:

<html>
    <head>
        <title>Go Web 編程之 request</title>
    </head>
    
    <body>
        <form action="/form?lang=cpp&name=dj" method="post" enctype="application/x-www-form-urlencoded">
            <label>Form:</label>
            <input type="text" name="lang" />
            <input type="text" name="age" />
            <button type="submit">提交</button>
        </form>
    </body>
</html>
  • action表示提交表單時請求的 URL,method表示請求的方法。若是使用GET請求,因爲GET方法沒有請求體,參數將會拼接到 URL 尾部
  • enctype指定請求體的編碼方式,默認爲application/x-www-form-urlencoded。若是須要發送文件,必須指定爲multipart/form-data

咱們介紹一下什麼是urlencoded編碼。RFC 3986 中定義了 URL 中的保留字以及非保留字,全部保留字符都須要進行 URL 編碼。URL 編碼會把字符轉換成它在 ASCII 編碼中對應的字節值,接着把這個字節值表示爲一個兩位長的十六進制數字,最後在這個數字前面加上一個百分號(%)。例如空格的 ASCII 編碼爲 32,十六進制爲 20,故 URL 編碼爲%20

Form字段

使用x-www-form-urlencoded編碼的請求體,在處理時首先調用請求的ParseForm方法解析,而後從Form字段中取數據:

func formHandler(w http.ResponseWriter, r *http.Request) {
    r.ParseForm()
    fmt.Fprintln(w, r.Form)
}

mux.HandleFunc("/form", formHandler)

運行程序,驗證結果:

Form字段的類型url.Values底層其實是map[string][]string。調用ParseForm方法以後,可使用url.Values的方法操做數據。

使用ParseForm還能解析查詢字符串,將上面的表單改成:

<html>
    <head>
        <title>Go Web 編程之 request</title>
    </head>
    
    <body>
        <form action="/form?lang=cpp&name=dj" method="post" enctype="application/x-www-form-urlencoded">
            <label>Form:</label>
            <input type="text" name="lang" />
            <input type="text" name="age" />
            <button type="submit">提交</button>
        </form>
    </body>
</html>

請求結果:

能夠看出,查詢字符串中的鍵值對和表單中解析處理的合併到一塊兒了。同一個鍵下,表單值老是排在前面,如[golang cpp]

PostForm字段

若是一個請求,同時有 URL 鍵值對和表單數據,而用戶只想獲取表單數據,可使用PostForm字段。

使用PostForm只會返回表單數據,不包括 URL 鍵值。若是把上面的程序中,r.Form改成r.PostForm,那麼程序將顯示如下結果:

MultipartForm字段

若是要處理上傳的文件,那麼就必須使用multipart/form-data編碼。與以前的Form/PostForm相似,處理multipart/form-data編碼的請求時,也須要先解析後使用。只不過使用的方法不一樣,解析使用ParseMultipartForm,以後從MultipartForm字段取值。

<form action="/multipartform?lang=cpp&name=dj" method="post" enctype="multipart/form-data">
    <label>MultipartForm:</label>
    <input type="text" name="lang" />
    <input type="text" name="age" />
    <input type="file" name="uploaded" />
    <button type="submit">提交</button>
</form>
func multipartFormHandler(w http.ResponseWriter, r *http.Request) {
    r.ParseMultipartForm(1024)
    fmt.Fprintln(w, r.MultipartForm)
    
    fileHeader := r.MultipartForm.File["uploaded"][0]
    file, err := fileHeader.Open()
    if err != nil {
        fmt.Println("Open failed: ", err)
        return
    }

    data, err := ioutil.ReadAll(file)
    if err == nil {
        fmt.Fprintln(w, string(data))
    }
}

mux.HandleFunc("/multipartform", multipartFormHandler)

運行程序:

MultipartForm包含兩個map類型的字段,一個表示表單鍵值對,另外一個爲上傳的文件信息。

使用表單中文件控件名獲取MultipartForm.File獲得經過該控件上傳的文件,能夠是多個。獲得的是multipart.FileHeader類型,經過該類型能夠獲取文件的各個屬性。

須要注意的是,這種方式用來處理文件。爲了安全,ParseMultipartForm方法須要傳一個參數,表示最大使用內存,避免上傳的文件佔用空間過大。

FormValue/PostFormValue

爲了方便地獲取值,net/http包提供了FormValue/PostFormValue方法。它們在須要時會自動調用ParseForm/ParseMultipartForm方法。

FormValue方法返回請求的Form字段中指定鍵的值。若是同一個鍵對應多個值,那麼返回第一個。若是須要獲取所有值,直接使用Form字段。下面代碼將返回hello對應的第一個值:

fmt.Fprintln(w, r.FormValue("hello"))

PostFormValue方法返回請求的PostForm字段中指定鍵的值。若是同一個鍵對應多個值,那麼返回第一個。若是須要獲取所有值,直接使用PostForm字段

注意:
當編碼被指定爲multipart/form-data時,FormValue/PostFormValue將不會返回任何值,它們讀取的是Form/PostForm字段,而ParseMultipartForm將數據寫入MultipartForm字段。

其餘格式

經過 AJAX 之類的技術能夠發送其它格式的數據,例如application/json等。這種狀況下:

  • 首先經過首部Content-Type來獲知具體是什麼格式;
  • 經過r.Body讀取字節流;
  • 解碼使用。

總結

本文介紹了net/http包中請求的各方面內容。從Request結構到如何傳遞參數,最後介紹各類編碼的請求如何處理。

相關文章
相關標籤/搜索