zoukankan      html  css  js  c++  java
  • [Go] Http包 使用简介

    请求的结构

    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
    %#v
    ", 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 的细节,以后再做讨论。访问官网文档获取更多的信息。

    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 := `<html>
    <head><title>Go Web Programming</title></head>
    <body><h1>Hello World</h1></body>
    </html>`
        w.Write([]byte(str))
    }                                                                           
    $  curl -i http://127.0.0.1:8000/
    HTTP/1.1 200 OK
    Date: Wed, 07 Dec 2016 09:13:04 GMT
    Content-Length: 95
    Content-Type: text/html; charset=utf-8
    
    <html>
    <head><title>Go Web Programming</title></head>
    <body><h1>Hello World</h1></body>
    </html>%   

    Go 根据返回的字符,自动修改成了 text/html 的 Content-Type 格式。返回数据自定义通常需要修改 header 相关信息。

    func indexHandler(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(501)
        fmt.Fprintln(w, "No such service, try next door")
    }
    
    $ curl -i http://127.0.0.1:8000/
    HTTP/1.1 501 Not Implemented
    Date: Wed, 07 Dec 2016 09:14:58 GMT
    Content-Length: 31
    Content-Type: text/plain; charset=utf-8
    
    No such service, try next door

    重定向

    重定向的功能可以更加设置 header 的 location 和 http 状态码实现。

    func indexHandler(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Location", "https://google.com")
        w.WriteHeader(302)
    }
    
    $  curl -i http://127.0.0.1:8000/
    HTTP/1.1 302 Found
    Location: https://google.com
    Date: Wed, 07 Dec 2016 09:20:19 GMT
    Content-Length: 31
    Content-Type: text/plain; charset=utf-8

    重定向是常用的功能,因此 Go 也提供了工具方法,http.Redirect(w, r, "https://google.com", http.StatusFound)。

    与请求的 Header 结构一样,w.Header 也有几个方法用来设置 headers

    func (h Header) Add(key, value string) {
        textproto.MIMEHeader(h).Add(key, value)
    }
    
    func (h Header) Set(key, value string) {
        textproto.MIMEHeader(h).Set(key, value)
    }
    
    func (h MIMEHeader) Add(key, value string) {
        key = CanonicalMIMEHeaderKey(key)
        h[key] = append(h[key], value)
    }
    
    func (h MIMEHeader) Set(key, value string) {
        h[CanonicalMIMEHeaderKey(key)] = []string{value}
    }

    Set和Add方法都可以设置 headers,对于已经存在的 key,Add 会追加一个值 value 的数组中,,set 则是直接替换 value 的值。即 append 和赋值的差别。

    Json

    请求发送的数据可以是 JSON,同样响应的数据也可以是 json。restful 风格的 api 也是返回 json 格式的数据。对于请求是解码 json 字串,响应则是编码 json 字串,Go 提供了标准库 encoding/json

    type Post struct {
        User string
        Threads []string
    }
    
    func indexHandler(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        post := &Post{
            User: "vanyar",
            Threads: []string{"first", "second", "third"},
        }
        json, _ := json.Marshal(post)
        w.Write(json)
    }
    
    $  curl -i http://127.0.0.1:8000/
    HTTP/1.1 200 OK
    Content-Type: application/json
    Date: Thu, 08 Dec 2016 06:45:17 GMT
    Content-Length: 54
    
    {"User":"vanyar","Threads":["first","second","third"]}

    当然,更多的 json 处理细节稍后再做介绍。

    总结

    对于 web 应用程式,处理请求,返回响应是基本的内容。Golang 很好的封装了 Request 和 ReponseWriter 给开发者。无论是请求还是响应,都是针对 url,header 和 body 相关数据的处理。也是 http 协议的基本内容。

    除了 body 的数据处理,有时候也需要处理 header 中的数据,一个常见的例子就是处理 cookie。这将会在 cookie 的话题中讨论。

  • 相关阅读:
    Java vs Python
    Compiled Language vs Scripting Language
    445. Add Two Numbers II
    213. House Robber II
    198. House Robber
    276. Paint Fence
    77. Combinations
    54. Spiral Matrix
    82. Remove Duplicates from Sorted List II
    80. Remove Duplicates from Sorted Array II
  • 原文地址:https://www.cnblogs.com/52php/p/6407592.html
Copyright © 2011-2022 走看看