写点什么

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

评论

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

Java基础总结三(泛型、异常,阿里java面试题及答案

Java 程序员 后端

第二模块作业

River Tree

架构实战营

Java基础面试题整理,java数组排序sort原理

Java 程序员 后端

Java中高级核心知识全面解析——Dubbo,java语言入门自学书

Java 程序员 后端

Java反射的深入浅出,mongodb优化面试

Java 程序员 后端

Java之StringUtils的常用方法,java整体项目架构图

Java 程序员 后端

JAVA代码审计之Shiro反序列化漏洞分析,浦发银行Java开发笔试题

Java 程序员 后端

Java基础面试题——IO流,springboot实战项目百度云

Java 程序员 后端

Java多线程 高频面试题,java架构师工作内容

Java 程序员 后端

Java基础09 面向对象~类和对象,阿里面试官必问

Java 程序员 后端

JavaWeb快速入门--Tomcat,java高级特性面试

Java 程序员 后端

java中常用单词系列(一),最新Java高级面试题汇

Java 程序员 后端

Java中的几种线程池详解,rabbitmqpdf百度云

Java 程序员 后端

Spring是如何自动注入多类型的?

编号94530

spring bean @Autowired 自动注入

Java内存问题 及 LeakCanary 原理分析,mybatis架构设计层次

Java 程序员 后端

Java基础 - 单例(饿汉、懒汉),Runtime类,java教程推荐学客巴巴

Java 程序员 后端

Java基础 - 多态,抽象类,接口,linux源码学习

Java 程序员 后端

Java基础27~使用JDBC+连接池,这篇文章可以满足你80%日常工作

Java 程序员 后端

架构实战营模块二学习总结

吴霏

「架构实战营」

Java中高级核心知识全面解析-容器(ArrayList)

Java 程序员 后端

Java多态大乱炖,mysql基础教程视频

Java 程序员 后端

Java如何支持函数式编程?,大专生面试阿里P7居然过了

Java 程序员 后端

Java中当对象不再使用时,不赋值为null会导致什么后果?

Java 程序员 后端

Java中的泛型,java程序执行过程与编译原理

Java 后端

Java几种常用JSON库性能比较,java接口开发面试

Java 程序员 后端

Java初学01:学习路线,java基础入门清华大学出版社

Java 程序员 后端

Java基础25~XML和JSON解析,springbootoauth2教程

Java 程序员 后端

Java个人学习之旅(第十天),java就业班百度网盘

Java 程序员 后端

Java中高级核心知识全面解析——常用框架(Spring常用注解

Java 程序员 后端

Java之Spring基础与IOC,java实用教程第五版答案第四章

Java 程序员 后端

架构实战模块二作业

吴霏

「架构实战营」

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