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等的使用