NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

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

评论

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

Dapp系统开发及智能合约部署技术

薇電13242772558

智能合约 dapp

【超级账本开发者系列】专访——肖慧 : 不忘初心,方得始终

旺链科技

区块链 技术 超级账本

九章云极DataCanvas APS机器学习平台获得中国信通院“领先级”评级

九章云极DataCanvas

人工智能 机器学习 大数据 数据分析 数据智能

暴雨天,看天翼云如何“快准稳”防涝

天翼云开发者社区

云计算 大数据 云平台

MySQL 原理与优化:Limit 查询优化

老崔说架构

6大论坛,30+技术干货议题,2022首届阿里巴巴开源开放周来了!

阿里云数据库开源

阿里巴巴 开源 开源数据库 阿里巴巴开源开放周 阿里技术

为什么我要迁移 SpringBoot 到函数计算

阿里巴巴云原生

阿里云 云原生 springboot 函数计算

看得见风和日丽,看不见风起“云”涌

天翼云开发者社区

云计算 大数据

【活动报名】8月13日杭州站-开源遇上大数据

亚马逊云科技 (Amazon Web Services)

大数据 开源

融云 x N 世界:构建无限用户实时交互的「元宇宙会场」

融云 RongCloud

isc 元宇宙

6大论坛,30+技术干货议题,2022首届阿里巴巴开源开放周来了!

阿里技术

数据库 云计算 大数据 #开源

玩转云端 | 天翼云电脑的百变玩法

天翼云开发者社区

云平台 云电脑

开源星「001 号」落地 FlyFish,欢迎登陆赢神秘大礼包!

云智慧AIOps社区

大前端 低代码 开源项目 数据可视化 大屏可视化

视频聊天源码——一对一直播如何提高直播质量?

开源直播系统源码

软件开发 直播系统源码 开源源码 语音聊天 视频聊天源码

真香现场!全网首发Spring5秘籍手册+架构导图,我爱了.

冉然学Java

Spring5 技术栈 笔记 java 日志 构架

深入理解Linux I/O系统

C++后台开发

性能优化 Linux内核 网络io C/C++后台开发 C/C++开发

大咖说·对话生态|当Confluent遇见云:实时流动的数据更有价值

大咖说

数字化转型 Confluent 大数据应用

SpringBatch入门

五毛

sping ETL

现在,怎么挑选舞台租赁LED显示屏?

Dylan

LED LED显示屏

走得通,看得见!你的交通“好帮手”

天翼云开发者社区

云计算 大数据 AI

Python零基础入门的第一天——开发环境的搭建

北极的三哈

学习 后端 开发语言 Python.

Python语言基本语法元素

北极的三哈

Python 学习 开发语言

如何通过 open-local 玩转容器本地存储? | 龙蜥技术

OpenAnolis小助手

开源 云原生 分布式存储 龙蜥技术 open-local

[ Kitex 源码解读 ] Kitex 请求重试的分类及实现原理

baiyutang

Go 微服务架构 云原生 kitex CloudWeGo

vivo手机上的系统级消息推送平台的架构设计实践

JackJiang

网络编程 架构设计 消息推送 即时通讯 即时通讯IM

从函数计算到 Serverless 架构

hum建应用专家

云计算 Serverless

防汛添利器,数字技术筑起抗洪“大堤”

天翼云开发者社区

云计算 大数据

架构实战营第九模块作业-毕业项目

Geek_53787a

新思科技推动产业革新 为智能网联车系好“安全带”

InfoQ_434670063458

软件 车联网 新思科技

开源一夏 | 使用 HTML、CSS 和 JS 制作一个中国象棋

海拥(haiyong.site)

开源 前端 8月月更

2022年中国第三方证券APP创新专题分析

易观分析

App 金融 证券

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