写点什么

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

评论

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

React Fragment介绍与使用

编程江湖

React

aPaaS将如何改变软件行业?

华为云开发者联盟

云计算 软件 软件开发 SaaS aPaaS

做了一份前端面试复习计划,保熟~

CRMEB

vivo 评论中台的流量及数据隔离实践

vivo互联网技术

mongodb 中台 分布式

卷起来了!软件开发正在越来越快……

SoFlu软件机器人

百度APP视频播放中的解码优化

百度Geek说

百度 性能优化 视频

大数据开发之运维面试题汇总分享

@零度

面试题 大数据运维

如何解决Python项目在VScode中跨文件夹导包问题

IT蜗壳-Tango

IT蜗壳教学 2月月更

零代码技能平台技术实践探索

OPPO小布助手

人工智能 低代码 零代码 智能助手 对话系统

Java11特性-效能翻倍的HttpClient

蜜糖的代码注释

Java 后端技术 2月月更

探秘 k8e:极简 Kubernetes 发行版

张晓辉

Kubernetes 云原生

渗透测试之中间件漏洞复现

网络安全学海

黑客 网络安全 信息安全 渗透测试 安全漏洞

新思科技BSIMM评估为安全团队提供“他山之石”

InfoQ_434670063458

汽车 新思科技 软件定义汽车 软件安全 BSIMM评估

sql 学习笔记

孙青

Moviepy+OpenCV-python结合进行音视频剪辑处理 | 社区征文

老猿Python

音视频 Moviepy 数字图像处理 新春征文 OpenCV-Python

分配任务的人才是团队的核心成员

panda

管理 限界上下文 任务分配

盘点冬奥会上的那些“黑科技”,你也可以掌握它!

博文视点Broadview

如何让代码代码管理变的更安全更高效 | 云效开发篇

阿里云云效

阿里云 DevOps 云原生 代码管理 Codeup

如何帮助金融客户“用好云”?

阿里云云效

阿里云 运维 云原生 云平台 阿里云混合云

一文读懂 Serverless 的起源、发展和落地实践

Serverless Devs

2月月更

从原理到操作,让你在 APISIX 中代理 Dubbo 服务更便捷

API7.ai 技术团队

微服务网关

Ceph核心概念解读

总想做点什么

分布式存储 Ceph 云存储 云计算,

Spring Boot Serverless 实战 | Serverless 应用的监控与调试

Serverless Devs

如何用建木CI导入导出Redis数据

Jianmu

DevOps CI/CD Redis 数据结构

Kubernetes HPA 基于 Prometheus 自定义指标的可控弹性伸缩

张晓辉

Kubernetes 弹性 HPA

优秀程序员的30种思维--行为准则篇(11/100)

hackstoic

辩论这件事,其实不止奇葩说

Taylor

面向推理训练一体化的 MNN 工作台

阿里巴巴终端技术

端智能

人手一个数字人还有多远?百度吴甜做客央视《对话》

百度大脑

Lazada D11 体验升级技术实践

阿里巴巴终端技术

ios android 客户端开发 移动端 体验优化

网易传媒Go语言探索

月读

golang 开源治理

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