HTTP/2 in GO(四)

2019 年 11 月 18 日

HTTP/2 in GO(四)

上篇文章我们了解了如何在 HTTP/2 server 端进行 Header 信息的发送,同时保持连接不断开。这次我们在这个基础上,实现自动下发 PUSH。


Start


上篇文章我们了解了如何在 HTTP/2 server 端进行 Header 信息的发送,同时保持连接不断开。这次我们在这个基础上,实现自动下发 PUSH。


先来实现一个最简单的 Server Push 的例子, 我们在上次的 demo 基础上继续改进


package main
import ( "html/template" "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 {} })
// 用于push的 handler http.HandleFunc("/crt", func(w http.ResponseWriter, r *http.Request) { tpl := template.Must(template.ParseFiles("server.crt")) tpl.Execute(w, nil) })
// 请求该Path会触发Push http.HandleFunc("/push", func(w http.ResponseWriter, r *http.Request) { pusher, ok := w.(http.Pusher) if !ok { log.Println("not support server push") } else { err := pusher.Push("/crt", nil) if err != nil { log.Printf("Failed for server push: %v", err) } } w.WriteHeader(http.StatusOK) })
log.Println("start listen on 8080...") log.Fatal(http.ListenAndServeTLS(":8080", "server.crt", "server.key", nil))}
复制代码


以上代码添加了两个 Hanlder,一个是 /crt,返回我们的证书内容,这个是用来给做客户端 push 的内容。另一个是 /push,请求该链接时,我们会将 /crt 的内容主动 push 到客户端。


GO 服务启动后,我们通过 h2c 来访问下/push :


先在一个终端通过 h2c start -d 启动进行输出显示,然后另外开一个终端窗口发起请求 h2c connect localhost:8080 和 h2c get /push :



来解读下这个请求中都发生了什么:


1.客户端通过 stream id=1 发送 HEADERS FRAME 进行请求,请求 Path 是 /push


2.服务端在 stream id=1 中返回一个 PUSH_PROMISE(配合下表食用) ,携带了部分 Header 信息,承诺会在 stream id=2 中返回 path: /crt 的相关信息,这里相当于告诉客户端,如果你接下来需要请求 /crt 的时候,就不要请求了,这个内容我一会就给你发过去了。


3.服务端正常响应 get /push 的请求,返回了对应的 Header 信息,并通过 END_STREAM 表示此 stream 的交互完成了。


4.服务端通过 stream id=2 下发 /crt 的相关信息,第四步是返回的 Header 信息.


5.服务端通过 stream id=2 下发 /crt 的相关 DATA 信息, 并通过 END_STREAM 表示承诺的 /crt 的内容发送完毕。


 // PUSH_PROMISE Frame结构 +---------------+ |Pad Length? (8)| +-+-------------+-----------------------------------------------+ |R|                  Promised Stream ID (31)                    | +-+-----------------------------+-------------------------------+ |                   Header Block Fragment (*)                 ... +---------------------------------------------------------------+ |                           Padding (*)                       ... +---------------------------------------------------------------+
复制代码


通过这个例子,我们应该就掌握了 Server Push 的用法,在此基础上,我们结合上一章讲到的内容,再改进一下,实现 “服务端定时主动 PUSH”:


// 服务端定时 "主动" push内容http.HandleFunc("/autoPush", func(w http.ResponseWriter, r *http.Request) {    w.Header().Add("X-custom-header", "custom")    w.WriteHeader(http.StatusNoContent)
if f, ok := w.(http.Flusher); ok { f.Flush() } pusher, ok := w.(http.Pusher) if ok { for { select { case <-time.Tick(5 * time.Second): err := pusher.Push("/crt", nil) if err != nil { log.Printf("Failed for server push: %v", err) } } } }})
复制代码


效果如图:



服务端一直发送 PUSH_PROMISE 消息给客户端,每次间隔 5s,并且每次 Promised Strea Id 都在偶数范围内进行递增 2,4,6,8,10…


这个例子里,我们用了一个 for 循环 和一个定时器 time.Tick ,在服务端返回不带 END_STREAM 的 Headers 后,每隔 5s 向客户端主动 Push 一个内容,这里我们 Push 的内容是固定的,在实际应用场景中,可以从一个特定的 channel 中取出需要下发的消息,然后再动态的构造请求的 path,可以是携带参数的,来实现动态的控制需要 Push 什么内容。这样就实现了 “服务端主动 PUSH” 的功能。


HTTP/2 PUSH in Go


接下来看下 Server Push 在 Go 中的实现:


// Push implements http.Pusher.func (w *http2responseWriter) Push(target string, opts *PushOptions) error {    internalOpts := http2pushOptions{}    if opts != nil {        internalOpts.Method = opts.Method        internalOpts.Header = opts.Header    }    return w.push(target, internalOpts)}
func (w *http2responseWriter) push(target string, opts http2pushOptions) error { // ... // Push只能是对 GET or HEAD 方法 if opts.Method != "GET" && opts.Method != "HEAD" { return fmt.Errorf("method %q must be GET or HEAD", opts.Method) } // 构造要Push的内容的请求 msg := &http2startPushRequest{ parent: st, method: opts.Method, url: u, header: http2cloneHeader(opts.Header), done: http2errChanPool.Get().(chan error), } // 在客户端连接断开或者END_STREAM之前可以发送PUSH,把构造好的PushRequest放到 sc.serveMsgCh channel 里 select { case <-sc.doneServing: return http2errClientDisconnected case <-st.cw: return http2errStreamClosed case sc.serveMsgCh <- msg: }}// 在serve中会 取出 sc.serveMsgCh 中的消息进行对应的操作,当取到 PushRequest 时,就会发送Push消息func (sc *http2serverConn) serve() { // ... loopNum := 0 for { loopNum++ select { // ... case msg := <-sc.serveMsgCh: switch v := msg.(type) { // ... case *http2startPushRequest: sc.startPush(v) // ... } } }}func (sc *http2serverConn) startPush(msg *http2startPushRequest) { // ... // 获取Prosise的Stream id,当真正要发送PUSH_PROMISE时才进行获取,并且同时异步启动需要Push的Handler的请求. allocatePromisedID := func() (uint32, error) { // ... sc.maxPushPromiseID += 2 promisedID := sc.maxPushPromiseID // 新建Stream用于push内容的发送 promised := sc.newStream(promisedID, msg.parent.id, http2stateHalfClosedRemote) rw, req, err := sc.newWriterAndRequestNoBody(promised, http2requestParam{ method: msg.method, scheme: msg.url.Scheme, authority: msg.url.Host, path: msg.url.RequestURI(), header: http2cloneHeader(msg.header), }) // ...
// 进行handle请求 go sc.runHandler(rw, req, sc.handler.ServeHTTP) return promisedID, nil } // 构造好 PUSH_PROMISE, 开始发送 sc.writeFrame(http2FrameWriteRequest{ write: &http2writePushPromise{ streamID: msg.parent.id, method: msg.method, url: msg.url, h: msg.header, allocatePromisedID: allocatePromisedID, }, stream: msg.parent, done: msg.done, })}
复制代码


Done.


本文转载自公众号 360 云计算(ID:hulktalk)。


原文链接:


https://mp.weixin.qq.com/s/kKQuv9tK791eFxmqGRWy2Q


2019 年 11 月 18 日 22:58135

评论

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

以为是青铜,没想到是王者的dubbo标签路由

小楼

dubbo

如何在非 sudo 用户下运行 docker 命令?

愚一

Docker DevOps

Ledge:这可能是距今最好的『DevOps + 研发效能』知识平台

Phodal

DevOps 敏捷开发 软件开发 研发效能

要不要重新认识一下递归与迭代?

西了意

编程

IPFS 星际传输协议的入门(二)

AIbot

区块链 分布式数据库

用jdk8的stream实现斐波那契数列

编号94530

jdk stream 斐波那契 fibonacci

什么是物联网中台

老任物联网杂谈

物联网中台 IOT Platform 物联网平台

centos7.6操作系统安装

桥哥技术之路

Linux

Sentinel在docker中获取CPU利用率的一个BUG

小楼

Java sentinel cpu

一个工程师向电信公司的维权

记一次spring注解@Value不生效的深度排查

小楼

spring Spring Boot dubbo

skywalking内存泄露排查

小楼

dubbo 内存泄露

C++数组可以为变量吗

程序喵大人

c++ 互联网 编程语言

Docker运行常用软件:MySQL,Redis,Nginx,RabbitMQ,Neuxs,Gitlab

读钓

MySQL nginx Docker gitlab

在Kubernetes上运行SpringBoot应用

铁花盆

Docker Kubernetes Spring Boot

广告与数据算法系列1.1.1: 什么是广告

黄崇远@数据虫巢

互联网 算法 广告

SpringBoot中如何优雅的使用多线程

读钓

Java spring Spring Boot

当dubbo多注册中心碰上标签路由

小楼

dubbo

LeetCode 前1000题二叉树题目系统总结

Yano

面试 算法 LeetCode 二叉树 刷题

Linux系统优化

桥哥技术之路

Linux

一次漫长的dubbo网关内存泄露排查经历

小楼

dubbo 内存泄露

项目实施要避免哪些坑?

顾强

项目管理

MySQL死锁与Spring事务

Dean

MySQL

MacOS配置网络命令

编程随想曲

macos network

Django 中如何优雅的记录日志

AlwaysBeta

Python django Web 后端

格局不行,有机会也抓不住

池建强

创业 格局 MacTalk

零基础应该如何学习爬虫技术?

极客时间

Python 编程 爬虫

Apache Beam 大数据处理一站式分析

李孟

Java 大数据 数据中台 数据交换 Beam

思维导图学《Linux性能优化实战》

Yano

Linux 后端

nacos的一致性协议distro介绍

小楼

nacos

身为程序员,怎么接私活赚外快?

爱看书的小代码

HTTP/2 in GO(四)-InfoQ