1. 程式人生 > >Go Web:處理請求

Go Web:處理請求

處理請求

Request和Response

http Requset和Response的內容包括以下幾項:

  1. Request or response line
  2. Zero or more headers
  3. An empty line, followed by …
  4. … an optional message body

例如一個http Request:

GET /Protocols/rfc2616/rfc2616.html HTTP/1.1
Host: www.w3.org
User-Agent: Mozilla/5.0
(empty line)

如果是POST方法,在empty line後還包含請求體。

一個http Response:

HTTP/1.1 200 OK
Content-type: text/html
Content-length: 24204
(empty line)
and then 24,204 bytes of HTML code

go http包分為兩種角色:http Client和http Server。http Client可以傳送請求,比如寫爬蟲程式時語言扮演的角色就是http Client;http Server用來提供web服務,可以處理http請求並響應。

對於Request,作為http客戶端(如編寫爬蟲類工具)常需要關注的是URL和User-Agent以及其它幾個Header;作為http服務端(web服務端,處理請求)常需要關注的幾項是:

URL
Header
Body
Form,、PostForm、MultipartForm

以下是完整的Request結構以及相關的函式、方法:混個眼熟就好了

type Request struct {
        Method string
        URL *url.URL
        Header Header
        Body io.ReadCloser
        GetBody func() (io.ReadCloser, error)  // Server: x, Cleint: √
        ContentLength int64
        TransferEncoding []string
        Close bool                 // Server: x, Cleint: √
        Host string
        Form url.Values
        PostForm url.Values
        MultipartForm *multipart.Form
        Trailer Header
        RemoteAddr string
        RequestURI string           // x
        TLS *tls.ConnectionState
        Cancel <-chan struct{}      // x
        Response *Response          // x
}

func NewRequest(method, url string, body io.Reader) (*Request, error)
func ReadRequest(b *bufio.Reader) (*Request, error)
func (r *Request) AddCookie(c *Cookie)
func (r *Request) BasicAuth() (username, password string, ok bool)
func (r *Request) Context() context.Context
func (r *Request) Cookie(name string) (*Cookie, error)
func (r *Request) Cookies() []*Cookie
func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error)
func (r *Request) FormValue(key string) string
func (r *Request) MultipartReader() (*multipart.Reader, error)
func (r *Request) ParseForm() error
func (r *Request) ParseMultipartForm(maxMemory int64) error
func (r *Request) PostFormValue(key string) string
func (r *Request) ProtoAtLeast(major, minor int) bool
func (r *Request) Referer() string
func (r *Request) SetBasicAuth(username, password string)
func (r *Request) UserAgent() string
func (r *Request) WithContext(ctx context.Context) *Request
func (r *Request) Write(w io.Writer) error
func (r *Request) WriteProxy(w io.Writer) error

注意有哪些欄位和方法,欄位的詳細說明見go doc http.Request。上面打了"x"的表示不需要了解的或者廢棄的。

有一個特殊的欄位Trailer,它是Header型別的,顯然它存放的是一個個請求header,它表示請求傳送完成之後再發送的額外的header。對於Server來說,讀取了request.Body之後才會讀取Trailer。很少有瀏覽器支援HTTP Trailer功能。

以下是完整的Response結構以及相關的函式、方法:混個眼熟就好了

type Response struct {
        Status     string // e.g. "200 OK"
        StatusCode int    // e.g. 200
        Proto      string // e.g. "HTTP/1.0"
        ProtoMajor int    // e.g. 1
        ProtoMinor int    // e.g. 0
        Header Header
        Body io.ReadCloser
        ContentLength int64
        TransferEncoding []string
        Close bool
        Uncompressed bool
        Trailer Header
        Request *Request
        TLS *tls.ConnectionState
}

func Get(url string) (resp *Response, err error)
func Head(url string) (resp *Response, err error)
func Post(url string, contentType string, body io.Reader) (resp *Response, err error)
func PostForm(url string, data url.Values) (resp *Response, err error)
func ReadResponse(r *bufio.Reader, req *Request) (*Response, error)
func (r *Response) Cookies() []*Cookie
func (r *Response) Location() (*url.URL, error)
func (r *Response) ProtoAtLeast(major, minor int) bool
func (r *Response) Write(w io.Writer) error

其實有些直接從字面意思看就知道了。

URL

內容太多,見:Go Web:URLs

Http Header

Request和Response結構中都有Header欄位,Header是一個map結構。

type Header map[string][]string
    A Header represents the key-value pairs in an HTTP header.

func (h Header) Add(key, value string)
func (h Header) Del(key string)
func (h Header) Get(key string) string
func (h Header) Set(key, value string)
func (h Header) Write(w io.Writer) error
func (h Header) WriteSubset(w io.Writer, exclude map[string]bool) error

key是Header欄位名,value是Header欄位的值,同個欄位多個值放在string的slice中。

Add()、Del()、Get()、Set()意義都很明確。

Write()是將Header寫進Writer中,比如從網路連線中傳送出去。WriteSubSet()和Write()類似,但可以指定exclude[headerkey]==true排除不寫的欄位。

下面是一個示例:

package main

import (
    "fmt"
    "net/http"
)

func headers(w http.ResponseWriter, r *http.Request) {
    for key := range r.Header {
        fmt.Fprintf(w, "%s: %s\n", key, r.Header[key])
    }
    fmt.Fprintf(w, "--------------\n")
    fmt.Fprintf(w, "the key: %s\n", r.Header["Accept-Encoding"])
    fmt.Fprintf(w, "the key: %s\n", r.Header.Get("Accept-Encoding"))
}
func main() {
    server := http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/headers", headers)
    server.ListenAndServe()
}

瀏覽器中訪問http://127.0.0.1:8080/headers的結果:

Connection: [keep-alive]
Cache-Control: [max-age=0]
Upgrade-Insecure-Requests: [1]
User-Agent: [Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 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]
Accept-Language: [zh-CN,zh;q=0.9,en;q=0.8]
--------------
the key: [gzip, deflate, br]
the key: gzip, deflate, br

Http Body

Request和Response結構中都有Body欄位,它們都是io.ReadCloser介面型別。從名字可以看出,io.ReadCloser由兩個介面組成:Reader和Closer,意味著它實現了Reader介面的Read()方法,也實現了Closer介面的Close()方法。這意味著Body的例項可以呼叫Read()方法,也可以呼叫Close()方法。

例如,下面寫一個handler,從請求中讀取Body並輸出:

package main

import (
    "fmt"
    "net/http"
)

func body(w http.ResponseWriter, r *http.Request) {
    len := r.ContentLength
    body := make([]byte, len)
    r.Body.Read(body)
    fmt.Fprintf(w, "%s\n", string(body))
}

func main() {
    server := http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/body", body)
    server.ListenAndServe()
}

因為使用HTTP Get方法的Request沒有Body,所以這裡使用curl的"-d"選項來構造一個POST請求,併發送Request Body:

$ curl -id "name=lognshuai&age=23" 127.0.0.1:8080/body
HTTP/1.1 200 OK
Date: Mon, 26 Nov 2018 09:04:40 GMT
Content-Length: 22
Content-Type: text/plain; charset=utf-8

name=lognshuai&age=23

Go和HTML Form

在Request結構中,有3個和form有關的欄位:

// Form欄位包含了解析後的form資料,包括URL的query、POST/PUT提交的form資料
// 該欄位只有在呼叫了ParseForm()之後才有資料
Form url.Values

// PostForm欄位不包含URL的query,只包括POST/PATCH/PUT提交的form資料
// 該欄位只有在呼叫了ParseForm()之後才有資料
PostForm url.Values // Go 1.1

// MultipartForm欄位包含multipart form的資料
// 該欄位只有在呼叫了ParseMultipartForm()之後才有資料
MultipartForm *multipart.Form

所以,一般的邏輯是:

  1. 先呼叫ParseForm()或ParseMultipartForm()解析請求中的資料
  2. 按需訪問Request結構中的Form、PostForm或MultipartForm欄位

除了先解析再訪問欄位的方式,還可以直接使用Request的方法:

  • FormValue(key)
  • PostFormValue(key)

稍後解釋這兩個方法。

取Form和PostForm欄位

給定一個html檔案,這個html檔案裡是form表單:

<html>
  <head>    
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Go Web</title>
  </head>
  <body>
    <form action=http://127.0.0.1:8080/process?name=xiaofang&boyfriend=longshuai
      method="post" enctype="application/x-www-form-urlencoded">
      <input type="text" name="name" value="longshuai"/>
      <input type="text" name="age" value="23"/>
      <input type="submit"/>
    </form>
  </body>
</html>

在這個form裡,action指定了要訪問的url,其中path=process,query包含name和boyfriend兩個key。除此之外,form表單的input屬性裡,也定義了name和age兩個key,由於method為post,這兩個key是作為request body傳送的,且因為enctype指定為application/x-www-form-urlencoded,這兩個key會按照URL編碼的格式進行組織。

下面是web handler的程式碼:

package main

import (
    "fmt"
    "net/http"
)

func form(w http.ResponseWriter, r *http.Request) {
    r.ParseForm()
    fmt.Fprintf(w, "%s\n", r.Form)
    fmt.Fprintf(w, "%s\n", r.PostForm)
}

func main() {
    server := http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/process", form)
    server.ListenAndServe()
}

上面先使用ParseForm()方法解析Form,再訪問Request中的Form欄位和PostForm欄位。

開啟前面的Html檔案,點選"提交"後,將輸出:

map[name:[longshuai xiaofang] age:[23] boyfriend:[longshuai]]
map[name:[longshuai] age:[23]]

如果這時,將application/x-www-form-urlencoded改成multipart/form-data,再點選提交,將輸出:

map[name:[xiaofang] boyfriend:[longshuai]]
map[]

顯然,使用multipart/form-data編碼form的時候,編碼的內容沒有放進Form和PostForm欄位中,或者說編碼的結果沒法放進這兩個欄位中。

取MultipartForm欄位

要取MultipartForm欄位的資料,先使用ParseMultipartForm()方法解析Form,解析時會讀取所有資料,但需要指定儲存在記憶體中的最大位元組數,剩餘的位元組數會儲存在臨時磁碟檔案中。

package main

import (
    "fmt"
    "net/http"
)

func form(w http.ResponseWriter, r *http.Request) {
    r.ParseMultipartForm(1024)
    fmt.Fprintf(w,"%s\n",r.Form)
    fmt.Fprintf(w,"%s\n",r.PostForm)
    fmt.Fprintf(w,"%s\n",r.MultipartForm)

}

func main() {
    server := http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/process", form)
    server.ListenAndServe()
}

將html檔案的enctype改為multipart/form-data後,重新點開html檔案,將輸出:

map[name:[xiaofang longshuai] boyfriend:[longshuai] age:[23]]
map[name:[longshuai] age:[23]]
&{map[name:[longshuai] age:[23]] map[]}

前兩行結果意味著ParseMultipartForm()方法也呼叫了ParseForm()方法,使得除了設定MultipartForm欄位,也會設定Form欄位和PostForm欄位。

注意上面的第三行,返回的是一個struct,這個struct中有兩個map,第一個map是來自form的key/value,第二個map為空,這個見後面的File。

最後還需注意的是,enctype=multipart/form-dataenctype=application/x-www-form-urlencoded時,Request.Form欄位中key的儲存順序是不一致的:

// application/x-www-form-urlencoded
map[name:[longshuai xiaofang] age:[23] boyfriend:[longshuai]]

// multipart/form-data
map[name:[xiaofang longshuai] boyfriend:[longshuai] age:[23]]

FormValue()和PostFormValue()

前面都是先呼叫ParseForm()或ParseMultipartForm()解析Form後再呼叫Request中對應欄位的。還可以直接呼叫FormValue()或PostFormValue()方法。

  • FormValue(key)
  • PostFormValue(key)

這兩個方法在需要時會自動呼叫ParseForm()或ParseMultipartForm(),所以使用這兩個方法取Form資料的時候,可以不用顯式解析Form。

FormValue()返回form資料和url query組合後的第一個值。要取得完整的值,還是需要訪問Request.Form或Request.PostForm欄位。但因為FormValue()已經解析過Form了,所以無需再顯式呼叫ParseForm()再訪問request中Form相關欄位。

PostFormValue()返回form資料的第一個值,因為它只能訪問form資料,所以忽略URL的query部分。

先看FormValue()方法的使用。注意,下面呼叫了FormValue()之後沒有呼叫ParseForm()和ParseMultipartForm()解析Form,就可以直接訪問Request中和Form相關的3個欄位。

package main

import (
    "fmt"
    "net/http"
)

func form(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w,"%s\n",r.FormValue("name"))
    fmt.Fprintf(w,"%s\n",r.FormValue("age"))
    fmt.Fprintf(w,"%s\n",r.FormValue("boyfriend"))
    fmt.Fprintf(w,"%s\n",r.Form)
    fmt.Fprintf(w,"%s\n",r.PostForm)
    fmt.Fprintf(w,"%s\n",r.MultipartForm)
}

func main() {
    server := http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/process", form)
    server.ListenAndServe()
}

enctype=multipart/form-data時,會自動呼叫ParseMultipartForm(),輸出結果:

xiaofang
23
longshuai
map[name:[xiaofang longshuai] boyfriend:[longshuai] age:[23]]
map[name:[longshuai] age:[23]]
&{map[name:[longshuai] age:[23]] map[]}

enctype=application/x-www-form-urlencoded時,會自動呼叫ParseForm(),輸出結果:

longshuai
23
longshuai
map[name:[longshuai xiaofang] age:[23] boyfriend:[longshuai]]
map[name:[longshuai] age:[23]]
%!s(*multipart.Form=<nil>)

仍然注意,因為兩種enctype導致的Request.Form儲存key時的順序不一致,使得訪問有多個值的key得到的結果不一致。

再看PostFormValue()方法的使用。

package main

import (
    "fmt"
    "net/http"
)

func form(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w,"%s\n",r.PostFormValue("name"))
    fmt.Fprintf(w,"%s\n",r.PostFormValue("age"))
    fmt.Fprintf(w,"%s\n",r.PostFormValue("boyfriend"))
    fmt.Fprintf(w,"%s\n",r.Form)
    fmt.Fprintf(w,"%s\n",r.PostForm)
    fmt.Fprintf(w,"%s\n",r.MultipartForm)
}

func main() {
    server := http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/process", form)
    server.ListenAndServe()
}

enctype=multipart/form-data時,會自動呼叫ParseMultipartForm(),輸出結果:

longshuai
23

map[name:[xiaofang longshuai] boyfriend:[longshuai] age:[23]]
map[name:[longshuai] age:[23]]
&{map[name:[longshuai] age:[23]] map[]}

enctype=application/x-www-form-urlencoded時,會自動呼叫ParseForm(),輸出結果:

longshuai
23

map[age:[23] boyfriend:[longshuai] name:[longshuai xiaofang]]
map[name:[longshuai] age:[23]]
%!s(*multipart.Form=<nil>)

注意,由於PostFormValue()方法只能訪問form資料,上面呼叫了PostFormValue()後,無法使用PostFormValue()訪問URL中的query的Key/value,儘管request中的欄位都合理設定了。

Files

multipart/form-data最常用的場景可能是上傳檔案,比如在form中使用file標籤。以下是html檔案:

<html>
  <head>    
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Go Web Programming</title>
  </head>
  <body>
    <form action=http://127.0.0.1:8080/process?name=xiaofang&boyfriend=longshuai
      method="post" enctype="multipart/form-data">
      <input type="text" name="name" value="longshuai"/>
      <input type="text" name="age" value="23"/>
      <input type="file" name="file_to_upload">
      <input type="submit"/>
    </form>
  </body>
</html>

下面是服務端接收上傳檔案資料的程式碼:

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
)

func form(w http.ResponseWriter, r *http.Request) {
    r.ParseMultipartForm(1024)
    fileHeader := r.MultipartForm.File["file_to_upload"][0]
    file, err := fileHeader.Open()
    if err == nil {
        dataFromFile, err := ioutil.ReadAll(file)
        if err == nil {
            fmt.Fprintf(w, "%s\n", dataFromFile)
        }
    }
    fmt.Fprintf(w, "%s\n", r.MultipartForm)
}

func main() {
    server := http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/process", form)
    server.ListenAndServe()
}

上面先呼叫ParseMultipartForm()解析multipart form,然後訪問request的MultipartForm欄位,這個欄位的型別是*multipart.Form,該型別定義在mime/multipart/formdata.go檔案中:

$ go doc multipart.Form
package multipart // import "mime/multipart"

type Form struct {
        Value map[string][]string
        File  map[string][]*FileHeader
}

Form型別表示解析後的multipart form,欄位File和Value都是map型別的,其中File的map value是*FileHeader型別:

$ go doc multipart.fileheader
package multipart // import "mime/multipart"

type FileHeader struct {
        Filename string
        Header   textproto.MIMEHeader
        Size     int64

        // Has unexported fields.
}
    A FileHeader describes a file part of a multipart request.


func (fh *FileHeader) Open() (File, error)

它實現了Open()方法,所以可以直接呼叫Open()來開啟multipart.Form的File部分。即:

fileHeader := r.MultipartForm.File["file_to_upload"][0]
file, err := fileHeader.Open()

然後讀取這段資料,響應給客戶端。注意,有了File後,request.MultipartForm欄位的第二個map就有了值,第二個map對應的就是multipart.Form.File的內容。

整個返回結果如下:

FormFile()

類似於FormValue()和PostFormValue()方法的便捷,讀取multipart.Form也有快捷方式:

$ go doc http.formfile
func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error)
    FormFile returns the first file for the provided form key. FormFile calls
    ParseMultipartForm and ParseForm if necessary.

FormFile()方法會在需要的時候自動呼叫parseMultipartForm()或ParseForm()。注意它的返回值。因為第一個返回值為multipart.File,說明至少實現了io.Reader介面,可以直接讀取這個檔案。

修改上一節的示例:

func form(w http.ResponseWriter, r *http.Request) {
    file, _, err := r.FormFile("file_to_upload")

    if err != nil {
        panic(err)
    }
    dataFromFile, err := ioutil.ReadAll(file)
    if err != nil {
        panic(err)
    }
    fmt.Fprintf(w, "%s\n", dataFromFile)
}

ResponseWriter

ResponseWriter介面用於傳送響應資料、響應header。它有3個方法:

type ResponseWriter interface {
    Header() Header
    Write([]byte) (int, error)
    WriteHeader(statusCode int)
}
    A ResponseWriter interface is used by an HTTP handler to construct an HTTP
    response.

    A ResponseWriter may not be used after the Handler.ServeHTTP method has
    returned.

Header()用於構造response header,構造好的header會在稍後自動被WriteHeader()傳送出去。比如設定一個Location欄位:

w.Header().Set("Location", "http://google.com")

Write()用於傳送響應資料,例如傳送html格式的資料,json格式的資料等。

str := `<html>
<head><title>Go Web Programming</title></head>
<body><h1>Hello World</h1></body>
</html>`
w.Write([]byte(str))

WriteHeader(int)可以接一個數值HTTP狀態碼,同時它會將構造好的Header自動傳送出去。如果不顯式呼叫WriteHeader(),會自動隱式呼叫併發送200 OK。

下面是一個示例:

package main

import (
    "fmt"
    "encoding/json"
    "net/http"
)

func commonWrite(w http.ResponseWriter, r *http.Request) {
    str := `<html>
        <head>
            <title>Go Web</title>
        </head>
        <body>
            <h1>Hello World</h1>
        </body>
    </html>`
    w.Write([]byte(str))
}

func writeHeader(w http.ResponseWriter,r *http.Request){
    w.WriteHeader(501)
    fmt.Fprintln(w,"not implemented service")
}

func header(w http.ResponseWriter,r *http.Request){
    w.Header().Set("Location","http://www.baidu.com")
    w.WriteHeader(302)
}

type User struct {
    Name    string
    Friends []string
}

func jsonWrite(w http.ResponseWriter, r *http.Request) {
    var user = &User{
        Name:    "longshuai",
        Friends: []string{"personA", "personB", "personC"},
    }
    w.Header().Set("Content-Type", "application/json")
    jsonData, _ := json.Marshal(user)
    w.Write(jsonData)
}

func main() {
    server := http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/commonwrite", commonWrite)
    http.HandleFunc("/writeheader", writeHeader)
    http.HandleFunc("/header", header)
    http.HandleFunc("/jsonwrite", jsonWrite)
    server.ListenAndServe()
}

commonWrite()這個handler用於輸出帶html格式的資料。訪問結果:

writeheader()這個handler用於顯式傳送501狀態碼。訪問結果:

$ curl -i 127.0.0.1:8080/writeheader
HTTP/1.1 501 Not Implemented
Date: Tue, 27 Nov 2018 03:36:57 GMT
Content-Length: 24
Content-Type: text/plain; charset=utf-8

not implemented service

header()這個handler用於設定響應的Header,這裡設定了302重定向,客戶端收到302狀態碼後會找Location欄位的值,然後重定向到http://www.baidu.com

jsonWrite()這個handler用於傳送json資料,所以傳送之前先設定了Content-Type: application/json