HTTP/2 in GO(四)

HTTP/2 in GO(一)

HTTP/2 in GO(二)

HTTP/2 in GO(三)

HTTP/2 in GO(四)

HTTP/2 in GO(五)

 

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 :

 

來解讀下這個請求中都發生了什麼:

  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”:

// 服務端定時 "主動" 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.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章