上篇文章我们了解了如何在 HTTP/2 server 端进行 Header 信息的发送,同时保持连接不断开。这次我们在这个基础上,实现自动下发 PUSH。
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 {} })
http.HandleFunc("/crt", func(w http.ResponseWriter, r *http.Request) { tpl := template.Must(template.ParseFiles("server.crt")) tpl.Execute(w, nil) })
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 :
来解读下这个请求中都发生了什么:
1.客户端通过 stream id=1 发送 HEADERS FRAME 进行请求,请求 Path 是 /push
2.服务端在 stream id=1 中返回一个 PUSH_PROMISE(配合下表食用) ,携带了部分 Header 信息,承诺会在 stream id=2 中返回 path: /crt 的相关信息,这里相当于告诉客户端,如果你接下来需要请求 /crt 的时候,就不要请求了,这个内容我一会就给你发过去了。
3.服务端正常响应 get /push 的请求,返回了对应的 Header 信息,并通过 END_STREAM 表示此 stream 的交互完成了。
4.服务端通过 stream id=2 下发 /crt 的相关信息,第四步是返回的 Header 信息.
5.服务端通过 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”:
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 中的实现:
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 { if opts.Method != "GET" && opts.Method != "HEAD" { return fmt.Errorf("method %q must be GET or HEAD", opts.Method) } msg := &http2startPushRequest{ parent: st, method: opts.Method, url: u, header: http2cloneHeader(opts.Header), done: http2errChanPool.Get().(chan error), } select { case <-sc.doneServing: return http2errClientDisconnected case <-st.cw: return http2errStreamClosed case sc.serveMsgCh <- msg: }}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) { 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), })
go sc.runHandler(rw, req, sc.handler.ServeHTTP) return promisedID, nil } 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.
本文转载自公众号 360 云计算(ID:hulktalk)。
原文链接:
https://mp.weixin.qq.com/s/kKQuv9tK791eFxmqGRWy2Q
评论