本次讲一个非常简单的功能,然后把其内部实现串一下。
这次要实现的功能非常简单,就是一个 http2 的 server,对客户端的请求,只返回一个 header 信息,并且保持连接,以便在后续任何时候进行一些其他的响应操作。目前看起来这个场景可能没有太大作用,其实 HTTP/2 做为一个超文本传输协议,目前我们能想到的应用场景还都是普通的 web 业务,但是老外们的思路就比较广,已经把一些 HTTP/2 的特性在特定的场景发挥出来了,比如 Amazon 的 Alexa,Apple 的 APNS 等。这次实现的这个小功能,就是 Alexa 里用到的一小部分.
Amazon 的 avs(Alexa Voice Service)通过 HTTP/2 实现了全双工的传输功能,其下行功能就用到了这块,Alexa 跟 avs 建立链接后,客户端会发起一个 GET /v20160207/directives 的请求,服务端接受请求后,返回一个 200 的头信息,并 hold 住链接,后续使用该链接通过 Server Push 功能给客户端主动发送指令。
本次开始,我们先不管 Server Push,先从发送 Header 这个小功能开始吧。
HTTP/2 在 GO 语言的实现中没有支持 h2c,所以我们必须使用带证书的加密方式,那么首先需要有一张证书。
我们可以使用 openssl 自己生成一张:
 openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt
   复制代码
 
然后按提示随便输入一些内容就可以得到两个文件,server.key 和 server.crt,其实就是相当于私钥和公钥。当然这个证书是不能在互联网上正常流通使用的,因为证书是自己签发的,没有人给你做担保,能确认这个证书跟它所标识的内容提供方是匹配的。所以我们在做请求测试的时候,需要客户端忽略证书校验才可以。
服务端 GO 示例的代码如下:
 package main
import (    "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 {}    })
    log.Println("start listen on 8080...")    log.Fatal(http.ListenAndServeTLS(":8080", "server.crt", "server.key", nil))}
   复制代码
 
服务运行起来后我们在一个较新的支持 HTTP/2 的 curl 命令下执行:
 curl  "https://localhost:8080/header"  -k -i --http2
   复制代码
 
这样就实现了只返回了一个 header 信息,并且链接没有断开。
我们再通过前边介绍过的 h2c 来看下请求的效果:
可以看到返回的只有一个 Header 信息,并且是没有 END_STREAM 标记的。
本次的实践内容到这里就可以结束了,最终实现的代码很简单,但是为什么这样可以实现呢,在缺少相关资料的情况下,很难知道这样做是可以实现该目的的,那么接下来就从 Go 语言中对 HTTP/2 的实现来一探究竟吧:
HTTP/2 Frame in Go
首先来看 HTTP/2 中的最小传输单元:Frame:
 // A Frame is the base interface implemented by all frame types.// Callers will generally type-assert the specific frame type:// *HeadersFrame, *SettingsFrame, *WindowUpdateFrame, etc.//// Frames are only valid until the next call to Framer.ReadFrame.type http2Frame interface {    Header() http2FrameHeader
    // invalidate is called by Framer.ReadFrame to make this    // frame's buffers as being invalid, since the subsequent    // frame will reuse them.    invalidate()}
// A FrameHeader is the 9 byte header of all HTTP/2 frames.//// See http://http2.github.io/http2-spec/#FrameHeadertype http2FrameHeader struct {    valid bool // caller can access []byte fields in the Frame
    // Type is the 1 byte frame type. There are ten standard frame    // types, but extension frame types may be written by WriteRawFrame    // and will be returned by ReadFrame (as UnknownFrame).    Type http2FrameType
    // Flags are the 1 byte of 8 potential bit flags per frame.    // They are specific to the frame type.    Flags http2Flags
    // Length is the length of the frame, not including the 9 byte header.    // The maximum size is one byte less than 16MB (uint24), but only    // frames up to 16KB are allowed without peer agreement.    Length uint32
    // StreamID is which stream this frame is for. Certain frames    // are not stream-specific, in which case this field is 0.    StreamID uint32}
// A FrameType is a registered frame type as defined in// http://http2.github.io/http2-spec/#rfc.section.11.2type http2FrameType uint8
const (    http2FrameData         http2FrameType = 0x0    http2FrameHeaders      http2FrameType = 0x1    http2FramePriority     http2FrameType = 0x2    http2FrameRSTStream    http2FrameType = 0x3    http2FrameSettings     http2FrameType = 0x4    http2FramePushPromise  http2FrameType = 0x5    http2FramePing         http2FrameType = 0x6    http2FrameGoAway       http2FrameType = 0x7    http2FrameWindowUpdate http2FrameType = 0x8    http2FrameContinuation http2FrameType = 0x9)
   复制代码
 
每个 Frame 都包含一个 http2FrameHeader,这个是每个 Frame 都有的头信息,在 HTTP/2 的定义中如下:
  +-----------------------------------------------+ |                 Length (24)                   | +---------------+---------------+---------------+ |   Type (8)    |   Flags (8)   | +-+-------------+---------------+-------------------------------+ |R|                 Stream Identifier (31)                      | +=+=============================================================+ |                   Frame Payload (0...)                      ... +---------------------------------------------------------------+
   复制代码
 
能看到其结构分别对应头信息的一些字段。
然后我们以 Headers Frame 为例看下:
 // A HeadersFrame is used to open a stream and additionally carries a// header block fragment.type http2HeadersFrame struct {    http2FrameHeader
    // Priority is set if FlagHeadersPriority is set in the FrameHeader.    Priority http2PriorityParam
    headerFragBuf []byte // not owned}
// PriorityParam are the stream prioritzation parameters.type http2PriorityParam struct {    // StreamDep is a 31-bit stream identifier for the    // stream that this stream depends on. Zero means no    // dependency.    StreamDep uint32
    // Exclusive is whether the dependency is exclusive.    Exclusive bool
    // Weight is the stream's zero-indexed weight. It should be    // set together with StreamDep, or neither should be set. Per    // the spec, "Add one to the value to obtain a weight between    // 1 and 256."    Weight uint8}
  +---------------+ |Pad Length? (8)| +-+-------------+-----------------------------------------------+ |E|                 Stream Dependency? (31)                     | +-+-------------+-----------------------------------------------+ |  Weight? (8)  | +-+-------------+-----------------------------------------------+ |                   Header Block Fragment (*)                 ... +---------------------------------------------------------------+ |                           Padding (*)                       ... +---------------------------------------------------------------+
   复制代码
 
http2PriorityParam 表示了 Stream Dependency 和 Weight 信息,headerFragBuf 表示 Header Block Fragment, Padded 信息没有设置单独的结构存储,因为没啥特别的地方会用到,是否存在 Pad 信息放在了 Frame Header 的 Flag 信息里,当 Flags.Has(http2FlagHeadersPadded)时,会取出 Pad 的长度,并在取数据时删减掉。
 // Frame-specific FrameHeader flag bits.const (    //  ...
    // Headers Frame    http2FlagHeadersEndStream  http2Flags = 0x1    http2FlagHeadersEndHeaders http2Flags = 0x4    http2FlagHeadersPadded     http2Flags = 0x8    http2FlagHeadersPriority   http2Flags = 0x20
    // ...)
    // 计算Pad的长度    var padLength uint8    if fh.Flags.Has(http2FlagHeadersPadded) {        if p, padLength, err = http2readByte(p); err != nil {            return        }    }
    // ...
    // 取出 Header Block Fragment    hf.headerFragBuf = p[:len(p)-int(padLength)]
   复制代码
 http2Framer
Frame 的读写操作是通过 http2Framer 来进行的。
 // A Framer reads and writes Frames.type http2Framer struct {    r         io.Reader    lastFrame http2Frame    errDetail error
    lastHeaderStream uint32
    maxReadSize uint32    headerBuf   [http2frameHeaderLen]byte
    getReadBuf func(size uint32) []byte    readBuf    []byte // cache for default getReadBuf
    maxWriteSize uint32 // zero means unlimited; TODO: implement
    w    io.Writer    wbuf []byte
    // ....}// http2Framer的操作方法type http2Framer    func http2NewFramer(w io.Writer, r io.Reader) *http2Framer    func (fr *http2Framer) ErrorDetail() error    func (fr *http2Framer) ReadFrame() (http2Frame, error)    // ...    func (f *http2Framer) WriteData(streamID uint32, endStream bool, data []byte) error    // ...    func (f *http2Framer) WriteHeaders(p http2HeadersFrameParam) error    // ...    func (f *http2Framer) WritePushPromise(p http2PushPromiseParam) error    func (f *http2Framer) WriteRSTStream(streamID uint32, code http2ErrCode) error    // ...
   复制代码
 
可以看到,通过 http2Framer,我们可以很方便的对 http2Frame 进行读写操作,比如 http2Framer.ReadFrame,http2Framer.WritHeaders 等。
http2Framer 是在 http2Server.ServeConn 阶段初始化的:
 func (s *http2Server) ServeConn(c net.Conn, opts *http2ServeConnOpts) {    baseCtx, cancel := http2serverConnBaseContext(c, opts)    defer cancel()
    sc := &http2serverConn{        srv:                         s,        hs:                          opts.baseConfig(),        conn:                        c,        baseCtx:                     baseCtx,        remoteAddrStr:               c.RemoteAddr().String(),        bw:                          http2newBufferedWriter(c),        // ...    }
    // ...
    // 将conn交接给http2Framer进行最小粒度的Frame读写.    fr := http2NewFramer(sc.bw, c)    fr.ReadMetaHeaders = hpack.NewDecoder(http2initialHeaderTableSize, nil)    fr.MaxHeaderListSize = sc.maxHeaderListSize()    fr.SetMaxReadFrameSize(s.maxReadFrameSize())    sc.framer = fr}
   复制代码
 
然后在 serve 阶段通过 readFrames()和 writeFrame 进行 Frame 的读写操作。
 func (sc *http2serverConn) serve() {    // ...    go sc.readFrames()  // 读取Frame    // ...    select {    case wr := <-sc.wantWriteFrameCh:        sc.writeFrame(wr) // 写Frame    // ...    }    // ...}
   复制代码
 
最后还有一点,就是当我们通过调用了 w.Header().Add()方法设置了 Header 之后,如何马上让服务端把这些信息响应到客户端呢,这个时候就是通过 Flush()方法了。
 // Optional http.ResponseWriter interfaces implemented.var (    _ CloseNotifier     = (*http2responseWriter)(nil)    _ Flusher           = (*http2responseWriter)(nil)    _ http2stringWriter = (*http2responseWriter)(nil))
// ...
func (w *http2responseWriter) Flush() {    rws := w.rws    if rws == nil {        panic("Header called after Handler finished")    }    if rws.bw.Buffered() > 0 {        if err := rws.bw.Flush(); err != nil {            // Ignore the error. The frame writer already knows.            return        }    } else {        // The bufio.Writer won't call chunkWriter.Write        // (writeChunk with zero bytes, so we have to do it        // ourselves to force the HTTP response header and/or        // final DATA frame (with END_STREAM) to be sent.        rws.writeChunk(nil)    }}
// ...
func (rws *http2responseWriterState) writeChunk(p []byte) (n int, err error) {    if !rws.wroteHeader {        rws.writeHeader(200)    }
    isHeadResp := rws.req.Method == "HEAD"    if !rws.sentHeader {        // ...        err = rws.conn.writeHeaders(rws.stream, &http2writeResHeaders{            streamID:      rws.stream.id,            httpResCode:   rws.status,            h:             rws.snapHeader,            endStream:     endStream,            contentType:   ctype,            contentLength: clen,            date:          date,        })    }    // ...}
   复制代码
 
通过调用 Flush()方法,由于我们没有设置任何 body 的内容,所以会走到 rws.WriteChunk(nil)逻辑处,这里就是为了在没有内容时,如果希望给客户端响应,来发送 Headers Frame,这里也可以选择在 Header Frame 携带 END_STREAM 来关闭 Stream,这种是我们在 Go 中正常响应 HEAD 请求时的逻辑,如果我们自己通过 Flush 来发送,那么就不会有 END_STREAM,就达到我们的要求了。
ok,至此,整个流程就串起来了。
本文转载自公众号 360 云计算(ID:hulktalk)。
原文链接:
https://mp.weixin.qq.com/s/3IgNBUJpHXKxp6wT6A8EjA
评论