头图

大家好,针对Go语言 net/http 标准库,将梳理的相关知识点分享给大家~~
围绕 net/http 标准库相关知识点还有许多章节,请大家多多关注。
文章中代码案例只有关键片段,完整代码请查看github仓库:https://github.com/hltfaith/go-example/tree/main/net-http
本章节案例,请大家以 go1.16+ 版本以上进行参考。

net/http标准库系列文章

本节内容

  • Request结构体
  • 案例一:封装http服务实现chunked分块传输
  • 案例二:实现文件上传

Request结构体

Request类型,主要实现封装了http请求的内容,用于用户的请求的结构原型。
Request结构体原型

type Request struct {
    // Method可以指定HTTP方法(GET、POST、PUT等)
    Method string
    // 指定被请求的URI
    URL *url.URL
    // 传入服务器请求的协议版本
    Proto string // "HTTP/1.0"
    ProtoMajor int 
    ProtoMinor int
    // 设置请求头
    // Header字段用来表示HTTP请求的头域。如果header(多行键值对格式)为:
    //    accept-encoding: gzip, deflate
    //    Accept-Language: en-us
    //    Connection: keep-alive
    // 则:
    //    Header = map[string][]string{
    //        "Accept-Encoding": {"gzip, deflate"},
    //        "Accept-Language": {"en-us"},
    //        "Connection": {"keep-alive"},
    //    }
    Header Header // type Header map[string][]string
    // 传入body请求体
    Body io.ReadCloser
    // GetBody定义一个可选函数来返回Body的新副本
    GetBody func() (io.ReadCloser, error)
    // ContentLength记录关联内容的长度
    ContentLength int64
    // TransferEncoding列出了从最外层到最内层的传输编码
    // 本字段一般会被忽略。当发送或接受请求时,会自动添加或移除"chunked"传输编码。
    TransferEncoding []string
    // Close表示连接结束后是否关闭
    Close bool
    // 服务器主机地址,如果协议是http2请求头则显示 :Authority:伪头字段值
    // 也可以是 "host:port"形式
    Host string
    // 表单数据, 支持PATCH、POST、PUT表单数据
    Form url.Values
    // 同样支持PATCH、POST、PUT表单数据 
    PostForm url.Values
    // 解析多部分表单,包括文件上传
    // 字段在调用ParseMultiparForm后可用
    // http客户端请求会忽略MultipartForm
    MultipartForm *multipart.Form
    // Trailer指定在请求体之后发送附加头
    Trailer Header
    // HTTP服务器在调用处理程序之前将RemoteAddr设置为"IP:port"地址
    // 该字段HTTP客户端可以被忽略
    RemoteAddr string
    // RequestURI是客户端发送给服务器的请求目标
    RequestURI string
    // 启用tls的连接设置该字段,否则它将字段设为nil
    // 该字段HTTP客户端可以被忽略
    TLS *tls.ConnectionState
    // Cancel是一个可选通道
    Cancel <-chan struct{}
    // 此请求的重定向响应
    Response *Response
}

Request 方法函数

// 增加cookie信息
func (r *Request) AddCookie(c *Cookie)
// 开启身份验证, 返回请求的Authorization标头中提供的用户名和密码
func (r *Request) BasicAuth() (username, password string, ok bool)
// 克隆request的副本,需要传入context
func (r *Request) Clone(ctx context.Context) *Request
// 返回request的context
func (r *Request) Context() context.Context
// 返回请求中提供的命名Cookie
func (r *Request) Cookie(name string) (*Cookie, error)
// cookie解析并返回要发的cookie列表
func (r *Request) Cookies() []*Cookie
// 返回表单key匹配的文件, FormFile调用 Request.ParseMultipartForm和Request.ParseForm
func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error)
// 返回值来自 application/x-www-form-urlencoded, query查询参数, multipart/form-data
func (r *Request) FormValue(key string) string
// 返回body内容为流类型
func (r *Request) MultipartReader() (*multipart.Reader, error)
// 比如POST,PUT,PATCH请求将解析后, 放入PostForm
func (r *Request) ParseForm() error
// 将请求体解析为 MultipartForm
func (r *Request) ParseMultipartForm(maxMemory int64) error
// 返回传入的表单值
func (r *Request) PostFormValue(key string) string
// 报告请求中使用的HTTP协议是否为major.minor的版本格式
func (r *Request) ProtoAtLeast(major, minor int) bool
// 返回请求中的url
func (r *Request) Referer() string
// 设置请求授权头,进行身份验证
func (r *Request) SetBasicAuth(username, password string)
// 返回请求User-Agent
func (r *Request) UserAgent() string
// 返回request浅拷贝, 其上下文更改为ctx
func (r *Request) WithContext(ctx context.Context) *Request
// 写入http请求
func (r *Request) Write(w io.Writer) error
// 类似于Write(),但以HTTP代理所期望的形式写入请求
func (r *Request) WriteProxy(w io.Writer) error

案例一:封装http服务实现chunked分块传输

本功能中客户端携带JSON格式的数据向服务端发起POST请求,服务端收到请求数据后采用Transfer-Encoding: chunked分块传输,将数据返回响应。

目的是让HTTP响应中的body不是一次性发送完毕,而是分成了许多的块(chunk)逐个发送,直到发送完毕。

客户端代码片段
requestclient.go

在客户端中封装了请求体,设置载荷体为 Content-Type: application/json 类型,携带json格式数据内容。
最后发起HTTP请求,并读取服务端的响应。

服务端代码片段
requestserver.go

type UserInfo struct {
    ID       string `json:"id"`
    Username string `json:"username"`
    City     string `json:"city"`
}

func main() {
    http.HandleFunc("/post", func(w http.ResponseWriter, r *http.Request) {
        flusher, ok := w.(http.Flusher)
        if !ok {
            panic("expected http.ResponseWriter to be an http.Flusher")
        }
        w.Header().Set("Transfer-Encoding", "chunked")
        w.Header().Set("Connection", "Keep-Alive")
        w.Header().Set("X-Content-Type-Options", "nosniff")
        b, _ := io.ReadAll(r.Body)
        userinfos := []*UserInfo{}
        json.Unmarshal(b, &userinfos)

        for _, info := range userinfos {
            wbody := []byte("Chunk: " + info.Username + "\n")
            _, err := w.Write(wbody)
            if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
            }
            flusher.Flush()
        }
    })
    log.Fatal(http.ListenAndServe(":80", nil))
}

服务端代码是通过 http.Flusher 接口类型来实现的分块传输。Flush() 方法只会将当前缓存中的响应数据发送给客户端,而不会关闭连接。

其实现的思想就是通过http的Transfer-Encoding: chunked头告诉客户端,服务端的内容要分块传输了。然后服务端就将内容先写入缓冲区,然后立即使用Flush函数将缓冲区的内容输出到客户端。这就是一个块的输出。然后依次循环写入,Flush刷新输出这个过程。

这里说明下分块传输的编码规则:

  1. 每个分块包含两个部分,长度头数据块
  2. 长度头是以 CRLF(回车换行,即\r\n)结尾的一行明文,用 16 进制数字表示长度
  3. 数据块紧跟在长度头后,最后也用 CRLF 结尾;但数据不包含 CRLF
  4. 最后用一个长度为 0 的块表示数据传输结束,即0\r\n\r\n

在抓包中已经看到服务端的HTTP响应包,响应Body中已经返回了 chunked 分块的内容

打印出来HTTP原始报文如下

总结 Transfer-Encoding: chunked 分块传输报文数据结构如下

HTTP/1.1 200 OK\r\n
Transfer-Encoding: chunked\r\n
\r\n

10\rn
.....................\r\n
10\rn
.....................\r\n
0\r\n\r\n

案例二:文件上传

本功能使用 Request{} 类型封装客户端并携带多个文件发起文件上传HTTP请求,服务端通过 Request{} 类型中MultipartForm解析多部分表单数据。

首先,我们准备两个二进制文件,下面通过 dd 命令生成两个 10M 大小的二进制文件用于测试。

dd if=/dev/zero of=file1.bin bs=10240 count=1024
dd if=/dev/zero of=file2.bin bs=10240 count=1024

客户端代码片段
requestclient2.go

使用mime/multipart标准库,创建 multipart/form-data 类型数据作为请求体Body内容。


需要注意的是,客户端请求头类型是 Content-Type: multipart/form-data; boundary=7cfa31806e7151431dffe1d1d086eaaefbc2dbe5a61ced7c2bd8f51db01c
multipart/form-data 指定传输数据为二进制类型,比如图片、文件等。
boundary后面内容是指多部分的表单内容分隔标识 (也是随机生成的)。

服务端代码片段
requestserver2.go

func main() {
    http.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
        // 如果超过100字节使用临时文件来存储multipart/form中文件
        // 不超过则存入内存临时存储
        err := r.ParseMultipartForm(100)
        if err != nil {
            panic(err)
        }
        // MultipartForm解析多部分form内容
        m := r.MultipartForm
        for f := range m.File {
            // 取到文件信息
            file, fHeader, err := r.FormFile(f)
            if err != nil {
                panic(err)
            }
            defer file.Close()
            out, err := os.Create("upload/" + fHeader.Filename)
            if err != nil {
                panic(err)
            }
            defer out.Close()
            _, err = io.Copy(out, file)
            if err != nil {
                panic(err)
            }
        }
    })
    log.Fatal(http.ListenAndServe(":8080", nil))
}

服务端通过 MultipartForm 解析客户端携带的二进制文件,然后将解析到文件写入到本地目录中。
下面,我通过执行客户端代码向服务端发起HTTP请求。通过抓包网卡方式,将我们本次请求中的 multipart/form-data实际的报文内容进行分析,以助于理解。

我们发现在客户端代码 Request{} 中,我们并没有定义 Transfer-Encoding: chunked 头类型进行分块传输,但实际抓包报文中携带此类型,是因为HTTP/1.1协议中默认机制在不定义 ContentLength 长度时,则会通过 chunk 来进行分块传输。

Body请求体如下:

  1. 首先是,分块传输的大小 8000 为16进制表示,转换十进制长度是 32768
  2. 分隔是以前缀 -- 加上随机字符组成,这里随机字符是我们在 multipart标准库FormDataContentType()生成的 7cfa31806e7151431dffe1d1d086eaaefbc2dbe5a61ced7c2bd8f51db01c
  3. form-data头部信息是在客户端代码创建Body内容 CreateFormFile() 生成的,主要的作用是可以让服务端收到请求后拿到文件的名称等信息。
Contenet-Disposition: form-data; name="file1.bin"; filename="file1.bin"
Contenet-Type: application/octet-stream
  1. 最后则是实际的二进制数据内容

multipart/form-data 结束后,会以前缀 -- 加上随机字符 在加上后缀 -- ,组合成 --7cfa31806e7151431dffe1d1d086eaaefbc2dbe5a61ced7c2bd8f51db01c-- 代表结束。

分块传输最后也以 0\r\n\r\n 结尾,代表结束。(上面图片中0结尾描述错误)

总结上述 multipart/form-data 报文中的Body数据结构

POST /upload HTTP/1.1
Content-Type:multipart/form-data; boundary=xxxxwwwwttttdddd

100
--xxxxwwwwttttdddd
Contenet-Disposition: form-data; name="file1.bin"; filename="file1.bin"
Contenet-Type: application/octet-stream

...........................
...........................

100
--xxxxwwwwttttdddd
Contenet-Disposition: form-data; name="file2.bin"; filename="file2.bin"
Contenet-Type: application/octet-stream

...........................
...........................

--xxxxwwwwttttdddd--

0

技术文章持续更新,请大家多多关注呀~~

搜索微信公众号,关注我【 帽儿山的枪手 】


帽儿山的枪手
71 声望18 粉丝