Go語言HTTP/2探險之旅
女主宣言
大家都知道,Go的標準庫HTTP伺服器預設支援HTTP/2。那麼,在這篇文章中,我們將首先展示Go的http/2伺服器功能,並解釋如何將它們作為客戶端使用。
PS:豐富的一線技術、多元化的表現形式,盡在“HULK一線技術雜談”,點關注哦!
在這篇文章中,我們將首先展示Go的http/2伺服器功能,並解釋如何將它們作為客戶端使用。Go的標準庫HTTP伺服器預設支援HTTP/2。
HTTP/2 伺服器
首先,讓我們在Go中建立一個http/2伺服器!根據http/2文件,所有東西都是為我們自動配置的,我們甚至不需要匯入Go的標準庫http2包:
HTTP/2強制使用TLS。為了實現這一點,我們首先需要一個私鑰和一個證書。在Linux上,下面的命令執行這個任務。
openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt
該命令將生成兩個檔案:server.key 以及 server.crt
現在,對於伺服器程式碼,以最簡單的形式,我們將使用Go的標準庫HTTP伺服器,並啟用TLS與生成的SSL檔案。
package main
import (
"log"
"net/http"
)
func main() {
// 在 8000 埠啟動伺服器
// 確切地說,如何執行HTTP/1.1伺服器。
srv := &http.Server{Addr:":8000", Handler: http.HandlerFunc(handle)}
// 用TLS啟動伺服器,因為我們執行的是http/2,它必須是與TLS一起執行。
// 確切地說,如何使用TLS連線執行HTTP/1.1伺服器。 log.Printf("Serving on )
log.Fatal(srv.ListenAndServeTLS("server.crt", "server.key")) }
func handle(w http.ResponseWriter, r *http.Request) {
// 記錄請求協議 log.Printf("Got connection: %s", r.Proto)
// 向客戶傳送一條訊息 w.Write([]byte("Hello")) }
HTTP/2 客戶端
在go中,標準 http.Client 也用於http/2請求。惟一的區別是在客戶端的Transport欄位,使用 http2.Transport 代替 http.Transport。
我們生成的伺服器證書是“自簽名”的,這意味著它不是由一個已知的證書頒發機構(CA)簽署的。這將導致我們的客戶端不相信它:
package main
import (
"fmt"
"net/http"
)
const url = "
func main() { _, err := http.Get(url)
fmt.Println(err) }
讓我們試著執行它:
$ go run h2-client.go Get
在伺服器日誌中,我們還將看到客戶端(遠端)有一個錯誤:
http: TLS handshake error from [::1]:58228: remote error: tls: bad certificate
為了解決這個問題,我們可以用定製的TLS配置去配置我們的客戶端。我們將把伺服器證書檔案新增到客戶端“證書池”中,因為我們信任它,即使它不是由已知CA簽名的。
我們還將新增一個選項,根據命令列標誌在HTTP/1.1和HTTP/2傳輸之間進行選擇。
package main
import (
"crypto/tls" "crypto/x509" "flag" "fmt" "io/ioutil" "log" "net/http" "golang.org/x/net/http2"
)
const url = "
var httpVersion = flag.Int("version", 2, "HTTP version") func main() { flag.Parse() client := &http.Client{}
// Create a pool with the server certificate since it is not signed // by a known CA caCert, err := ioutil.ReadFile("server.crt")
if err != nil {
log.Fatalf("Reading server certificate: %s", err) } caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert)
// Create TLS configuration with the certificate of the server tlsConfig := &tls.Config{ RootCAs: caCertPool, }
// Use the proper transport in the client switch *httpVersion {
case 1: client.Transport = &http.Transport{ TLSClientConfig: tlsConfig, }
case 2: client.Transport = &http2.Transport{ TLSClientConfig: tlsConfig, } }
// Perform the request resp, err := client.Get(url)
if err != nil {
log.Fatalf("Failed get: %s", err) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalf("Failed reading response body: %s", err) } fmt.Printf(
"Got response %d: %s %s\n", resp.StatusCode, resp.Proto, string(body)
) }
這一次我們得到了正確的回應:
$ go run h2-client.go Got response 200: HTTP/2.0 Hello
在伺服器日誌中,我們將看到正確的日誌線:獲得連線:Got connection: HTTP/2.0!!
但是當我們嘗試使用HTTP/1.1傳輸時,會發生什麼呢?
$ go run h2-client.go -version 1 Got response 200: HTTP/1.1 Hello
我們的伺服器對HTTP/2沒有任何特定的東西,所以它支援HTTP/1.1連線。這對於向後相容性很重要。此外,伺服器日誌表明連線是HTTP/1.1:Got connection: HTTP/1.1。
HTTP/2 高階特性
我們建立了一個HTTP/2客戶機-伺服器連線,並且我們正在享受安全有效的連線帶來的好處。但是HTTP/2提供了更多的特性,讓我們來研究它們!
伺服器推送
這可以很容易地在伺服器處理程式中實現(在github上的檢視):
func handle(w http.ResponseWriter, r *http.Request) {
// Log the request protocol log.Printf("Got connection: %s", r.Proto)
// Handle 2nd request, must be before push to prevent recursive calls. // Don't worry - Go protect us from recursive push by panicking. if r.URL.Path == "/2nd" {
log.Println("Handling 2nd") w.Write([]byte("Hello Again!"))
return }
// Handle 1st request log.Println("Handling 1st")
// Server push must be before response body is being written. // In order to check if the connection supports push, we should use // a type-assertion on the response writer. // If the connection does not support server push, or that the push // fails we just ignore it - server pushes are only here to improve // the performance for HTTP/2 clients. pusher, ok := w.(http.Pusher)
if !ok {
log.Println("Can't push to client") } else { err := pusher.Push("/2nd", nil)
if err != nil {
log.Printf("Failed push: %v", err) } }
// Send response body w.Write([]byte("Hello")) }
使用伺服器推送
讓我們重新執行伺服器,並測試客戶機。
對於HTTP / 1.1客戶端:
$ go run ./h2-client.go -version 1 Got response 200: HTTP/1.1 Hello
伺服器日誌將顯示:
Got connection: HTTP/1.1Handling 1st Can't push to client
HTTP/1.1客戶端傳輸連線產生一個 http.ResponseWriter 沒有實現http.Pusher,這是有道理的。在我們的伺服器程式碼中,我們可以選擇在這種客戶機的情況下該做什麼。
對於HTTP/2客戶:
go run ./h2-client.go -version 2 Got response 200: HTTP/2.0 Hello
伺服器日誌將顯示:
Got connection: HTTP/2.0Handling 1st Failed push: feature not supported
這很奇怪。我們的HTTP/2傳輸的客戶端只得到了第一個“Hello”響應。日誌表明連線實現了 http.Pusher 介面——但是一旦我們實際呼叫 Push() 函式——它就失敗了。
排查發現,HTTP/2客戶端傳輸設定了一個HTTP/2設定標誌,表明推送是禁用的。
因此,目前沒有選擇使用Go客戶機來使用伺服器推送。
作為一個附帶說明,google chrome作為一個客戶端可以處理伺服器推送。
伺服器日誌將顯示我們所期望的,處理程式被呼叫兩次,路徑 / 和 /2nd,即使客戶實際上只對路徑 /:
Got connection: HTTP/2.0Handling 1st Got connection: HTTP/2.0Handling 2nd
全雙工通訊
Go HTTP/2演示頁面有一個echo示例,它演示了伺服器和客戶機之間的全雙工通訊。
讓我們先用CURL來測試一下:
$ curl -i -XPUT --http2
我們把curl配置為使用HTTP/2,並將一個PUT/ECHO傳送給“hello”作為主體。伺服器以“HELLO”作為主體返回一個HTTP/2 200響應。但我們在這裡沒有做任何複雜的事情,它看起來像是一個老式的HTTP/1.1半雙工通訊,有不同的頭部。讓我們深入研究這個問題,並研究如何使用HTTP/2全雙工功能。
伺服器實現
下面是HTTP echo處理程式的簡化版本(不使用響應)。它使用 http.Flusher 介面,HTTP/2新增到http.ResponseWriter。
type flushWriter struct { w io.Writer } func (fw flushWriter) Write(p []byte) (n int, err error) { n, err = fw.w.Write(p)
// Flush - send the buffered written data to the client if f, ok := fw.w.(http.Flusher); ok { f.Flush() }
return
}
func echoCapitalHandler(w http.ResponseWriter, r *http.Request) {
// First flash response headers if f, ok := w.(http.Flusher); ok { f.Flush() }
// Copy from the request body to the response writer and flush // (send to client) io.Copy(flushWriter{w: w}, r.Body) }
伺服器將從請求正文讀取器複製到寫入ResponseWriter和 Flush() 的“沖洗寫入器”。同樣,我們看到了笨拙的型別斷言樣式實現,沖洗操作將緩衝的資料傳送給客戶機。
請注意,這是全雙工,伺服器讀取一行,並在一個HTTP處理程式呼叫中重複寫入一行。
GO客戶端實現
我試圖弄清楚一個啟用了HTTP/2的go客戶端如何使用這個端點,並發現了這個Github問題。提出了類似於下面的程式碼。
const url = "
func main() {
// Create a pipe - an object that implements `io.Reader` and `io.Writer`. // Whatever is written to the writer part will be read by the reader part.
pr, pw := io.Pipe() // Create an `http.Request` and set its body as the reader part of the // pipe - after sending the request, whatever will be written to the pipe, // will be sent as the request body. // This makes the request content dynamic, so we don't need to define it // before sending the request. req, err := http.NewRequest(http.MethodPut, url, ioutil.NopCloser(pr))
if err != nil {
log.Fatal(err) }
// Send the request resp, err := http.DefaultClient.Do(req) if err != nil {
log.Fatal(err) }
log.Printf("Got: %d", resp.StatusCode) // Run a loop which writes every second to the writer part of the pipe // the current time. go func() { for { time.Sleep(1 * time.Second) fmt.Fprintf(pw, "It is now %v\n", time.Now()) } }()
// Copy the server's response to stdout. _, err = io.Copy(os.Stdout, res.Body)
log.Fatal(err) }
總結
Go支援與伺服器推送和全雙工通訊的HTTP/2連線,這也支援HTTP/1.1與標準庫的標準TLS伺服器的連線——這太不可思議了。對於標準的庫HTTP客戶端,它不支援伺服器推送,但是支援標準庫的標準HTTP的全雙工通訊。以上就是本篇的內容,大家有什麼疑問可以在文章下面留言溝通。
HULK一線技術雜談
由360雲平臺團隊打造的技術分享公眾號,內容涉及雲端計算、資料庫、大資料、監控、泛前端、自動化測試等眾多技術領域,透過夯實的技術積累和豐富的一線實戰經驗,為你帶來最有料的技術分享
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31555491/viewspace-2216451/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 【Go語言探險】線上奇怪日誌問題的排查Go
- OAuth 2.0 的探險之旅OAuth
- GO語言————2、GO語言環境安裝Go
- Rust 語言學習之旅(2)Rust
- Go 語言 HTTP Server 原始碼分析GoHTTPServer原始碼
- 用Go語言寫HTTP中介軟體GoHTTP
- Go語言HTTP請求流式寫入bodyGoHTTP
- Go語言學習(2) - HelloWorldGo
- 2-2 Go語言的包(package)GoPackage
- Go語言入門系列(六)之再探函式Go函式
- Go 語言 http/https 檔案伺服器GoHTTP伺服器
- Go語言————1、初識GO語言Go
- Go語言面試題分享:選擇題11道(2)go語言開發Go面試題
- 【搞定Go語言】第2天4:Go語言基礎之流程控制Go
- Go語言內幕(2):深入 Go 編譯器Go編譯
- go語言請求http介面示例 並解析jsonGoHTTPJSON
- GO語言Go
- Go 1.21的2個語言變化Go
- 【Go】Go語言學習筆記-2-函式Go筆記函式
- Rust 語言學習之旅Rust
- Go語言之旅:基本型別Go型別
- Go_go語言初探Go
- 【Go語言入門系列】(八)Go語言是不是面嚮物件語言?Go物件
- go 語言常量Go
- Go語言mapGo
- go 語言切片Go
- go語言使用Go
- Rust 語言學習之旅(6)Rust
- Rust 語言學習之旅(3)Rust
- Rust 語言學習之旅(7)Rust
- 用無上下文的Go語言實現HTTP服務GoHTTP
- 新書《Go 語言程式設計之旅:一起用 Go 做專案》出版啦!新書Go程式設計
- 什麼是Go語言?Go語言有什麼特點?Go
- Go語言簡史Go
- Go 語言效能分析Go
- go語言的介面Go
- GO語言併發Go
- Go語言介紹Go