Go 学习笔记(三十四)处理请求

本文原创地址:博客园骏马金龙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 请求并响应。

1.png

对于 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 的内容。

整个返回结果如下:

2.png

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 格式的数据。访问结果:

3.png

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
上一篇 Go 学习笔记(三十三)HttpRouter 路由
Go 学习笔记(目录)
下一篇 Go 学习笔记(三十五)Cookie