FinOps有望降低企业50%+的云成本! 了解详情
写点什么

Go 语言 HTTP/2 探险之旅

  • 2019-11-20
  • 本文字数:4683 字

    阅读完需:约 15 分钟

Go语言HTTP/2探险之旅

大家都知道,Go 的标准库 HTTP 服务器默认支持 HTTP/2。那么,在这篇文章中,我们将首先展示 Go 的 http/2 服务器功能,并解释如何将它们作为客户端使用。


在这篇文章中,我们将首先展示 Go 的 http/2 服务器功能,并解释如何将它们作为客户端使用。Go 的标准库 HTTP 服务器默认支持 HTTP/2。

HTTP/2 服务器

首先,让我们在 Go 中创建一个 http/2 服务器!根据 http/2 文档,所有东西都是为我们自动配置的,我们甚至不需要导入 Go 的标准库 http2 包:


HTTP/2 强制使用 TLS。为了实现这一点,我们首先需要一个私钥和一个证书。在 Linux 上,下面的命令执行这个任务。


openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt
复制代码


该命令将生成两个文件:server.key 以及 server.crt


现在,对于服务器代码,以最简单的形式,我们将使用 Go 的标准库 HTTP 服务器,并启用 TLS 与生成的 SSL 文件。


package main
import ( "log" "net/http") func main() { // 在 8000 端口启动服务器 // 确切地说,如何运行HTTP/1.1服务器。
srv := &http.Server{Addr:":8000", Handler: http.HandlerFunc(handle)} // 用TLS启动服务器,因为我们运行的是http/2,它必须是与TLS一起运行。 // 确切地说,如何使用TLS连接运行HTTP/1.1服务器。 log.Printf("Serving on https://0.0.0.0:8000") log.Fatal(srv.ListenAndServeTLS("server.crt", "server.key"))}
func handle(w http.ResponseWriter, r *http.Request) { // 记录请求协议 log.Printf("Got connection: %s", r.Proto) // 向客户发送一条消息 w.Write([]byte("Hello"))}
复制代码

HTTP/2 客户端

在 go 中,标准 http.Client 也用于 http/2 请求。惟一的区别是在客户端的 Transport 字段,使用 http2.Transport 代替 http.Transport。


我们生成的服务器证书是“自签名”的,这意味着它不是由一个已知的证书颁发机构(CA)签署的。这将导致我们的客户端不相信它:


package main
import ( "fmt" "net/http")
const url = "https://localhost:8000"
func main() { _, err := http.Get(url) fmt.Println(err)}
复制代码


让我们试着运行它:


$ go run h2-client.go Get https://localhost:8000: x509: certificate signed by unknown authority
复制代码


在服务器日志中,我们还将看到客户端(远程)有一个错误:


http: TLS handshake error from [::1]:58228: remote error: tls: bad certificate
复制代码


为了解决这个问题,我们可以用定制的 TLS 配置去配置我们的客户端。我们将把服务器证书文件添加到客户端“证书池”中,因为我们信任它,即使它不是由已知 CA 签名的。


我们还将添加一个选项,根据命令行标志在 HTTP/1.1 和 HTTP/2 传输之间进行选择。


package main
import ( "crypto/tls" "crypto/x509" "flag" "fmt" "io/ioutil" "log" "net/http" "golang.org/x/net/http2") const url = "https://localhost:8000"
var httpVersion = flag.Int("version", 2, "HTTP version")
func main() { flag.Parse() client := &http.Client{} // Create a pool with the server certificate since it is not signed // by a known CA caCert, err := ioutil.ReadFile("server.crt") if err != nil { log.Fatalf("Reading server certificate: %s", err) } caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) // Create TLS configuration with the certificate of the server tlsConfig := &tls.Config{ RootCAs: caCertPool, }
// Use the proper transport in the client switch *httpVersion { case 1: client.Transport = &http.Transport{ TLSClientConfig: tlsConfig, } case 2: client.Transport = &http2.Transport{ TLSClientConfig: tlsConfig, } } // Perform the request resp, err := client.Get(url)
if err != nil { log.Fatalf("Failed get: %s", err) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Fatalf("Failed reading response body: %s", err) } fmt.Printf( "Got response %d: %s %s\n", resp.StatusCode, resp.Proto, string(body) )}
复制代码


这一次我们得到了正确的回应:


$ go run h2-client.go Got response 200: HTTP/2.0 Hello
复制代码


在服务器日志中,我们将看到正确的日志线:获得连接:Got connection: HTTP/2.0!!


但是当我们尝试使用 HTTP/1.1 传输时,会发生什么呢?


$ go run h2-client.go -version 1Got response 200: HTTP/1.1 Hello
复制代码


我们的服务器对 HTTP/2 没有任何特定的东西,所以它支持 HTTP/1.1 连接。这对于向后兼容性很重要。此外,服务器日志表明连接是 HTTP/1.1:Got connection: HTTP/1.1。

HTTP/2 高级特性

我们创建了一个 HTTP/2 客户机-服务器连接,并且我们正在享受安全有效的连接带来的好处。但是 HTTP/2 提供了更多的特性,让我们来研究它们!

服务器推送

HTTP/2 允许服务器推送“使用给定的目标构造一个合成请求”。


这可以很容易地在服务器处理程序中实现(在 github 上的视图):


func handle(w http.ResponseWriter, r *http.Request) {  // Log the request protocol  log.Printf("Got connection: %s", r.Proto)  // Handle 2nd request, must be before push to prevent recursive calls.  // Don't worry - Go protect us from recursive push by panicking.  if r.URL.Path == "/2nd" {     log.Println("Handling 2nd")    w.Write([]byte("Hello Again!"))    return  }
// Handle 1st request log.Println("Handling 1st") // Server push must be before response body is being written. // In order to check if the connection supports push, we should use // a type-assertion on the response writer. // If the connection does not support server push, or that the push // fails we just ignore it - server pushes are only here to improve // the performance for HTTP/2 clients. pusher, ok := w.(http.Pusher)
if !ok { log.Println("Can't push to client") } else { err := pusher.Push("/2nd", nil) if err != nil { log.Printf("Failed push: %v", err) } } // Send response body w.Write([]byte("Hello"))}
复制代码

使用服务器推送

让我们重新运行服务器,并测试客户机。


对于 HTTP / 1.1 客户端:


$ go run ./h2-client.go -version 1Got response 200: HTTP/1.1 Hello
复制代码


服务器日志将显示:


Got connection: HTTP/1.1Handling 1stCan't push to client
复制代码


HTTP/1.1 客户端传输连接产生一个 http.ResponseWriter 没有实现 http.Pusher,这是有道理的。在我们的服务器代码中,我们可以选择在这种客户机的情况下该做什么。


对于 HTTP/2 客户:


go run ./h2-client.go -version 2Got response 200: HTTP/2.0 Hello
复制代码


服务器日志将显示:


Got connection: HTTP/2.0Handling 1stFailed push: feature not supported
复制代码


这很奇怪。我们的 HTTP/2 传输的客户端只得到了第一个“Hello”响应。日志表明连接实现了 http.Pusher 接口——但是一旦我们实际调用 Push() 函数——它就失败了。


排查发现,HTTP/2 客户端传输设置了一个 HTTP/2 设置标志,表明推送是禁用的。


因此,目前没有选择使用 Go 客户机来使用服务器推送。


作为一个附带说明,google chrome 作为一个客户端可以处理服务器推送。




服务器日志将显示我们所期望的,处理程序被调用两次,路径 / 和 /2nd,即使客户实际上只对路径 /:


Got connection: HTTP/2.0Handling 1stGot connection: HTTP/2.0Handling 2nd
复制代码

全双工通信

Go HTTP/2 演示页面有一个 echo 示例,它演示了服务器和客户机之间的全双工通信。


让我们先用 CURL 来测试一下:


$ curl -i -XPUT --http2 https://http2.golang.org/ECHO -d helloHTTP/2 200 content-type: text/plain; charset=utf-8date: Tue, 24 Jul 2018 12:20:56 GMT
HELLO
复制代码


我们把 curl 配置为使用 HTTP/2,并将一个 PUT/ECHO 发送给“hello”作为主体。服务器以“HELLO”作为主体返回一个 HTTP/2 200 响应。但我们在这里没有做任何复杂的事情,它看起来像是一个老式的 HTTP/1.1 半双工通信,有不同的头部。让我们深入研究这个问题,并研究如何使用 HTTP/2 全双工功能。

服务器实现

下面是 HTTP echo 处理程序的简化版本(不使用响应)。它使用 http.Flusher 接口,HTTP/2 添加到 http.ResponseWriter。


type flushWriter struct {  w io.Writer}
func (fw flushWriter) Write(p []byte) (n int, err error) { n, err = fw.w.Write(p) // Flush - send the buffered written data to the client if f, ok := fw.w.(http.Flusher); ok { f.Flush() } return}
func echoCapitalHandler(w http.ResponseWriter, r *http.Request) { // First flash response headers if f, ok := w.(http.Flusher); ok { f.Flush() } // Copy from the request body to the response writer and flush // (send to client) io.Copy(flushWriter{w: w}, r.Body)}
复制代码


服务器将从请求正文读取器复制到写入 ResponseWriter 和 Flush() 的“冲洗写入器”。同样,我们看到了笨拙的类型断言样式实现,冲洗操作将缓冲的数据发送给客户机。


请注意,这是全双工,服务器读取一行,并在一个 HTTP 处理程序调用中重复写入一行。

GO 客户端实现

我试图弄清楚一个启用了 HTTP/2 的 go 客户端如何使用这个端点,并发现了这个 Github 问题。提出了类似于下面的代码。


const url = "https://http2.golang.org/ECHO"
func main() { // Create a pipe - an object that implements `io.Reader` and `io.Writer`. // Whatever is written to the writer part will be read by the reader part.
pr, pw := io.Pipe() // Create an `http.Request` and set its body as the reader part of the // pipe - after sending the request, whatever will be written to the pipe, // will be sent as the request body. // This makes the request content dynamic, so we don't need to define it // before sending the request. req, err := http.NewRequest(http.MethodPut, url, ioutil.NopCloser(pr))
if err != nil { log.Fatal(err) } // Send the request resp, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } log.Printf("Got: %d", resp.StatusCode) // Run a loop which writes every second to the writer part of the pipe // the current time. go func() { for { time.Sleep(1 * time.Second) fmt.Fprintf(pw, "It is now %v\n", time.Now()) } }()
// Copy the server's response to stdout. _, err = io.Copy(os.Stdout, res.Body)
log.Fatal(err)}
复制代码

总结

Go 支持与服务器推送和全双工通信的 HTTP/2 连接,这也支持 HTTP/1.1 与标准库的标准 TLS 服务器的连接——这太不可思议了。对于标准的库 HTTP 客户端,它不支持服务器推送,但是支持标准库的标准 HTTP 的全双工通信。以上就是本篇的内容,大家有什么疑问可以在文章下面留言沟通。


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


原文链接:


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


2019-11-20 16:457258

评论

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

软件架构中的模块与组件

Simon

架构实战营

如何快速掌握 Kubernetes 网络

倪朋飞

学习方法 Kubernetes 云原生

推动产业数字化 提升服务实体经济质效

CECBC

科技

如何激励员工?—— 马斯洛需求理论

石云升

激励 28天写作 职场经验 管理经验 3月日更

Centos7下Docker安装&配置&镜像加速

happlyfox

学习 ,docker 3月日更

局域网服务器访问外网方案

程序员与厨子

Linux 网络 路由表

零信任提升组织的数字安全性

龙归科技

网络 数字时代 零信任

聊聊集群、分布式和微服务之间的异同点

架构精进之路

分布式 微服务 集群 3月日更

算法:求两个单向链表的最早公共交点

程序员架构进阶

算法 链表 28天写作 3月日更 算法解析

《MySQL》系列 - select 查询语句到底是怎么执行的?

一个优秀的废人

MySQL 数据库 原理 sql查询

跨越数据的“叹息墙”:华为下一代数据湖与HPDA时代

脑极体

Java + opencv实现视频人脸检测

张音乐

OpenCV 人脸识别 视频

从新手到专家:如何设计一套亿级消息量的分布式IM系统

JackJiang

架构设计 即时通讯 IM

记上周双休日的加班

sadhu

加班

满满干货|支付宝美女面试官的贴心锦囊

Lily

FFmpeg应用篇

Changing Lin

3月日更

工作三年,小胖不知道 MySQL 日志是干嘛的。真的菜

一个优秀的废人

MySQL mysql事务 MySQL日志

Java + opencv 实现图片人脸检测

张音乐

Java AI OpenCV ffmpeg 人脸识别

【Axure9百例】47.CSDN的列表样式

zhuchuanming

原型设计 Axure 交互原型

双非怎么了

我是程序员小贱

3月日更

普元CTO焦烈焱:成长之路务必重视工程能力

EAWorld

程序员

“数字足迹”怕暴露,数字人民币如何守护你我隐私安全?

CECBC

数字货币

关于Vue权限路由思考

程序员海军

Vue 大前端 vue-router 权限认证 按钮权限

工作四年,分享50个让你代码更好的小建议

比伯

Java 程序员 架构 程序人生 计算机

高性能公链能为 DeFi 带来什么?

CECBC

区块链

go + ffmpeg + goav 实现拉流解码器

张音乐

音视频 ffmpeg Go 语言 goav

Redis - 主从模式

insight

redis 3月日更

微服务时代组件化和服务化的抉择

vivo互联网技术

微服务 组件化 服务化 服务调用

Java反射简析

Langer

Java java反射

记一次生产环境大面积404问题!

冰河

nginx 网关

OKR实践中的痛点(4):再谈老板的KR我的O

大叔杨

OKR 敏捷 绩效 敏捷绩效

  • 需要帮助,请添加网站小助手,进入 InfoQ 技术交流群
Go语言HTTP/2探险之旅_文化 & 方法_360云计算_InfoQ精选文章