写点什么

HTTP/2 in GO(三)

  • 2019-11-18
  • 本文字数:5895 字

    阅读完需:约 19 分钟

HTTP/2 in GO(三)

本次讲一个非常简单的功能,然后把其内部实现串一下。


这次要实现的功能非常简单,就是一个 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
复制代码


  • -k 参数表示忽略证书校验,避免客户端拿到证书后校验不通过而拒绝链接

  • -i 参数表示显示返回的 header 信息

  • –http2 表示启用 http/2,这个参数也可以不带,因为客户端支持的话,会优先使用 http/2 去链接,服务端不支持的时候降级到 http/1.1



这样就实现了只返回了一个 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


2019-11-18 22:581884

评论

发布
暂无评论
发现更多内容

什么是iPaaS?iPaaS选型、落地及案例分析

RestCloud

数据集成 应用集成 ipaas

文档转换API功能与特性全景:选择指南与应用洞察

幂简集成

API

在Vue3中如何实现四种全局状态数据的统一管理?

不在线第一只蜗牛

JavaScript vue.js 前端

阿里云可观测 2024 年 9 月产品动态

阿里巴巴云原生

阿里云 云原生 可观测

拍立淘API返回值在商品数据分析中的应用

代码忍者

pinduoduo API API 性能测试

数据仓库 Palo 2.0 for Apache Doris 冷热分离原理分析

Baidu AICLOUD

数据仓库 数据仓库服务

活动预告|博睿数据将受邀出席GOPS全球运维大会上海站!

博睿数据

《Programming from the Ground Up》读后感

codists

汇编 编程人

倒计时1天 | 第三届OpenHarmony技术大会——明天,上海见!

OpenHarmony开发者

云桌面VS传统PC:企业用户该如何取舍

青椒云云电脑

云桌面

内核级流量治理引擎Kmesh八大新特性解读

华为云开发者联盟

服务网格 ebpf Sidecar Kmesh

精准监控,高效运营 —— 商品信息实时分析为商家带来新机遇

技术冰糖葫芦

API 接口 API 文档 API 测试 pinduoduo API API 性能测试

Spring Boot3集成 LiteFlow 实现业务流程编排

江南一点雨

鸿蒙原生应用开发者激励计划发布,冲刺HarmonyOS NEXT正式商用

最新动态

ECCV 2024 亮点!RoboTwin:首个双臂协同机器人策略学习Benchmark

松灵机器人

前沿科技 人工智能’ 具身智能 松灵机器人 双臂智能机器人

CocosCreator 快速部署 TON 游戏:Web2 游戏如何使用 Ton支付

股市老人

登顶!智源BGE首开国产模型Hugging Face月度下载全球第一

智源研究院

一文读懂HyperWorks的耦合求解功能

智造软件

CAE altair hyperworks

天猫商品描述API返回值中的商品参数对比与竞品分析

技术冰糖葫芦

API 接口 API 文档 API 测试 API 性能测试

软件测试学习笔记丨二叉树:添加练习

测试人

软件测试

老韩运维知识解析系列02:深入理解网络监控指标与实战应用

Geek_a83400

淘宝商品详情API返回值:深度挖掘其业务价值

代码忍者

pinduoduo API API 性能测试

博睿数据Bonree ONE全面适配HarmonyOS NEXT,守护鸿蒙原生应用稳健前行

博睿数据

如何挑选CDN加速器节点网络?

Ogcloud

CDN 网络加速 CDN加速 企业网络加速 CDN网络加速

Python:条件分支 if 语句全讲解

不在线第一只蜗牛

Python

繁星·数智思享会第2期:流程挖掘,全知视角驱动业务增长

望繁信科技

流程挖掘 流程资产 流程智能 望繁信科技 数字换转型

HTTP/2 in GO(三)_文化 & 方法_付坤_InfoQ精选文章