grpc基于HTTP/2

grpc的client和server通信是基于HTTP/2,client发出的消息是HTTP/2协议格式,server按照HTTP/2协议解析收到的消息。grpc把这个过程包装了。下面看一个最简单的grpc例子。

./server/server.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
    "grpc-example/service"
    "net"

    "google.golang.org/grpc"
)

func main() {
    rpcServer := grpc.NewServer()
    service.RegisterOrderServiceServer(rpcServer, new(service.OrderService))
    lis, _ := net.Listen("tcp", ":9005")
    rpcServer.Serve(lis)
}

可以看到,server监听tcp的9005端口,client建立与server的tcp连接。我们根本不需要处理HTTP/2相关的问题,grpc自己解决了。

grpc提供http2接口

这一节暂时还不会讲到grpc-gateway,只是让grpc使用http连接代替直接使用TCP。

我们在第一节看到rpcServer.Serve(lis),这是grpc提供的方法:

1
2
3
func (s *Server) Serve(lis net.Listener) error{
  ...
}

实际上还提供了另一个方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// ServeHTTP implements the Go standard library's http.Handler
// interface by responding to the gRPC request r, by looking up
// the requested gRPC method in the gRPC server s.
//
// ServeHTTP实现了go标准库里面的http.Handler接口,通过在gRPC服务中查找请求的gRPC方法,来响应gRPC请求
//
// The provided HTTP request must have arrived on an HTTP/2
// connection. When using the Go standard library's server,
// practically this means that the Request must also have arrived
// over TLS.
//
// HTTP请求必须是走HTTP/2连接。如果使用的是Go标准库的http服务,意味着必须使用TLS加密方式建立http连接。

// To share one port (such as 443 for https) between gRPC and an
// existing http.Handler, use a root http.Handler such as:
//
// 为了让gRPC的http服务和已有的http服务共用一个端口,可以使用一个前置的http服务来进行转发,如下:
//
//   if r.ProtoMajor == 2 && strings.HasPrefix(
//      r.Header.Get("Content-Type"), "application/grpc") {
//      grpcServer.ServeHTTP(w, r)
//   } else {
//      yourMux.ServeHTTP(w, r)
//   }
//
// Note that ServeHTTP uses Go's HTTP/2 server implementation which is totally
// separate from grpc-go's HTTP/2 server. Performance and features may vary
// between the two paths. ServeHTTP does not support some gRPC features
// available through grpc-go's HTTP/2 server, and it is currently EXPERIMENTAL
// and subject to change.

// 注意,ServeHTTP使用Go的HTTP/2服务,这和gRPC基于HTTP/2所指的HTTP/2完全不是一个东西。他们两的行为、特征可能差异非常大。
// ServeHttp并不支持gRPC的HTTP/2服务所支持的一些特性,并且ServeHTTP是实验性质的,可能会有变化。
func (s *Server) ServeHTTP(w http.ResponseWriter, r*http.Request) {
  ...
}

对gRPC和HTTP/1.1的流量区分:

  • 对ProtoMajor进行判断,该字段代表客户端请求的版本号,客户端始终使用HTTP/1.1或HTTP/2协议。
  • 对Content一Type进行判断,通过grpe的标志位application/grpc来确定流量的类型。

这里特地翻译了一下源码的注释。有三个重点:

  • ServeHTTP实现了Go标准库里面提供Http服务的接口,所以ServeHTTP就可以对外提供Http服务了,在ServeHTTP里面,把收到的请求转发到对应的gRPC方法,并返回gRPC方法的返回。
    • 可以理解为ServeHTTP在gRPC外面包了一层HTTP/2协议编解码器。grpc与原生http2能够实现协议通信,但在通信包的解释上有更进一步的特殊处理,规定了一些固定的特定的头信息的值,从而会导致并不能直接与原生http2协议通信。
    • 原生http2协议server端能够完全解析grpc客户端发送的请求,获取任意请求信息,并返回.
    • 原生http2客户端与grpc服务端只能够实现通信,不能够实现完整正确的响应解析。
  • 因为Go标准库的HTTP/2必须使用TLS,所以使用ServeHTTP必须使用TLS,即必须使用证书和https访问。但这不是gRPC的要求,第一节中我们在client.go看到了grpc.WithInsecure()就是不使用加密证书的意思。这个问题在18年Go的Http标准库支持h2c之后已经解决。
  • ServeHTTP可以达到多个服务共用一个端口的目的。

我们修改一下服务端代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
./server/server.go

import (
    "grpc-example/service"
    "log"
    "net/http"
    "google.golang.org/grpc"
)
func main() {
    rpcServer := grpc.NewServer()
    service.RegisterOrderServiceServer(rpcServer, new(service.OrderService))
    http.ListenAndServe(":9005", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Printf("收到请求%v", r)
        rpcServer.ServeHTTP(w, r)
    }))
}

使用Go的http库创建一个请求:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package asd

import (
    "crypto/tls"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "strings"
    "testing"

    "golang.org/x/net/http2"
)

func TestAsd(t *testing.T) {

    tr := &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    }
    http2.ConfigureTransport(tr)
    client := &http.Client{Transport: tr}

    req, err := http.NewRequest("POST", "https://localhost:9005/service.OrderService/GetOrder", strings.NewReader("OrderId=123"))
    if err != nil {
        t.Fatal(err)
    }
    req.Header.Add("Content-type", "application/grpc")
    resp, err := client.Do(req)
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        t.Fatal(err)
    }
    fmt.Println(string(body))
}

访问正常,但是还是不能收到正确的返回。gRPC提供了HTTP访问方式(虽然不能直接用http访问,但是gRPC client走的是http请求),就可以和其他http服务共用一个端口。就是上面文档注释提到的根据协议版本进行转发。

同端口支持GRPC与HTTP(HTTPS)

在pkg下新建util目录,新建grpc.go文件,写入内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package util

import (
    "net/http"
    "strings"

    "google.golang.org/grpc"
)

func GrpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
    if otherHandler == nil {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            grpcServer.ServeHTTP(w, r)
        })
    }
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
            grpcServer.ServeHTTP(w, r)
        } else {
            otherHandler.ServeHTTP(w, r)
        }
    })
}

GrpcHandlerFunc函数是用于判断请求是来源于Rpc客户端还是Restful Api的请求,根据不同的请求注册不同的ServeHTTP服务;r.ProtoMajor == 2也代表着请求必须基于HTTP/2

2、在pkg下的util目录下,新建tls.go文件,写入内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package util

import (
    "crypto/tls"
    "io/ioutil"
    "log"

    "golang.org/x/net/http2"
)

func GetTLSConfig(certPemPath, certKeyPath string) *tls.Config {
    var certKeyPair *tls.Certificate
    cert, _ := ioutil.ReadFile(certPemPath)
    key, _ := ioutil.ReadFile(certKeyPath)

    pair, err := tls.X509KeyPair(cert, key)
    if err != nil {
        log.Println("TLS KeyPair err: %v\n", err)
    }

    certKeyPair = &pair

    return &tls.Config{
        Certificates: []tls.Certificate{*certKeyPair},
        NextProtos:   []string{http2.NextProtoTLS},
    }
}

GetTLSConfig函数是用于获取TLS配置,在内部,我们读取了server.key和server.pem这类证书凭证文件

  • tls.X509KeyPair:从一对PEM编码的数据中解析公钥/私钥对。成功则返回公钥/私钥对
  • http2.NextProtoTLS:NextProtoTLS是谈判期间的NPN/ALPN协议,用于HTTP/2的TLS设置
  • tls.Certificate:返回一个或多个证书,实质我们解析PEM调用的X509KeyPair的函数声明就是func X509KeyPair(certPEMBlock, keyPEMBlock []byte) (Certificate, error),返回值就是Certificate

总的来说该函数是用于处理从证书凭证文件(PEM),最终获取tls.Config作为HTTP2的使用参数

3、修改server目录下的server.go文件,该文件是我们服务里的核心文件,写入内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package server

import (
    "crypto/tls"
    "net"
    "net/http"
    "log"

    "golang.org/x/net/context"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "github.com/grpc-ecosystem/grpc-gateway/runtime"

    pb "grpc-hello-world/proto"
    "grpc-hello-world/pkg/util"
)

var (
    ServerPort string
    CertName string
    CertPemPath string
    CertKeyPath string
    EndPoint string
)

func Serve() (err error){
    EndPoint = ":" + ServerPort
    conn, err := net.Listen("tcp", EndPoint)
    if err != nil {
        log.Printf("TCP Listen err:%v\n", err)
    }

    tlsConfig := util.GetTLSConfig(CertPemPath, CertKeyPath)
    srv := createInternalServer(conn, tlsConfig)

    log.Printf("gRPC and https listen on: %s\n", ServerPort)

    if err = srv.Serve(tls.NewListener(conn, tlsConfig)); err != nil {
        log.Printf("ListenAndServe: %v\n", err)
    }

    return err
}

func createInternalServer(conn net.Listener, tlsConfig *tls.Config) (*http.Server) {
    var opts []grpc.ServerOption

    // grpc server
    creds, err := credentials.NewServerTLSFromFile(CertPemPath, CertKeyPath)
    if err != nil {
        log.Printf("Failed to create server TLS credentials %v", err)
    }

    opts = append(opts, grpc.Creds(creds))
    grpcServer := grpc.NewServer(opts...)

    // register grpc pb
    pb.RegisterHelloWorldServer(grpcServer, NewHelloService())

    // gw server
    ctx := context.Background()
    dcreds, err := credentials.NewClientTLSFromFile(CertPemPath, CertName)
    if err != nil {
        log.Printf("Failed to create client TLS credentials %v", err)
    }
    dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)}
    gwmux := runtime.NewServeMux()

    // register grpc-gateway pb
    if err := pb.RegisterHelloWorldHandlerFromEndpoint(ctx, gwmux, EndPoint, dopts); err != nil {
        log.Printf("Failed to register gw server: %v\n", err)
    }

    // http服务
    mux := http.NewServeMux()
    mux.Handle("/", gwmux)

    return &http.Server{
        Addr:      EndPoint,
        Handler:   util.GrpcHandlerFunc(grpcServer, mux),
        TLSConfig: tlsConfig,
    }
}

server流程剖析

我们将这一大块代码,分成以下几个部分来理解

一、启动监听

net.Listen("tcp", EndPoint)用于监听本地的网络地址通知,它的函数原型func Listen(network, address string) (Listener, error)

参数:network必须传入tcp、tcp4、tcp6、unix、unixpacket,若address为空或为0则会自动选择一个端口号 返回值:通过查看源码我们可以得知其返回值为Listener,结构体原型:

1
2
3
4
5
type Listener interface {
    Accept() (Conn, error)
    Close() error
    Addr() Addr
}

通过分析得知,最后net.Listen会返回一个监听器的结构体,返回给接下来的动作,让其执行下一步的操作,它可以执行三类操作

  • Accept:接受等待并将下一个连接返回给Listener
  • Close:关闭Listener
  • Addr:返回Listener的网络地址

二、获取TLS

通过util.GetTLSConfig解析得到tls.Config,传达给http.Server服务的TLSConfig配置项使用

三、创建内部服务

createInternalServer函数,是整个服务端的核心流转部分

程序采用的是HTT2、HTTPS也就是需要支持TLS,因此在启动grpc.NewServer前,我们要将认证的中间件注册进去

而前面所获取的tlsConfig仅能给HTTP使用,因此第一步我们要创建grpc的TLS认证凭证

1、创建grpc的TLS认证凭证

新增引用 google.golang.org/grpc/credentials 的第三方包,它实现了grpc库支持的各种凭证,该凭证封装了客户机需要的所有状态,以便与服务器进行身份验证并进行各种断言,例如关于客户机的身份,角色或是否授权进行特定的呼叫

我们调用NewServerTLSFromFile来达到我们的目的,它能够从输入证书文件和服务器的密钥文件构造TLS证书凭证

1
2
3
4
5
6
7
8
9
func NewServerTLSFromFile(certFile, keyFile string) (TransportCredentials, error) {
    //LoadX509KeyPair读取并解析来自一对文件的公钥/私钥对
    cert, err := tls.LoadX509KeyPair(certFile, keyFile)
    if err != nil {
        return nil, err
    }
    //NewTLS使用tls.Config来构建基于TLS的TransportCredentials
    return NewTLS(&tls.Config{Certificates: []tls.Certificate{cert}}), nil
}
2、设置grpc ServerOption

以grpc.Creds(creds)为例,其原型为func Creds(c credentials.TransportCredentials) ServerOption,该函数返回ServerOption,它为服务器连接设置凭据

3、创建grpc服务端

函数原型:

1
func NewServer(opt ...ServerOption) *Server

我们在此处创建了一个没有注册服务的grpc服务端,还没有开始接受请求

1
grpcServer := grpc.NewServer(opts...)

4、注册grpc服务

1
pb.RegisterHelloWorldServer(grpcServer, NewHelloService())

5、创建grpc-gateway关联组件

1
2
3
4
5
6
ctx := context.Background()
dcreds, err := credentials.NewClientTLSFromFile(CertPemPath, CertName)
if err != nil {
    log.Println("Failed to create client TLS credentials %v", err)
}
dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)}
  • context.Background:返回一个非空的空上下文。它没有被注销,没有值,没有过期时间。它通常由主函数、初始化和测试使用,并作为传入请求的顶级上下文
  • credentials.NewClientTLSFromFile:从客户机的输入证书文件构造TLS凭证
  • grpc.WithTransportCredentials:配置一个连接级别的安全凭据(例:TLS、SSL),返回值为type DialOption
  • grpc.DialOption:DialOption选项配置我们如何设置连接(其内部具体由多个的DialOption组成,决定其设置连接的内容)

6、创建HTTP NewServeMux及注册grpc-gateway逻辑

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
gwmux := runtime.NewServeMux()

// register grpc-gateway pb
if err := pb.RegisterHelloWorldHandlerFromEndpoint(ctx, gwmux, EndPoint, dopts); err != nil {
    log.Println("Failed to register gw server: %v\n", err)
}

// http服务
mux := http.NewServeMux()
mux.Handle("/", gwmux)
  • runtime.NewServeMux:返回一个新的ServeMux,它的内部映射是空的;ServeMux是grpc-gateway的一个请求多路复用器。它将http请求与模式匹配,并调用相应的处理程序
  • RegisterHelloWorldHandlerFromEndpoint:如函数名,注册HelloWorld服务的HTTP Handle到grpc端点
  • http.NewServeMux:分配并返回一个新的ServeMux
  • mux.Handle:为给定模式注册处理程序

(带着疑问去看程序)为什么gwmux可以放入mux.Handle中?

首先我们看看它们的原型是怎么样的

(1)http.NewServeMux()

1
2
3
4
5
6
func NewServeMux() *ServeMux {
        return new(ServeMux)
}
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

(2)runtime.NewServeMux?

1
2
3
4
5
6
7
8
9
func NewServeMux(opts ...ServeMuxOption) *ServeMux {
    serveMux := &ServeMux{
        handlers:               make(map[string][]handler),
        forwardResponseOptions: make([]func(context.Context, http.ResponseWriter, proto.Message) error, 0),
        marshalers:             makeMarshalerMIMERegistry(),
    }
    ...
    return serveMux
}

(3)http.NewServeMux()的Handle方法

1
func (mux *ServeMux) Handle(pattern string, handler Handler)

通过分析可得知,两者NewServeMux都是最终返回serveMux,Handler中导出的方法仅有ServeHTTP,功能是用于响应HTTP请求

我们回到Handle interface中,可以得出结论就是任何结构体,只要实现了ServeHTTP方法,这个结构就可以称为Handle,ServeMux会使用该Handler调用ServeHTTP方法处理请求,这也就是自定义Handler

而我们这里正是将grpc-gateway中注册好的HTTP Handler无缝的植入到net/http的Handle方法中

补充:在go中任何结构体只要实现了与接口相同的方法,就等同于实现了接口

7、注册具体服务

1
2
3
if err := pb.RegisterHelloWorldHandlerFromEndpoint(ctx, gwmux, EndPoint, dopts); err != nil {
    log.Println("Failed to register gw server: %v\n", err)
}

注意:EndPoint字段不能为":8080",必须为实际的ip+port,比如"127.0.0.1:8080".

四、创建tls.NewListener

1
2
3
4
5
6
func NewListener(inner net.Listener, config *Config) net.Listener {
    l := new(listener)
    l.Listener = inner
    l.config = config
    return l
}

NewListener将会创建一个Listener,它接受两个参数,第一个是来自内部Listener的监听器,第二个参数是tls.Config(必须包含至少一个证书)

五、服务开始接受请求

在最后我们调用srv.Serve(tls.NewListener(conn, tlsConfig)),可以得知它是http.Server的方法,并且需要一个Listener作为参数,那么Serve内部做了些什么事呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func (srv *Server) Serve(l net.Listener) error {
    defer l.Close()
    ...

    baseCtx := context.Background() // base is always background, per Issue 16220
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
        rw, e := l.Accept()
        ...
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew) // before Serve can return
        go c.serve(ctx)
    }
}

粗略的看,它创建了一个context.Background()上下文对象,并调用Listener的Accept方法开始接受外部请求,在获取到连接数据后使用newConn创建连接对象,在最后使用goroutine的方式处理连接请求,达到其目的

补充:对于HTTP/2支持,在调用Serve之前,应将srv.TLSConfig初始化为提供的Listener的TLS配置。如果srv.TLSConfig非零,并且在Config.NextProtos中不包含字符串h2,则不启用HTTP/2支持

支持H2C

经过社区的不断讨论,最后在 2018 年 6 月,代表 “h2c” 标志的 golang.org/x/net/http2/h2c 标准库正式合并进来,自此我们就可以使用官方标准库(h2c),这个标准库实现了 HTTP/2 的未加密模式,因此我们就可以利用该标准库在同个端口上既提供 HTTP/1.1 又提供 HTTP/2 的功能了。

使用标准库 h2c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import (
    ...

    "golang.org/x/net/http2"
    "golang.org/x/net/http2/h2c"
    "google.golang.org/grpc"

    "github.com/grpc-ecosystem/grpc-gateway/runtime"

    pb "github.com/EDDYCJY/go-grpc-example/proto"
)

...

func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
    return h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
            grpcServer.ServeHTTP(w, r)
        } else {
            otherHandler.ServeHTTP(w, r)
        }
    }), &http2.Server{})
}

func main() {
    server := grpc.NewServer()

    pb.RegisterSearchServiceServer(server, &SearchService{})

    mux := http.NewServeMux()
    gwmux := runtime.NewServeMux()
    dopts := []grpc.DialOption{grpc.WithInsecure()}

    err := pb.RegisterSearchServiceHandlerFromEndpoint(context.Background(), gwmux, "localhost:"+PORT, dopts)
    ...
    mux.Handle("/", gwmux)
    http.ListenAndServe(":"+PORT, grpcHandlerFunc(server, mux))
}

我们可以看到关键之处在于调用了 h2c.NewHandler 方法进行了特殊处理,h2c.NewHandler 会返回一个 http.handler,主要的内部逻辑是拦截了所有 h2c 流量,然后根据不同的请求流量类型将其劫持并重定向到相应的 Hander 中去处理。

验证

HTTP/1.1

1
2
$ curl -X GET 'http://127.0.0.1:9005/search?request=EDDYCJY'
{"response":"EDDYCJY"}

HTTP/2(gRPC)

1
2
3
4
5
6
7
8
9
...
func main() {
    conn, err := grpc.Dial(":"+PORT, grpc.WithInsecure())
    ...
    client := pb.NewSearchServiceClient(conn)
    resp, err := client.Search(context.Background(), &pb.SearchRequest{
        Request: "gRPC",
    })
}

输出结果:

1
2
$ go run main.go
2019/06/21 20:04:09 resp: gRPC h2c Server

参考

grpc同时提供grpc和http接口—h2c和grpc-gateway等的使用