大家好,針對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過大。
技術文章持續更新,請大家多多關注呀~~
搜索微信公眾號,關注我【 帽兒山的槍手 】