絕了,Go HTTP/2 終于要進(jìn)入標(biāo)準(zhǔn)庫(kù)了?。?!
絕了。最近 Go 社區(qū)有個(gè)重磅消息,HTTP/2 終于要正式進(jìn)入標(biāo)準(zhǔn)庫(kù)了!
這事兒說(shuō)起來(lái)挺有意思的。很多同學(xué)可能都遇到過(guò)這樣的尷尬場(chǎng)景:想配置一下 HTTP/2 的參數(shù),結(jié)果發(fā)現(xiàn)必須要導(dǎo)入 golang.org/x/net/http2 這個(gè)官方的 ”外部“ 包。
圖片
前兩天剛哭訴完 HTTP/3 被擱置。今天我們?cè)賮?lái)聊聊 Go 團(tuán)隊(duì)為什么終于肯把 HTTP/2 搬進(jìn)標(biāo)準(zhǔn)庫(kù),以及這背后的故事。
背景:一個(gè)別扭的設(shè)計(jì)
從 Go 1.6 開(kāi)始,net/http 包就提供了對(duì) HTTP/2 的透明支持。但這個(gè)支持方式有點(diǎn)特別——HTTP/2 的真正實(shí)現(xiàn)其實(shí)在 golang.org/x/net/http2 包里,標(biāo)準(zhǔn)庫(kù)只是把它打包進(jìn)來(lái)。
為什么要這么做呢?主要是為了避免循環(huán)依賴(lài)的問(wèn)題。http2 包依賴(lài) net/http,而 net/http 又要用 http2,這就形成了一個(gè)死循環(huán)。
Go 團(tuán)隊(duì)用了一個(gè)叫 bundle 的工具,把整個(gè) http2 包合并成一個(gè)單獨(dú)的文件(h2_bundle.go),然后塞進(jìn) net/http 里。
圖片
這個(gè)設(shè)計(jì)在早期是有好處的:
- 可以在 Go 發(fā)布周期之外更新 HTTP/2 實(shí)現(xiàn)
- 快速迭代,不受兼容性承諾約束
- 用戶(hù)可以自己選擇 HTTP/2 的版本
有什么問(wèn)題?
但現(xiàn)在問(wèn)題越來(lái)越多了。
1、關(guān)系太復(fù)雜了:net/http、h2_bundle.go、golang.org/x/net/http2 三者之間的關(guān)系讓人頭大,新手看了直接懵。
2、修 bug 太麻煩了:每次要給 HTTP/2 打安全補(bǔ)丁,都得在多個(gè)版本之間來(lái)回折騰,backport 的流程異常復(fù)雜。
3、配置不方便:想調(diào)個(gè) HTTP/2 參數(shù)?對(duì)不起,必須導(dǎo)入外部包。而且導(dǎo)入之后,不光配置變了,連實(shí)現(xiàn)都換了,這就很尷尬。
4、開(kāi)發(fā)受限:HTTP/1 和 HTTP/2 的代碼分別在不同的倉(cāng)庫(kù),想要同時(shí)改動(dòng)兩邊的邏輯,幾乎不可能原子化完成。
舉個(gè)例子,之前有個(gè) issue #52459,說(shuō)的是 HTTP/1 和 HTTP/2 都有自己的重試邏輯,導(dǎo)致請(qǐng)求可能被重試多次。
圖片
最簡(jiǎn)單的解決方案是搞一個(gè)統(tǒng)一的重試循環(huán),但因?yàn)榇a分散在兩個(gè)倉(cāng)庫(kù),實(shí)現(xiàn)起來(lái)困難重重。
提案:是時(shí)候搬家了
Go 團(tuán)隊(duì)提出了一個(gè)大膽的計(jì)劃:把 HTTP/2 徹底搬進(jìn)標(biāo)準(zhǔn)庫(kù)。
圖片
具體來(lái)說(shuō),就是把 golang.org/x/net/http2 的實(shí)現(xiàn)挪到 net/http/internal/http2 里。
這是個(gè)內(nèi)部包(internal),普通用戶(hù)不能直接導(dǎo)入。所有新的開(kāi)發(fā)都會(huì)在標(biāo)準(zhǔn)庫(kù)里進(jìn)行。
那老的 x/net/http2 包怎么辦呢?
Go 團(tuán)隊(duì)的計(jì)劃是這樣的:
- 在過(guò)渡期內(nèi),繼續(xù)給
x/net/http2包提供 bug 修復(fù)。 - 更新
x/net/http2,讓它在新版本 Go 里直接調(diào)用標(biāo)準(zhǔn)庫(kù)的實(shí)現(xiàn)。 - 等所有還在維護(hù)的 Go 版本都不再使用舊的 vendor 版本后,正式廢棄
x/net/http2。
這個(gè)方案很巧妙。既保證了平滑過(guò)渡,又能讓新代碼享受到統(tǒng)一實(shí)現(xiàn)的好處。
新的配置方式
搬進(jìn)標(biāo)準(zhǔn)庫(kù)之后,最直觀的變化就是配置方式變了。以前我們要這樣寫(xiě):
import (
"net/http"
"golang.org/x/net/http2"
)
func main() {
server := &http.Server{
Addr: ":8080",
}
// 必須調(diào)用這個(gè)函數(shù)來(lái)配置 HTTP/2
http2.ConfigureServer(server, &http2.Server{
MaxConcurrentStreams: 250,
IdleTimeout: 5 * time.Minute,
})
}現(xiàn)在可以直接在 net/http 里配置了:
import (
"net/http"
"time"
)
func main() {
server := &http.Server{
Addr: ":8080",
HTTP2: http.HTTP2Config{
MaxConcurrentStreams: 250,
MaxDecoderHeaderTableSize: 4096,
MaxEncoderHeaderTableSize: 4096,
MaxReadFrameSize: 16384,
MaxUploadBufferPerConnection: 1 << 20,
MaxUploadBufferPerStream: 1 << 20,
SendPingTimeout: 15 * time.Second,
PingTimeout: 15 * time.Second,
},
}
server.ListenAndServeTLS("cert.pem", "key.pem")
}是不是清爽多了?不用再導(dǎo)入外部包,所有配置都在標(biāo)準(zhǔn)庫(kù)里搞定。
客戶(hù)端也是一樣的:
transport := &http.Transport{
HTTP2: http.HTTP2Config{
MaxDecoderHeaderTableSize: 4096,
MaxEncoderHeaderTableSize: 4096,
MaxReadFrameSize: 16384,
},
}
client := &http.Client{
Transport: transport,
}接下來(lái)我們看下 HTTP2Config 的具體字段。
HTTP2Config 配置詳解
新的 HTTP2Config 結(jié)構(gòu)體統(tǒng)一了服務(wù)端和客戶(hù)端的配置項(xiàng),核心字段包括:
type HTTP2Config struct {
// MaxConcurrentStreams 指定對(duì)端可以同時(shí)打開(kāi)的流數(shù)量
// 對(duì)應(yīng) HTTP/2 的 SETTINGS_MAX_CONCURRENT_STREAMS
// 如果為 0,默認(rèn)值至少為 100
MaxConcurrentStreams uint32
// MaxDecoderHeaderTableSize 設(shè)置用于解碼 header 的壓縮表大小
// 對(duì)應(yīng) SETTINGS_HEADER_TABLE_SIZE,默認(rèn) 4096 字節(jié)
MaxDecoderHeaderTableSize uint32
// MaxEncoderHeaderTableSize 設(shè)置用于編碼 header 的壓縮表大小上限
// 接收到的 SETTINGS_HEADER_TABLE_SIZE 會(huì)被限制在這個(gè)值
// 默認(rèn) 4096 字節(jié)
MaxEncoderHeaderTableSize uint32
// MaxReadFrameSize 指定愿意接收的最大幀大小
// 對(duì)應(yīng) SETTINGS_MAX_FRAME_SIZE
// 有效值在 16k 到 16M 之間,默認(rèn)值會(huì)被使用
MaxReadFrameSize uint32
// MaxUploadBufferPerConnection 是每個(gè)連接的初始流控窗口大小
// HTTP/2 規(guī)范不允許小于 65535 或大于 2^32-1
MaxUploadBufferPerConnection int32
// MaxUploadBufferPerStream 是每個(gè)流的初始流控窗口大小
// HTTP/2 規(guī)范不允許大于 2^32-1
MaxUploadBufferPerStream int32
// SendPingTimeout 是在連接空閑時(shí)發(fā)送 ping 幀做健康檢查的超時(shí)時(shí)間
// 如果為 0,不進(jìn)行健康檢查
SendPingTimeout time.Duration
// PingTimeout 是 ping 響應(yīng)的超時(shí)時(shí)間,超時(shí)后連接會(huì)被關(guān)閉
// 如果為 0,使用默認(rèn)值 15 秒
PingTimeout time.Duration
// PermitProhibitedCipherSuites 如果為 true,
// 允許使用 HTTP/2 規(guī)范禁止的加密套件
PermitProhibitedCipherSuites bool
// CountError 在發(fā)生 HTTP/2 錯(cuò)誤時(shí)被調(diào)用
// 用于監(jiān)控指標(biāo)統(tǒng)計(jì),比如 expvar 或 Prometheus
// errType 只包含 ASCII 字母
CountError func(errType string)
}這些配置項(xiàng)涵蓋了 HTTP/2 協(xié)議的核心參數(shù),從并發(fā)控制、緩沖區(qū)大小到健康檢查,基本滿(mǎn)足了日常開(kāi)發(fā)的需求。
協(xié)議版本選擇
另一個(gè)重要的改進(jìn)是協(xié)議版本選擇。以前想禁用 HTTP/2,得這樣寫(xiě):
server := &http.Server{
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
}這寫(xiě)法看起來(lái)就很別扭,對(duì)吧?新的 API 直觀多了:
server := &http.Server{
Addr: ":8080",
Protocols: []http.Protocol{http.HTTP1}, // 只用 HTTP/1
}想同時(shí)支持 HTTP/1 和 HTTP/2,還能指定優(yōu)先級(jí):
server := &http.Server{
Addr: ":8080",
Protocols: []http.Protocol{http.HTTP2, http.HTTP1}, // 優(yōu)先 HTTP/2
}核心觀點(diǎn)在于,新的 Protocols 字段讓協(xié)議選擇變得清晰明了。列表中的順序代表了優(yōu)先級(jí),服務(wù)端會(huì)按這個(gè)順序和客戶(hù)端協(xié)商使用哪個(gè)協(xié)議。
如果不設(shè)置 Protocols,默認(rèn)值是 {HTTP2, HTTP1}。但如果你設(shè)置了 TLSNextProto 并且里面沒(méi)有 "h2" 這個(gè) key,默認(rèn)值就會(huì)變成 {HTTP1}。這樣就保證了向后兼容。
客戶(hù)端的用法也是一樣的:
transport := &http.Transport{
Protocols: []http.Protocol{http.HTTP2, http.HTTP1},
}
client := &http.Client{Transport: transport}h2c 支持
還有個(gè)值得一提的改進(jìn)是對(duì) h2c(未加密的 HTTP/2)的支持。
以前想用 h2c,要么得在 DialTLS 里返回未加密連接(這名字就很詭異),要么得用 golang.org/x/net/http2/h2c 包?,F(xiàn)在直接用協(xié)議選擇就行了:
server := &http.Server{
Addr: ":8080",
Protocols: []http.Protocol{http.UnencryptedHTTP2},
}
server.ListenAndServe() // 注意不是 ListenAndServeTLS客戶(hù)端也一樣簡(jiǎn)單:
transport := &http.Transport{
Protocols: []http.Protocol{http.UnencryptedHTTP2},
}
client := &http.Client{Transport: transport}
resp, err := client.Get("http://example.com") // http:// 不是 https://簡(jiǎn)單來(lái)說(shuō),Go 團(tuán)隊(duì)新增了一個(gè) UnencryptedHTTP2 常量,專(zhuān)門(mén)用來(lái)表示未加密的 HTTP/2 協(xié)議。
當(dāng) Protocols 同時(shí)包含 HTTP1 和 UnencryptedHTTP2 時(shí),服務(wù)端和客戶(hù)端都會(huì)支持 RFC 7540 Section 3.2 定義的 Upgrade: h2c 頭。服務(wù)端會(huì)把帶 Upgrade: h2c 的 HTTP/1 請(qǐng)求升級(jí)到 HTTP/2,客戶(hù)端也會(huì)發(fā)送這個(gè)頭。
如果只設(shè)置了 UnencryptedHTTP2 而沒(méi)有 HTTP1,那么客戶(hù)端會(huì)對(duì) http:// 開(kāi)頭的 URL 直接使用 HTTP/2。
向后兼容性保障
Go 核心團(tuán)隊(duì)特別注重向后兼容。現(xiàn)有代碼怎么辦?
如果你在用 http2.ConfigureServer 或 http2.ConfigureTransports,代碼還是能正常工作的。這兩個(gè)函數(shù)會(huì)被更新,在新版本 Go 里自動(dòng)把 HTTP2 加到 Protocols 列表里。
如果你通過(guò)設(shè)置 TLSNextProto 為空 map 來(lái)禁用 HTTP/2,這種做法也繼續(xù)有效。
如果你直接用 http2.Server 或 http2.Transport,照樣沒(méi)問(wèn)題。
對(duì)于所有已經(jīng)發(fā)布的 golang.org/x/net/http2 版本,行為會(huì)保持不變——選擇了這個(gè)包的實(shí)現(xiàn),就會(huì)用這個(gè)包的,覆蓋標(biāo)準(zhǔn)庫(kù)的。
但是很無(wú)奈的是,新版本的 golang.org/x/net/http2 在遇到支持非 vendor HTTP/2 的 Go 版本時(shí),會(huì)直接調(diào)用標(biāo)準(zhǔn)庫(kù)的實(shí)現(xiàn)。除非你用了標(biāo)準(zhǔn)庫(kù)不支持的特性(比如 Server.NewWriteScheduler 或 Transport.ConnPool),否則用的就是標(biāo)準(zhǔn)庫(kù)版本。
總之,老代碼不會(huì)因?yàn)檫@次改動(dòng)而出問(wèn)題。
總結(jié)
HTTP/2 進(jìn)入標(biāo)準(zhǔn)庫(kù)這事兒,說(shuō)白了就是一次技術(shù)債償還。當(dāng)年為了快速迭代選擇了折衷方案,現(xiàn)在該還的債總是要還的。
這次改動(dòng)帶來(lái)的好處很明顯:
- 配置更簡(jiǎn)單,不用導(dǎo)入外部包就能配置 HTTP/2 參數(shù)
- 維護(hù)更容易,bug 修復(fù)不用到處 backport 了
- 開(kāi)發(fā)更靈活,HTTP/1 和 HTTP/2 可以一起改,不用擔(dān)心跨倉(cāng)庫(kù)的依賴(lài)問(wèn)題
- API 更統(tǒng)一,協(xié)議選擇、配置方式都變得清晰明了
預(yù)計(jì)這個(gè)改動(dòng)會(huì)在 Go 1.26 落地。
































