Start
上篇文章我們瞭解瞭如何在HTTP/2 server端進行Header信息的發送,同時保持連接不斷開。這次我們在這個基礎上,實現自動下發PUSH
。
先來實現一個最簡單的Server Push
的例子, 我們在上次的demo基礎上繼續改進
package main
import (
"html/template"
"log"
"net/http"
)
func main() {
http.HandleFunc("/header", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("X-custom-header", "custom header")
w.WriteHeader(http.StatusNoContent)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
select {}
})
// 用於push的 handler
http.HandleFunc("/crt", func(w http.ResponseWriter, r *http.Request) {
tpl := template.Must(template.ParseFiles("server.crt"))
tpl.Execute(w, nil)
})
// 請求該Path會觸發Push
http.HandleFunc("/push", func(w http.ResponseWriter, r *http.Request) {
pusher, ok := w.(http.Pusher)
if !ok {
log.Println("not support server push")
} else {
err := pusher.Push("/crt", nil)
if err != nil {
log.Printf("Failed for server push: %v", err)
}
}
w.WriteHeader(http.StatusOK)
})
log.Println("start listen on 8080...")
log.Fatal(http.ListenAndServeTLS(":8080", "server.crt", "server.key", nil))
}
以上代碼添加了兩個Hanlder
,一個是 /crt
,返回我們的證書內容,這個是用來給做客戶端push的內容。另一個是 /push
,請求該鏈接時,我們會將 /crt
的內容主動 push
到客戶端。
GO服務啓動後,我們通過h2c來訪問下/push
: 先在一個終端通過 h2c start -d
啓動進行輸出顯示,然後另外開一個終端窗口發起請求 h2c connect localhost:8080
和 h2c get /push
:
來解讀下這個請求中都發生了什麼:
- 客戶端通過
stream id=1
發送HEADERS FRAME
進行請求,請求Path是/push
- 服務端在
stream id=1
中返回一個PUSH_PROMISE
(配合下表食用) ,攜帶了部分Header
信息,承諾會在stream id=2
中返回path: /crt
的相關信息,這裏相當於告訴客戶端,如果你接下來需要請求/crt
的時候,就不要請求了,這個內容我一會就給你發過去了。 - 服務端正常響應
get /push
的請求,返回了對應的Header
信息,並通過END_STREAM
表示此stream
的交互完成了。 - 服務端通過
stream id=2
下發/crt
的相關信息,第四步是返回的Header
信息. - 服務端通過
stream id=2
下發/crt
的相關DATA
信息, 並通過END_STREAM
表示承諾的/crt
的內容發送完畢。
// PUSH_PROMISE Frame結構
+---------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|R| Promised Stream ID (31) |
+-+-----------------------------+-------------------------------+
| Header Block Fragment (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+
通過這個例子,我們應該就掌握了 Server Push
的用法,在此基礎上,我們結合上一章講到的內容,再改進一下,實現 “服務端定時主動PUSH”:
// 服務端定時 "主動" push內容
http.HandleFunc("/autoPush", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("X-custom-header", "custom")
w.WriteHeader(http.StatusNoContent)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
pusher, ok := w.(http.Pusher)
if ok {
for {
select {
case <-time.Tick(5 * time.Second):
err := pusher.Push("/crt", nil)
if err != nil {
log.Printf("Failed for server push: %v", err)
}
}
}
}
})
效果如圖:
服務端一直髮送 PUSH_PROMISE
消息給客戶端,每次間隔5s,並且每次 Promised Strea Id
都在偶數範圍內進行遞增 2,4,6,8,10…
這個例子裏,我們用了一個 for
循環 和一個定時器 time.Tick
,在服務端返回不帶 END_STREAM
的 Headers
後,每隔5s向客戶端主動 Push
一個內容,這裏我們 Push
的內容是固定的,在實際應用場景中,可以從一個特定的 channel
中取出需要下發的消息,然後再動態的構造請求的path,可以是攜帶參數的,來實現動態的控制需要 Push
什麼內容。這樣就實現了 “服務端主動PUSH” 的功能。
HTTP/2 PUSH in Go
接下來看下 Server Push
在 Go 中的實現:
// Push implements http.Pusher.
func (w *http2responseWriter) Push(target string, opts *PushOptions) error {
internalOpts := http2pushOptions{}
if opts != nil {
internalOpts.Method = opts.Method
internalOpts.Header = opts.Header
}
return w.push(target, internalOpts)
}
func (w *http2responseWriter) push(target string, opts http2pushOptions) error {
// ...
// Push只能是對 GET or HEAD 方法
if opts.Method != "GET" && opts.Method != "HEAD" {
return fmt.Errorf("method %q must be GET or HEAD", opts.Method)
}
// 構造要Push的內容的請求
msg := &http2startPushRequest{
parent: st,
method: opts.Method,
url: u,
header: http2cloneHeader(opts.Header),
done: http2errChanPool.Get().(chan error),
}
// 在客戶端連接斷開或者END_STREAM之前可以發送PUSH,把構造好的PushRequest放到 sc.serveMsgCh channel 裏
select {
case <-sc.doneServing:
return http2errClientDisconnected
case <-st.cw:
return http2errStreamClosed
case sc.serveMsgCh <- msg:
}
}
// 在serve中會 取出 sc.serveMsgCh 中的消息進行對應的操作,當取到 PushRequest 時,就會發送Push消息
func (sc *http2serverConn) serve() {
// ...
loopNum := 0
for {
loopNum++
select {
// ...
case msg := <-sc.serveMsgCh:
switch v := msg.(type) {
// ...
case *http2startPushRequest:
sc.startPush(v)
// ...
}
}
}
}
func (sc *http2serverConn) startPush(msg *http2startPushRequest) {
// ...
// 獲取Prosise的Stream id,當真正要發送PUSH_PROMISE時才進行獲取,並且同時異步啓動需要Push的Handler的請求.
allocatePromisedID := func() (uint32, error) {
// ...
sc.maxPushPromiseID += 2
promisedID := sc.maxPushPromiseID
promised := sc.newStream(promisedID, msg.parent.id, http2stateHalfClosedRemote)
rw, req, err := sc.newWriterAndRequestNoBody(promised, http2requestParam{
method: msg.method,
scheme: msg.url.Scheme,
authority: msg.url.Host,
path: msg.url.RequestURI(),
header: http2cloneHeader(msg.header),
})
// ...
// 進行handle請求
go sc.runHandler(rw, req, sc.handler.ServeHTTP)
return promisedID, nil
}
// 構造好 PUSH_PROMISE, 開始發送
sc.writeFrame(http2FrameWriteRequest{
write: &http2writePushPromise{
streamID: msg.parent.id,
method: msg.method,
url: msg.url,
h: msg.header,
allocatePromisedID: allocatePromisedID,
},
stream: msg.parent,
done: msg.done,
})
}
Done.