所有文章

解析客户端参数

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/plainapplication/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协议标准定义了几种请求类型,如:POSTGETDELETEPATCH等,但大多数编程语言中并没有硬性规定,所以如果你喜欢的话,可以使用任何字符串来代替POST。URL后面的query参数会被服解析出来放在map结构中,golang的http库还会对URL进行安全检查,比如不能含有..

请求头

这里的参数都是键值对形式,用户可以添加自定义的请求头,并且也没有限制。但有一个常用的请求头:Content-Type,它标识了请求体的数据格式,服务器端在收到请求后,会根据该字段来解析请求体中的数据,HTTP协议标准为这个字段预定义了几十种值,但我们常用的只有几种:application/x-www-form-urlencodedmultipart/form-dataapplication/jsontext/plain等。

请求体

这里是最主要的存放数据的地方,服务端收到请求后,先判断请求头Content-Type的值:

  1. 如果是application/x-www-form-urlencoded,则认为请求本中的数据为key=value形式,对数据进行解码然后保存在map中;
  2. 如果是multipart/form-data,则用符识符对数据进行切分,然后保存在map中;
  3. 如果是application/jsontext/plain等类型,则不对请求体进行处理。

编写日期:2019-03-02