大家好,针对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标准库常用方法(三)
本节内容
- ProxyFromEnvironment() 函数
- ProxyURL() 函数
- Serve() 函数
- ServeContent() 函数
- DetectContentType() 函数
- MaxBytesReader() 函数
ProxyFromEnvironment()
ProxyFromEnvironment()
函数,用于读取所在环境的环境变量返回代理地址。比如环境变量HTTP_PROXY
、HTTPS_PROXY
和NO_PROXY
,如果在 NO_PROXY
排除的地址则不进行代理。
代理地址格式可以是完整的URL
,也可以是host[:port]
。支持 HTTP
、HTTPS
、SOCKS5
代理。
如果环境中未定义代理,或者NO_PROXY
定义的给定请求不应使用代理,则返回nil URL
和nil
错误。如果 req.URL.Host
地址为 localhost
加或没加端口,都会返回 nil
错误。
函数原型
func ProxyFromEnvironment(req *Request) (*url.URL, error)
函数使用
proxyfromenvironment.go
func main() {
os.Setenv("HTTP_PROXY", "http://127.0.0.1:12345")
req, err := http.NewRequest("GET", "http://example.com", nil)
if err != nil {
panic(err)
}
url, err := http.ProxyFromEnvironment(req)
if err != nil {
panic(err)
}
fmt.Println(url)
}
案例中 http.ProxyFromEnvironment(req)
仅会把读取环境变量 HTTP_PROXY
的代理地址,在我们使用 http.NewRequest()
请求时,不会使用代理请求。
下面通过 ProxyURL()
函数案例,发起代理请求。
ProxyURL()
ProxyURL()
作用是返回一个代理函数主要用于在 Transport{}
类型中,其参数是代理地址。
函数原型
func ProxyURL(fixedURL *url.URL) func(*Request) (*url.URL, error)
举例:使用代理发送 HTTP 请求。
proxyurl.go
func main() {
url, err := url.Parse("http://188.68.176.2:8080")
if err != nil {
panic(err)
}
client := http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(url),
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
res, err := client.Get("http://baidu.com")
if err != nil {
panic(err)
}
b, _ := httputil.DumpRequest(res.Request, false)
fmt.Println(string(b))
}
上述例子中, 将代理函数ProxyURL(url)
通过Transport{}
类型封装好后,向目标服务发送GET
请求。
Client{}
、Transport{}
类型后续文章将详细讲解。
注:代理地址,可以参考 https://www.kuaidaili.com/free/fps/ 用于测试使用。
上面案例,也可以将 http.ProxyURL()
函数改成 ProxyFromEnvironment()
用环境变量的方式。
proxyurl2.go
func main() {
url, err := url.Parse("http://google.com")
if err != nil {
panic(err)
}
os.Setenv("HTTP_PROXY", "http://127.0.0.1:7890")
client := http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // 跳过https
},
}
req := http.Request{
Method: "GET",
URL: url,
Header: map[string][]string{
"Proxy-Connection": {"keep-alive"},
},
}
res, err := client.Do(&req)
if err != nil {
panic(err)
}
defer res.Body.Close()
}
这里是通过我本地环境的代理VPN所监听的端口 http://127.0.0.1:7890
下面我通过抓包,大家可以看到执行代理请求的时候源端口 50130
是我们请求端,访问的谷歌网站目的端已经变成了 http://127.0.0.1:7890
地址也是我们的代理端,后面的响应也是由代理端给我们请求回应数据包。
Serve()
Serve()
函数,接收监听 HTTP 连接请求,为每个连接创建一个新goroutine
。goroutine
读取请求,然后调用处理程序来回复它们。
官方建议 handler
为 nil
类型, 则默认使用 DefaultServerMux
全局锁机制。 (可以参考上篇文章中有所介绍)
只有当 Listener
返回tls
的时候,才支持HTTP/2
协议。
Serve()
函数返回非 nil
的报错。
函数原型
func Serve(l net.Listener, handler Handler) error
Serve()
函数实际上是调用的 Server{}
类型中封装的一个方法。
func Serve(l net.Listener, handler Handler) error {
srv := &Server{Handler: handler}
return srv.Serve(l)
}
例如,上篇文章中介绍的 ListenAndServe()
、ListenAndServeTLS()
方法它们最终执行都是 Server{}
类型中的 Serve()
方法。
函数使用
serve.go
func main() {
ln, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, "帽儿山的枪手!\n")
})
log.Panicln(http.Serve(ln, nil))
}
ServeContent()
ServeContent() 函数,使用ReadSeeker
所读取的内容回复给用户请求。
ServeContent
比io.Copy
更好的是,他能够合适的处理一批请求,设置MIME类型,并且能够处理文件是否修改的请求。
如果响应的内容类型头没有设置,该函数首先会尝试从文件的文件扩展名推断文件类型。 如果推断不出来,则会读取文件的第一个块并传送给DetectContentType
来检测类型。
文件名称也可以不使用。 如果文字名称为空,则服务器不会传送给响应。 如果修改时间不为0,ServeContent
会把它放在服务器响应的Last-Modified
头里面。 如果客户端请求中包含了If-Modified-Since
头,ServeContent
会使用modtime
来判断是否把内容传给客户端。
content
的Seek
方法必须能够工作。 ServeContent
通过定位到文件结尾来确定文件大小。 *os.File
中实现了io.ReadSeeker
接口。
函数原型
func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker)
- 参数
w
服务器响应 - 参数
req
客户端请求 - 参数
name
文件名称 - 参数
modtime
文件的修改时间 - 参数
content
文件的内容,必须实现io.ReadSeeker
这个接口中的方法
下面案例使用 ServeContent()
函数实现文件下载功能。
servecontent.go
func main() {
http.HandleFunc("/download", func(w http.ResponseWriter, r *http.Request) {
file := "servecontent.go"
fileBytes, err := ioutil.ReadFile(file)
if err != nil {
panic(err)
}
mime := http.DetectContentType(fileBytes)
fileSize := len(string(fileBytes))
w.Header().Set("Content-Type", mime)
w.Header().Set("Content-Disposition", "attachment; filename="+file)
w.Header().Set("Content-Length", strconv.Itoa(fileSize))
http.ServeContent(w, r, file, time.Now(), bytes.NewReader(fileBytes))
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
首先通过 DetectContentType()
函数获取了文件的 MIME
类型,然后将文件转换为 Byte 类型传入 ServeContent()
函数中实现下载功能。
结合上篇文章中介绍的 ServeFile()
函数它实现起来更简洁仅需要一行代码实现文件下载,但前提需要知道文件上下文路径。
ServeContent()
函数更适用于当你只能拿到 byte[]
数据时,可以优先使用它。
DetectContentType()
DetectContentType()
该函数实现了一个算法,用来检测指定的数据是否符合该标准http://mimesniff.spec.whatwg.org
。
最多需要数据的前512个字节,DetectContentType()
会返回一个有效的MIME
类型。 如果它不能够识别数据,将会返回"application/octet-stream"
。
函数原型
func DetectContentType(data []byte) string
函数使用
func main() {
// image/png
fmt.Println(http.DetectContentType([]byte("\x89PNG\x0D\x0A\x1A\x0A")))
// image/jpeg
fmt.Println(http.DetectContentType([]byte("\xFF\xD8\xFF")))
}
注:一些类型的识别,可以参考go源码测试用例。
MaxBytesReader()
MaxBytesReader()
函数,用来保护服务器端,以避免客户端偶然或者恶意发送的长数据请求导致的服务端资源的浪费。
MaxBytesReader()
跟io.LimitReader
函数很像。但是它被设计来设置接收的请求体的最大大小。 跟io.LimitReader
不同MaxBytesReader()
的返回值是一个ReadCloser
,当读取超过限制时会返回non-nil
错误。 并且当它调用关闭方法的时候会把潜在的读取者(函数/进程)也关闭掉。
函数原型
func MaxBytesReader(w ResponseWriter, r io.ReadCloser, n int64) io.ReadCloser
- 参数
w
服务器响应 - 参数
r
可以指向 req.Body - 参数
n
限制大小
案例,限制客户端上传数据为10个字节。
maxbytesreader.go
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 10)
_, err := io.Copy(ioutil.Discard, r.Body)
if err != nil {
panic(err)
}
io.WriteString(w, "200\n")
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
下面我们通过 curl
命令模拟客户端请求, 其中body内容已经超出了10个字节
root@hc:~# curl --location --request POST 'http://127.0.0.1:8080' \
--header 'Content-Type: application/json' \
--data-raw '{
"t": "1234567890"
}'
请求完成后,看到服务端已经提示 请求Body过大。
技术文章持续更新,请大家多多关注呀~~
搜索微信公众号,关注我【 帽儿山的枪手 】