在了解了使用net/http
构建go web服务之后,这篇文章深入了解一下在Go中如何处理一个请求。
1. 请求包含什么
HTTP Message有Request和Response两种。这里详细看看Request Message,一个请求消息的格式如下:
method request-URL version
headers
entity-body
Go中的net/http
包提供了一个用于表示HTTP请求消息的结构Reqeust
。Request
将上面请求消息的内容经过分析后存储在不同的字段中,还包括一系列有用的方法。主要的字段有:
- URL
- Header
- Body
- Form, PostForm, MultipartForm
除了这些字段,还可以使用Request
的一些方法处理请求中的cookie等。
2. URL
Request
中的URL
字段可以存储请求中URL的信息,在Reqeuest
中的URL
字段是一个指向net/url
包中URL
结构的指针,URL
结构主要字段如下:
type URL struct {
Scheme string
User *Userinfo // username and password information
Host string // host or host:port
Path string // path (relative paths may omit leading slash)
RawPath string // encoded path hint (see EscapedPath method)
RawQuery string // encoded query values, without '?'
Fragment string // fragment for references, without '#'
}
URL的一般格式是这样的:
scheme://[userinfo@]host/path[?query][#fragment]
其中[]中的是可选的信息。
在net/url
包中有解析URL的函数Parse()
和ParseRequestURI()
,两者都是将传进来的URL解析成一个URL
结构并返回这个URL
的指针。
解析之后的结果如下:
Field | Value |
---|---|
Scheme |
https |
Host |
book.douban.com |
Path |
/people/valineliu/collect |
RawQuery |
start=0&sort=time&rating=all&filter=all&mode=list |
Fragment |
here |
Parse()
和ParseRequestURI()
不解析RawPath
字段,需要调用URL.EscapedPath()
函数来获得这个字段值。
RawQuery
字段包含了没有解析的查询参数,可以通过对这个参数的解析获得URL中的查询参数,不过可以直接使用Request
结构中的Form
字段来获取,后面再详细介绍。
还有一个需要注意的问题就是,如果请求是通过浏览器发送的话,程序就得不到Fragment
字段,因为浏览器会截断Fragment
,与net/http
库无关。
3. Header
HTTP请求和响应消息都有一个Header结构,在go中,net/http
包中有一个Header
结构来存储Header结构。
Header
本质上就是一个map
,定义如下:
type Header map[string][]string
不过值是一个[]string
,也就是说,对一个key可以有多个值。
Header
有四个基本方法,可以方便地对内容进行操作,分别是Add
, Del
, Set
, Get
。其中Set
方法会对已有的值进行覆盖。
除了Get
方法外,还可以直接访问Header
中某个key的值:
fmt.Println(r.Header["Accept-Encoding"])
就像访问一个map
一样。这种方式和Get
的区别在于,直接访问得到的是一个字符串切片,而使用Get
方法得到的是所有的值用逗号分割的字符串:
fmt.Println(r.Header["Accept-Encoding"])
fmt.Println(r.Header.Get("Accept-Encoding"))
结果:
[gzip, deflate, br]
gzip, deflate, br
4. Body
Request
中的Body
不仅是请求的Body,也是响应的Body,这里不展开描述。
Body
是一个io.ReaderCloser
接口,意味着可以调用Read
和Close
两个方法。下面的例子描述了如何读取请求中的Body:
length := r.ContentLength
body := make([]byte, length)
r.Body.Read(body)
fmt.Println(string(body))
Request
中有一个ContentLength
字段,这个字段记录了Body的长度,可以通过这个字段指定长度来读取Body中的内容。
GET请求没有Body,所以需要一个POST请求,可以使用curl:
curl -id "start=30&sort=time&rating=all&filter=all&mode=list" localhost:8080/world
就可以得到Body了:
start=30&sort=time&rating=all&filter=all&mode=list
5. Form, PostForm & MultipartForm
前面几个字段在请求达到服务器的时候就通过一定的流程解析出来了,而Request
中的这几个关于Form的字段不是自动解析的,需要手动调用各自对应的函数来解析。
5.1 概述
这几个字段的定义如下:
Form url.Values
PostForm url.Values
MultipartForm *multipart.Form
其中Form
和PostForm
类型一样:
type Values map[string][]string
也就是一个map
,key是字符串,而值是一个字符串切片。即,对于一个key来说,可以有多个值。
MultipartForm
的类型不一样:
type Form struct {
Value map[string][]string
File map[string][]*FileHeader
}
多了一个File
字段,这是因为MultipartForm
还需要存储文件。
请求中的参数可以在URL中,也就是这种格式:/people/valineliu/collect?start=30&sort=time
,也可以在Body中,比如:curl -id "start=40" localhost:8080/world
。同时请求消息中Body中的数据可以通过不同的格式编码发送给服务器。其中常用的有application/x-www-form-urlencoded
和multipart/form-data
。前一种格式可以将Body中的参数按照URL中参数的格式编码,而后者会将数据编码成一条MIME报文。
这就导致,三种Form字段在不同的编码方式下能够获得的数据也是不一样的。
5.2 Form & PostForm
为了获得Form
和PostForm
字段,需要调用ParseForm
方法:
func (r *Request) ParseForm() error {
var err error
if r.PostForm == nil {
if r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH" {
r.PostForm, err = parsePostForm(r)
}
if r.PostForm == nil {
r.PostForm = make(url.Values)
}
}
if r.Form == nil {
if len(r.PostForm) > 0 {
r.Form = make(url.Values)
copyValues(r.Form, r.PostForm)
}
var newValues url.Values
if r.URL != nil {
var e error
newValues, e = url.ParseQuery(r.URL.RawQuery)
if err == nil {
err = e
}
}
if newValues == nil {
newValues = make(url.Values)
}
if r.Form == nil {
r.Form = newValues
} else {
copyValues(r.Form, newValues)
}
}
return err
}
这个函数是幂等的,也就是多次调用效果一样。从这个解析过程可以发现:
- 函数首先解析
PostForm
字段,然后解析Form
字段; - 而在
parsePostForm()
函数中,只对Body中application/x-www-form-urlencoded
编码的参数解析,其余编码格式下PostForm
字段为空; Form
中包含PostForm
中的信息,而且PostForm
中的信息优先级高;- URL中的参数只会到被解析到
Form
字段中。
下面的例子演示了上面的分析:
r.ParseForm()
fmt.Fprintln(w, r.Form)
fmt.Fprintln(w, r.PostForm)
使用curl模拟一个请求:
curl -id "start=30&mode=list" "localhost:8080/world?start=40&sort=time"
结果:
map[mode:[list] sort:[time] start:[30 40]]
map[mode:[list] start:[30]]
第一个map
是Form
,第二个是PostForm
,从结果可以看到两者的区别。
5.3 MultipartForm
对于MultipartForm
,需要调用ParseMultipartForm
方法,这个方法会在需要的时候(也就是Form
和PostForm
为nil
的时候)调用ParseForm
方法先来解析Form
和PostForm
两个字段:
func (r *Request) ParseMultipartForm(maxMemory int64) error {
if r.MultipartForm == multipartByReader {
return errors.New("http: multipart handled by MultipartReader")
}
if r.Form == nil {
err := r.ParseForm()
if err != nil {
return err
}
}
if r.MultipartForm != nil {
return nil
}
mr, err := r.multipartReader(false)
if err != nil {
return err
}
f, err := mr.ReadForm(maxMemory)
if err != nil {
return err
}
if r.PostForm == nil {
r.PostForm = make(url.Values)
}
for k, v := range f.Value {
r.Form[k] = append(r.Form[k], v...)
// r.PostForm should also be populated. See Issue 9305.
r.PostForm[k] = append(r.PostForm[k], v...)
}
r.MultipartForm = f
return nil
}
同时需要一个最大读取字节数的参数。
从这个解析过程可以看到:
- 如果
Form
和PostForm
为nil
的话,这个函数就先调用ParseForm
来进行解析; - 不过如果编码格式是
multipart/form-data
的话,Form
中就只会有URL中的参数,而PostForm
中啥也没有(暂时); ParseMultipartForm
方法会解析Body中multipart/form-data
格式的数据到MultipartForm
字段中;- 最后还会将Body中的参数添加到
Form
和PostForm
中。
例子:
r.ParseMultipartForm(1024)
fmt.Fprintln(w, r.Form)
fmt.Fprintln(w, r.PostForm)
fmt.Fprintln(w, r.MultipartForm)
使用curl模拟一个请求:
curl -F "start=30" -F "mode=list" -F upload=@hello "localhost:8080/world?start=40&sort=time"
其中前两个-F生成两个multipart/form-data
格式的参数,而后一个-F上传一个文件。结果:
map[mode:[list] sort:[time] start:[40 30]]
map[mode:[list] start:[30]]
&{map[mode:[list] start:[30]] map[upload:[0xc0000fa140]]}
前两个map
还是和之前的一样,最后一个就是MultipartForm
,这是两个map
,一个是Body中的参数,一个是上传的文件。
关于Form
和之前有一个不同,就是start
值的顺序。之前使用ParseForm
解析的时候30在前而现在40在前。从两个函数的代码中我们可以看到这个结果的原因。
5.4 Another Way
除了上面调用ParseForm
和ParseMultipartForm
函数外,还可以通过其它的方法获取值:FormValue()
和PostFormValue()
。两个方法的定义如下:
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 ""
}
func (r *Request) PostFormValue(key string) string {
if r.PostForm == nil {
r.ParseMultipartForm(defaultMaxMemory)
}
if vs := r.PostForm[key]; len(vs) > 0 {
return vs[0]
}
return ""
}
这两个函数都会在必要的时候调用ParseMultipartForm
函数来进行解析。和前面的方法不同在于,这两个方法返回的是对应key的第一个value值,而不是一个字符串切片。
例子:
fmt.Fprintln(w, r.FormValue("start"))
fmt.Fprintln(w, r.PostFormValue("start"))
结果:
40
30
这个结果很奇怪是吧,所以对于既在URL中也在Body中的参数,在获取的时候需要小心一些。最好的办法就是参数只出现在一个地方,要么URL中,要么Body中。
其实常用的编码还有application/json
,也就是读取的Body数据可以解析成一个Json结构,这也很常见,不过不在这里细说了。
To Be Continued…