Http请求
最后更新于:2022-04-02 04:57:10
### 请求的结构
HTTP的交互以请求和响应的应答模式。go的请求我们早就见过了,handler函数的第二个参数http.Requests。其结构为:
~~~
type Request struct {
Method string
URL *url.URL
Proto string // "HTTP/1.0"
ProtoMajor int // 1
ProtoMinor int // 0
Header Header
Body io.ReadCloser
ContentLength int64
TransferEncoding []string
Close bool
Host string
Form url.Values
PostForm url.Values
MultipartForm *multipart.Form
....
ctx context.Context
}
~~~
从request结构可以看到,http请求的基本信息都囊括了。对于请求而言,主要关注一下请求的URL,Method,Header,Body这些结构。
### URL
HTTP的url请求格式为`scheme://[userinfo@]host/path[?query][#fragment]`, go的提供了一个URL结构,用来映射HTTP的请求URL。
~~~
type URL struct {
Scheme string
Opaque string
User *Userinfo
Host string
Path string
RawQuery string
Fragment string
}
~~~
URL的格式比较明确,其实更好的名词应该是URI,统一资源定位。url中比较重要的是查询字符串query。通常作为get请求的参数。query是一些使用`&`符号分割的`key1=value1&key2=value2`键值对,由于url编码是ASSIC码,因此query需要进行urlencode。go可以通过request.URI.RawQuery读取query
~~~
func indexHandler(w http.ResponseWriter, r *http.Request) {
info := fmt.Sprintln("URL", r.URL, "HOST", r.Host, "Method", r.Method, "RequestURL", r.RequestURI, "RawQuery", r.URL.RawQuery)
fmt.Fprintln(w, info)
}
☁ ~ curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'name=vanyar&age=27' "http://127.0.0.1:8000?lang=zh&version=1.1.0"
URL /?lang=zh&version=1.1.0 HOST 127.0.0.1:8000 Method POST RequestURL /?lang=zh&version=1.1.0 RawQuery lang=zh&version=1.1.0
~~~
### header
header也是HTTP中重要的组成部分。Request结构中就有Header结构,Header本质上是一个map(`map[string][]string`)。将http协议的header的key-value进行映射成一个图:
~~~
Host: example.com
accept-encoding: gzip, deflate
Accept-Language: en-us
fOO: Bar
foo: two
Header = map[string][]string{
"Accept-Encoding": {"gzip, deflate"},
"Accept-Language": {"en-us"},
"Foo": {"Bar", "two"},
}
~~~
header中的字段包含了很多通信的设置,很多时候请求都需要指定`Content-Type`。
~~~
func indexHandler(w http.ResponseWriter, r *http.Request) {
info := fmt.Sprintln(r.Header.Get("Content-Type"))
fmt.Fprintln(w, info)
}
☁ ~ curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'name=vanyar&age=27' "http://127.0.0.1:8000?lang=zh&version=1.1.0"
application/x-www-form-urlencoded
~~~
> Golng 提供了不少打印函数,基本上分为三类三种。即 `Print` `Println` 和`Printf`。
> `Print`比较简单,打印输出到标准输出流,`Println`则也一样不同在于多打印一个换行符。至于`Printf`则是打印格式化字符串,三个方法都返回打印的bytes数。`Sprint`,`Sprinln`和`Sprintf`则返回打印的字符串,不会输出到标准流中。`Fprint`,`Fprintf`和`Fprinln`则把输出的结果打印输出到`io.Writer`接口中,http中则是`http.ReponseWriter`这个对象中,返回打印的bytes数。
### Body
http中数据通信,主要通过body传输。go把body封装成Request的Body,它是一个ReadCloser接口。接口方法Reader也是一个接口,后者有一个`Read(p []byte) (n int, err error)`方法,因此body可以通过读取byte数组获取请求的数据。
~~~
func indexHandler(w http.ResponseWriter, r *http.Request) {
info := fmt.Sprintln(r.Header.Get("Content-Type"))
len := r.ContentLength
body := make([]byte, len)
r.Body.Read(body)
fmt.Fprintln(w, info, string(body))
}
☁ ~ curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'name=vanyar&age=27' "http://127.0.0.1:8000?lang=zh&version=1.1.0"
application/x-www-form-urlencoded
name=vanyar&age=27
~~~
可见,当请求的content-type为application/x-www-form-urlencoded, body也是和query一样的格式,key-value的键值对。换成json的请求方式则如下:
~~~
☁ ~ curl -X POST -H "Content-Type: application/json" -d '{name: "vanyar", age: 27}' "http://127.0.0.1:8000?lang=zh&version=1.1.0"
application/json
{name: "vanyar", age: 27}
~~~
multipart/form-data的格式用来上传图片,请求的body如下:
~~~
☁ ~ curl -X POST -H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW" -F "name=vanyar" -F "age=27" "http://127.0.0.1:8000?lang=zh&version=1.1.0"
multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW; boundary=------------------------d07972c7800e4c23
--------------------------d07972c7800e4c23
Content-Disposition: form-data; name="name"
vanyar
--------------------------d07972c7800e4c23
Content-Disposition: form-data; name="age"
27
--------------------------d07972c7800e4c23--
~~~
### 表单
解析body可以读取客户端请求的数据。而这个数据是无论是键值对还是form-data数据,都比较原始。直接读取解析还是挺麻烦的。这些body数据通常也是表单提供。因此go提供处理这些表单数据的方法。
#### Form
go提供了ParseForm方法用来解析表单提供的数据,即content-type 为 x-www-form-urlencode的数据。
~~~
func indexHandler(w http.ResponseWriter, r *http.Request) {
contentType := fmt.Sprintln(r.Header.Get("Content-Type"))
r.ParseForm()
fromData := fmt.Sprintf("%#v", r.Form)
fmt.Fprintf(w, contentType, fromData)
}
☁ ~ curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'name=vanyar&age=27' "http://127.0.0.1:8000?lang=zh&version=1.1.0"
application/x-www-form-urlencoded
%!(EXTRA string=url.Values{"name":[]string{"vanyar"}, "age":[]string{"27"}, "lang":[]string{"zh"}, "version":[]string{"1.1.0"}})%
~~~
用来读取数据的结构和方法大致有下面几个:
~~~
fmt.Println(r.Form["lang"])
fmt.Println(r.PostForm["lang"])
fmt.Println(r.FormValue("lang"))
fmt.Println(r.PostFormValue("lang"))
~~~
其中`r.Form`和`r.PostForm`必须在调用`ParseForm`之后,才会有数据,否则则是空数组。
而`r.FormValue`和`r.PostFormValue("lang")`无需`ParseForm`的调用就能读取数据。
此外r.Form和r.PostForm都是数组结构,对于body和url都存在的同名参数,r.Form会有两个值,即 \["en", "zh"\],而带`POST`前缀的数组和方法,都只能读取body的数据。
~~~
☁ ~ curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'name=vanyar&age=27&lang=en' "http://127.0.0.1:8000?lang=zh&version=1.1.0"
application/x-www-form-urlencoded
%!(EXTRA string=url.Values{"version":[]string{"1.1.0"}, "name":[]string{"vanyar"}, "age":[]string{"27"}, "lang":[]string{"en", "zh"}})%
~~~
此时可以看到,lang参数不仅url的query提供了,post的body也提供了,go默认以body的数据优先,两者的数据都有,并不会覆盖。
如果不想读取url的参数,调用`PostForm`或`PostFormValue`读取字段的值即可。
~~~
r.PostForm["lang"][0]
r.PostFormValue["lang"]
~~~
对于form-data的格式的数据,ParseForm的方法只会解析url中的参数,并不会解析body中的参数。
~~~
☁ ~ curl -X POST -H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW" -F "name=vanyar" -F "age=27" -F "lang=en" "http://127.0.0.1:8000?lang=zh&version=1.1.0"
multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW; boundary=------------------------5f87d5bfa764488d
%!(EXTRA string=url.Values{"lang":[]string{"zh"}, "version":[]string{"1.1.0"}}
)%
~~~
因此当请求的content-type为form-data的时候,ParseFrom则需要改成 MutilpartFrom,否则r.From是读取不到body的内容,只能读取到query string中的内容。
#### MutilpartFrom
ParseMutilpartFrom方法需要提供一个读取数据长度的参数,然后使用同样的方法读取表单数据,MutilpartFrom只会读取body的数据,不会读取url的query数据。
~~~
func indexHandler(w http.ResponseWriter, r *http.Request) {
r.ParseMultipartForm(1024)
fmt.Println(r.Form["lang"])
fmt.Println(r.PostForm["lang"])
fmt.Println(r.FormValue("lang"))
fmt.Println(r.PostFormValue("lang"))
fmt.Println(r.MultipartForm.Value["lang"])
fmt.Fprintln(w, r.MultipartForm.Value)
}
~~~
可以看到请求之后返回 `map[name:[vanyar] age:[27] lang:[en]]`。即`r.MultipartForm.Value`并没有url中的参数。
总结一下,读取urlencode的编码方式,只需要ParseForm即可,读取form-data编码需要使用ParseMultipartForm方法。如果参数中既有url,又有body,From和FromValue方法都能读取。而带`Post`前缀的方法,只能读取body的数据内容。其中MultipartForm的数据通过`r.MultipartForm.Value`访问得到。
### 文件上传
form-data格式用得最多方式就是在图片上传的时候。`r.MultipartForm.Value`是post的body字段数据,`r.MultipartForm.File`则包含了图片数据:
~~~
func indexHandler(w http.ResponseWriter, r *http.Request) {
r.ParseMultipartForm(1024)
fileHeader := r.MultipartForm.File["file"][0]
fmt.Println(fileHeader)
file, err := fileHeader.Open()
if err == nil{
data, err := ioutil.ReadAll(file)
if err == nil{
fmt.Println(len(data))
fmt.Fprintln(w, string(data))
}
}
fmt.Println(err)
}
~~~
发出请求之后,可以看见返回了图片。当然,go提供了更好的工具函数`r.FormFile`,直接读取上传文件数据。而不需要再使用 ParseMultipartForm 方法。
~~~
file, _, err := r.FormFile("file")
if err == nil{
data, err := ioutil.ReadAll(file)
if err == nil{
fmt.Println(len(data))
fmt.Fprintln(w, string(data))
}
}
fmt.Println(err)
~~~
这种情况只适用于出了文件字段没有其他字段的时候,如果仍然需要读取lang参数,还是需要加上 ParseMultipartForm 调用的。读取到了上传文件,接下来就是很普通的写文件的io操作了。
### JSON
现在流行前后端分离,客户端兴起了一些框架,angular,vue,react等提交的数据,通常习惯为json的格式。对于json格式,body就是原生的json字串。也就是go解密json为go的数据结构。
~~~
type Person struct {
Name string
Age int
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
decode := json.NewDecoder(r.Body)
var p Person
err := decode.Decode(&p)
if err != nil{
log.Fatalln(err)
}
info := fmt.Sprintf("%T\n%#v\n", p, p)
fmt.Fprintln(w, info)
}
☁ ~ curl -X POST -H "Content-Type: application/json" -d '{"name": "vanyar", "age": 27 }' "http://127.0.0.1:8000?lang=zh&version=1.1.0"
main.Person
main.Person{Name:"vanyar", Age:27}
~~~
更多关于json的细节,以后再做讨论。访问官网[文档](https://link.jianshu.com?t=https://blog.golang.org/json-and-go)获取更多的信息。
### Response
请求和响应是http的孪生兄弟,不仅它们的报文格式类似,相关的处理和构造也类似。go构造响应的结构是`ResponseWriter`接口。
~~~
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(int)
}
~~~
里面的方法也很简单,`Header`方法返回一个header的map结构。`WriteHeader`则会返回响应的状态码。`Write`返回给客户端的数据。
我们已经使用了fmt.Fprintln 方法,直接向w写入响应的数据。也可以调用Write方法返回的字符。
~~~
func indexHandler(w http.ResponseWriter, r *http.Request) {
str := `
Go Web Programming
Go Web Programming
';