写点什么

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

评论

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

使用logstash作为docker日志驱动收集日志

远鹏

Docker Logstash 日志系统

长篇总结之JavaScript,巩固前端基础

我是哪吒

JavaScript 程序员 面试 大前端 2月春节不断更

终于搞懂了Python模块之间的相互引用问题

华为云开发者联盟

Python 路径 代码 模块

使用V8和node轻松profile分析nodejs应用程序

程序那些事

性能优化 Profile nodejs 性能分析 程序那些事

工业绿色哪家强?3D可视化盾构机构建隧道,推动设备预测性运维

一只数据鲸鱼

物联网 数据可视化 绿色交通 绿色工业 盾构机

并发队列:PriorityBlockingQueue和DelayQueue案例使用

叫练

并发编程 队列 优先级队列 条件队列 并发队列

零基础实现Java直播(二):实现流程

ZEGO即构

面试时遇到一致性哈希算法这样回答会让面试官眼前一亮

中间件兴趣圈

负载均衡 面试 一致性哈希

浪潮云洲工业互联网平台创新实践,受权威机构关注!

工业互联网

看KubeEdge携手K8S,如何管理中国高速公路上的10万边缘节点

华为云开发者联盟

Kubernetes 微服务 kubeedge 边缘

关于京东技术,你想了解的都在这里丨征文活动获奖及优秀专栏推荐

京东科技开发者

京东 征文大赛

Elasticsearch 搜索结果解析

escray

elastic 七日更 死磕Elasticsearch 60天通过Elastic认证考试 2月春节不断更

一款基于 Web 的通用数据管理工具(转载)

BinTools图尔兹

数据库 运维 开发工具 dba 数据管理工具

天天向上跑分模式介绍开发

luluhulian

电子书下载|2020 年云原生年货小红书来啦!

阿里巴巴云原生

容器 微服务 开发者 云原生 k8s

Kafka 发送消息过程中拦截器的用途?

李尚智

Java kafka 程序员 架构

图解分析:Kafka 生产者客户端工作原理

李尚智

Java 程序员 架构 kafak

2021版阿里Java亿级并发设计手册:基础+数据库+缓存+消息队列+分布式+维护+实战

Java架构追梦

Java 阿里巴巴 面试 系统架构 亿级并发

架构师训练营大作业

Geek_f06ede

2021年的十五个DevOps趋势预测

禅道项目管理

DevOps 微服务 自动化 趋势

redis分布式锁的这些坑,我怀疑你是假的开发

华为云开发者联盟

redis 分布式 分布式锁 服务器 value

目前的区块链IPFS矿机模式交易系统开发的简单解析

v16629866266

程序员成长第四篇:程序员的职业天花板

石云升

28天写作 2月春节不断更 职场天花板

产品经理:一款用户全量会话轨迹管理产品是怎么炼成的?

博睿数据

【无偿分享】史上最全Python学习大礼包

sum56

Python 学习 学习方法 python 爬虫 资料整理

深度集成 Flink: Apache Iceberg 0.11.0 最新功能解读

Apache Flink

flink

即构发布 LCEP 低代码互动平台产品 RoomKit,实现互动房间0代码搭建

ZEGO即构

IDEA永久注册码来了!!!(支持2020.3.1版本)

程序员生活志

IDEA

CSS开发过程中的20个快速提升技巧

华为云开发者联盟

CSS

架构师训练营大作业(二)

Geek_f06ede

【JS】事件委托

德育处主任

JavaScript 大前端 js 2月春节不断更

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