Jump to Navigation

golang/Go中HTTP2.0协议的应用

http2协议: tls or non-tls

http2协议的安全问题,实际上与http1协议一样,同样可选择http/https两种,一种是明文传输,另一种是加密传递。

只不过在google设计http2协议的时候,偏向了加密传输,以至于golang 1.6中默认支持的http2协议包,现在却没有办法实现非加密http2通信了。

在实现中,http2分为两个类型,普通的加密称称为http2/h2,非加密的称为http2/h2c。

在使用中,http2/h2一般都有官方标准的实现,而http2/h2c则支持力度次之。

而由于加密http2/h2由于在TLS握手阶段,可能消耗约~10ms时间,在有些场合是非常大的开销,所以还可能要用到http2/h2c实现方式。

比如在grpc中,服务器之间通信要求更高,就自带了一个http2/h2c的实现。

1、http2/h2类型server实现

看这段示例代码:

srv := &http.Server{Addr: “:5678”,
              Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                       log.Println(r) }
       }

srv.ListenAndServe()

曾经以为,这在go1.6中就能支持http2/h2c协议了,然后错了。实际上这段代码只支持http1协议,因为没有提供TLS支持,不会执行golang实现的http2协议逻辑。

其实把最后一行改一下:

srv.ListenAndServeTLS(“local.cert”, “local.key”)

改完之后,这个服务会是什么服务行为呢?这个实现支持https/1.0,http2/h2两种协议了。

注:使用go1.6自带的http.Server提供http2/h2c服务失败,因为至少在go1.6版本它需要开启tls才行,而这儿作为rpc通信使用时并不想开启tls。(因为tls的握手阶段可能会用掉~10ms的时间)

2、http2/h2c类型server

这种需要用到最新的golang.org/x/net/http2包,也只是最近(2016/06)才接收了无缝支持h2c模式http2/h2c实现的patch。

以下是演示代码:

import “golang.org/x/net/http2"
import “net/http"
import “net"
import “log"

lsner, err := net.Listen(“tcp”, “:5678”)
if err != nil {panic(err)}

http2.VerboseLogs()
srv := &http2.Server{}

for {
    conn, err := lsner.Accept()
    if err != nil {panic(err)}

     go func() {
            opts := &http2.ServerConnOpts{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                        log.Println(r)
                 ))}
            srv.ServeConn(conn, opts)
      }()
}

注:使用时要注意,与net/http包使用方法不同,需要自己listen,accept并创建goroutine处理新连接请求。

这么实现的server,为什么实现了http2/h2c协议呢,因为我们跳过了TLS的握手协议创建加密连接一步,而是直接给http2请求处理函数一个明文的连接。

并且还要知道,这个实现是只支持http2/h2c协议的,没有对fallback协议的任何支持,可能的fallback协议包括http2/h2和http1。

测试一下:nghttp2 -v http://localhost:5678/

3、http2/h2c类型client - GET

这么魔性的http2/h2c服务端实现完了,再来看看http2/h2c的客户端实现有什么魔性吧。

tr := &http2.Transport{AllowHTTP: true,
    DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
        return net.Dial(network, addr)
    }}
cli := http.Client{Transport: tr}
resp, err := cli.Get("http://localhost:5678/")
log.Println(err, resp)

这个还没有什么魔性,除了定制的http2.Transport(注意是http2.)。

  • AllowHTTP: 这个字段指定允许非TLS加密传输
  • DialTLS: 需要覆盖默认的创建连接函数。这个为什么不能写在http2包中直接默认实现了呢?

server端执行结果显示正常使用了http2/h2c协议:

   2016/06/13 16:46:34 / get map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/2.0]]

再来看一段实现:

tr := &http2.Transport{AllowHTTP: true,
    DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
        return net.Dial(network, addr)
    }}
req, err := http.NewRequest("get", "http://localhost:5678/", nil)
resp, err := tr.RoundTrip(req)
log.Println(err, resp)

再来看一段实现+1:

tr := &http2.Transport{AllowHTTP: true,
    DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
        return net.Dial(network, addr)
    }}
req, err := http.NewRequest("get", "http://localhost:5678/", nil)
resp, err := (&http.Client{Transport: tr}).Do(req)
log.Println(err, resp)

RoundTrip函数是啥,直接就调用了网络请求吗?

这三段代码在该测试中的结果相同,what’s the diffecrence?

3、http2/h2c类型client - POST

POST方法比GET方法稍有不同,也单独列举一下,但只涉及到不同的部分,不会像上面一节那么详细。

不同的地方在于,用POST到服务器的数据。

改动上面的创建http.Request的代码,改为“GET”方法,添加有效的body参数:

buf := strings.NewReader(“\x00\x00\x00\x00\x12\n\aellox++\x12\aorldx++”)
// buf := bytes.NewBuffer([]byte{“\x00\x00\x00\x00\x12\n\aellox++\x12\aorldx++"})
req, err := http.NewRequest(“POST”, “http://localhost:5678/“, buf)

这样便实现http2/h2c的POST提交数据功能了。并且这里还可以用二进制的io.Reader。

客户端的http2调试输出信息:

http2: Transport received HEADERS flags=END_HEADERS stream=1 len=14
http2: Transport received DATA stream=1 len=23 data="\x00\x00\x00\x00\x12\n\aellox++\x12\aorldx++"
http2: Transport received HEADERS flags=END_STREAM|END_HEADERS stream=1 len=24

可以看到,http2协议已经把POST的提交数据转换为了http2中的data frame了。

针对grpc-go的http2/h2c协议server端完善

grpc-go虽然使用的是http2/h2c标准协议,但依然在使用上稍有不同。

使用其他协议与grpc-go服务端/客户端直接通信时,可能产生一问题,再此做一些解决说明。

没有正确拷贝响应的头信息时报错:

transport: http2Client.notifyError got notified that the client transport was broken EOF.
rpc error: code = 9 desc = transport: received the unexpected content-type "text/plain; charset=utf-8"

没有正确发送Trailer时报错:

rpc error: code = 13 desc = server closed the stream without sending trailers

完善部分,主要在ServeHttp(w ResponseWriter, r *http.Request)方法中。

首先,加一段拷贝resp.Header的代码段:

for k, vec := range resp.Header {
    if false {
        w.Header().Add(k, strings.Join(vec, ";"))
    }
}

resp的值来自对grpc上游服务的调用结果。

其次:添加Trailer相关调用

w.Header().Set("Trailer", "Grpc-Status, Grpc-Message")
w.Header().Set("Grpc-Status", "1")
w.Header().Set("Grpc-Message", err.Error())

这样的ServeHTTP,能够当作一个grpc-go的server端来使用了。

修正 h2c 模式 schema

需要做一个 fix,在h2c模式时,http2调试包,输出的schema依旧是https。

这个fix在x/net/http2包中的transport.go文件中,但没有提供PR方式啊。

diff --git a/http2/transport.go b/http2/transport.go
index 060471e..e4a1c09 100644
--- a/http2/transport.go
+++ b/http2/transport.go
@@ -1004,7 +1004,11 @@ func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trail
        cc.writeHeader(":method", req.Method)
        if req.Method != "CONNECT" {
                cc.writeHeader(":path", req.URL.RequestURI())
-               cc.writeHeader(":scheme", "https")
+               if cc.t.AllowHTTP {
+                       cc.writeHeader(":scheme", "http")
+               } else {
+                       cc.writeHeader(":scheme", "https")
+               }
        }
        if trailers != "" {
                cc.writeHeader("trailer", trailers)

参考资料:

https://github.com/golang/net/commit/b3e9c8fbe09fe2b5b27bfb7320c1f77b640111be https://godoc.org/golang.org/x/net/http2#ServeConnOpts https://http2.golang.org/ https://http2.github.io/ https://httpwg.github.io/specs/rfc7540.html https://httpwg.github.io/specs/rfc7541.html https://golang.org/pkg/net/http/#example_ResponseWriter_trailers

Category:

添加新评论

Plain text

  • 不允许HTML标记。
  • 自动将网址与电子邮件地址转变为链接。
  • 自动断行和分段。
CAPTCHA
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.
Image CAPTCHA
Enter the characters shown in the image.


Main menu 2

Story | by Dr. Radut