大家好,针对Go语言 net/http
标准库,将梳理的相关知识点分享给大家~~
围绕 net/http
标准库相关知识点还有许多章节,请大家多多关注。
文章中代码案例只有关键片段,完整代码请查看github
仓库:https://github.com/hltfaith/go-example/tree/main/net-http
本章节案例,请大家以 go1.16+
版本以上进行参考。
net/http标准库系列文章
- Golang net/http标准库常用请求方法(一)
- Golang net/http标准库常用方法(二)
- Golang net/http标准库常用方法(三)
- Golang net/http标准库Request类型(四)
本节内容
- 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刷新输出这个过程。
这里说明下分块传输的编码规则:
- 每个分块包含两个部分,
长度头
和数据块
长度头
是以 CRLF(回车换行,即\r\n
)结尾的一行明文,用 16 进制数字表示长度数据块
紧跟在长度头
后,最后也用 CRLF 结尾;但数据不包含 CRLF- 最后用一个长度为 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请求体如下:
- 首先是,分块传输的大小
8000
为16进制表示,转换十进制长度是 32768 - 分隔是以前缀
--
加上随机字符组成,这里随机字符是我们在multipart
标准库FormDataContentType()
生成的7cfa31806e7151431dffe1d1d086eaaefbc2dbe5a61ced7c2bd8f51db01c
。 - form-data头部信息是在客户端代码创建Body内容
CreateFormFile()
生成的,主要的作用是可以让服务端收到请求后拿到文件的名称等信息。
Contenet-Disposition: form-data; name="file1.bin"; filename="file1.bin"
Contenet-Type: application/octet-stream
- 最后则是实际的二进制数据内容
在 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
技术文章持续更新,请大家多多关注呀~~
搜索微信公众号,关注我【 帽儿山的枪手 】
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。