解析客户端参数#
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等类型,则不对请求体进行处理。