解析客户端参数
1.发送简单键值对
curl -H "Content-Type: application/x-www-form-urlencoded" \
-d id=100 \
-d name=golang \
localhost/books
这时curl会把数据拼接为id=100&name=golang
形式放入请求体中,不对数据进行编码,意味着如果数据中含有& ? = @
等符号时,服务端将无法解析出键值对。
服务端解析
服务端收到请求后发现内容类型为application/x-www-form-urlencoded
,所以当r.ParseForm()
函数被执行时,它会将r.Body
中的数据解析为键值对形式,并保存在map结构中,以便使用r.FormValue()
函数获取。
func books(w http.ResponseWriter, r *http.Request) {
id := r.FormValue("id")
name := r.FormValue("name")
}
2.发送编码的键值对
curl -H "Content-Type: application/x-www-form-urlencoded" \
localhost/books
--data-urlencode 'id=100' \
--data-urlencode "bookContent=`</tmp/book.txt`"
这时curl同样会将所有键值对用&
符号拼接起来,不同的是它会将所有值部分编码为%39%AC+%0B
形式,也就是用三个字节代表一个字节,这样服务端就不会出现解析不了的问题,如果有多个键值对,必须用多个--data-urlencode
标识。
服务端解析
服务端收到请求后发现内容类型为application/x-www-form-urlencoded
,所以当r.ParseForm()
函数被执行时,它会将r.Body
中的数据解析为键值对形式,同时所有数据会被解码为原始数据(其实在上一种情况中,数据同样会被解码一次,但解码后的数据与解码前是一样的),然后保存在map结构中,以便使用r.FormValue()
函数获取。
func books(w http.ResponseWriter, r *http.Request) {
id := r.FormValue("id")
bookContent := r.FormValue("bookContent")
}
3.发送文件中的数据
curl -H "Content-Type: text/plain" \
--data-binary @file.txt \
'localhost/books?id=100&name=book1'
这时我们并不希望我们的数据被服务端当做键对来解析,所以应该用其它字段值来替换application/x-www-form-urlencoded
,比如我们设置为text/plain
或application/json
都是可以的,这里没有用-d
是因为-d
会默认去掉文件内容中所有的\n
和\r
,然后放入请求体中,显然我们并不希望数据被偷偷改掉,所以应该用--data-binary
选项来代替-d
。
服务端解析
服务端收到请求后发现内容类型为text/plain
,这时你仍可以执行r.ParseForm()
函数,但它只会解析query
中的参数而不会解析r.Body
中的数据。
func books(w http.ResponseWriter, r *http.Request) {
id := r.FormValue("id")
name := r.FormValue("name")
data, _ := ioutil.ReadAll(r.Body)
}
4.发送字符串
curl -H "Content-Type: application/json" \
-d '{"k1": "v1"}' \
localhost/books
这时数据被直接放入请求体,它与text/plain
的行为是一样的。
服务端解析
处理方式与前一种相同
func books(w http.ResponseWriter, r *http.Request) {
data, _ := ioutil.ReadAll(r.Body)
}
5.发送文件
curl localhost/books \
-F book1=@/tmp/book1.txt \
-F book2=@/tmp/book2.txt
这时每一个-F
数据块的前后都会被加上一个含有随机串的标识符:--------------------------b6c4d1ab01136e11--
,然后放入请求体中,且Content-Type
会被设置为multipart/form-data; boundary=------------------------b6c4d1ab01136e11
。
服务端解析
服务端发现内容类型为multipart/form-data
,会读出紧跟在后面的标识符,然后将r.Body
中的数据用该标识符切分成开,切分开的每个数据块中都含有一个name
字段,这时我们就可以用该name
获取相应的数据了。
func books(w http.ResponseWriter, r *http.Request) {
book1, book1Meta, err := r.FormFile("book1")
book2, book2Meta, err := r.FormFile("book2")
...
}
总结
当服务器端收到一个HTTP请求时,需要从原始的二进制数据中解析出客户端参数,这些参数大概可以分三个部分。
请求方法与URL
这两个部分都可以自定义,虽然HTTP协议标准定义了几种请求类型,如:POST
、GET
、DELETE
、PATCH
等,但大多数编程语言中并没有硬性规定,所以如果你喜欢的话,可以使用任何字符串来代替POST
。URL后面的query参数会被服解析出来放在map结构中,golang的http库还会对URL进行安全检查,比如不能含有..
。
请求头
这里的参数都是键值对形式,用户可以添加自定义的请求头,并且也没有限制。但有一个常用的请求头:Content-Type
,它标识了请求体的数据格式,服务器端在收到请求后,会根据该字段来解析请求体中的数据,HTTP协议标准为这个字段预定义了几十种值,但我们常用的只有几种:application/x-www-form-urlencoded
、multipart/form-data
、application/json
、text/plain
等。
请求体
这里是最主要的存放数据的地方,服务端收到请求后,先判断请求头Content-Type
的值:
- 如果是
application/x-www-form-urlencoded
,则认为请求本中的数据为key=value
形式,对数据进行解码然后保存在map中;
- 如果是
multipart/form-data
,则用符识符对数据进行切分,然后保存在map中;
- 如果是
application/json
或text/plain
等类型,则不对请求体进行处理。