一、 gRPC 和 HTTP
我们通常把 RPC 用作内部通信,而使用 Restful Api 进行外部通信。在某些时候,我们需要同时提供 RPC 服务和 HTTP 接口,
这种情况下为了避免写两套应用,可以使用 grpc-gateway 把gRPC转成HTTP。服务接收到HTTP请求后,grpc-gateway 把它转成gRPC进行处理,然后以JSON形式返回数据。
为什么可以同时提供 HTTP 接口?
关键一点,gRPC 的协议是基于 HTTP/2 的,因此应用程序能够在单个 TCP 端口上提供 HTTP/1.1 和 gRPC 接口服务(两种不同的流量)
怎么同时提供 HTTP 接口?
- 检测请求协议是否为 HTTP/2
- 判断 Content-Type 是否为 application/grpc(gRPC 的默认标识位)
- 根据协议的不同转发到不同的服务处理
if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") { server.ServeHTTP(w, r) } else { mux.ServeHTTP(w, r) }
安装 grpc-gateway
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
二、 完成项目编码
项目目录:
grpc_demo/
|—— demo04/
|—— client/
|—— client.go // 客户端
|—— conf/ // 证书文件,与上一节相同
|—— pkg/
|—— util/
|—— tls_support.go
|—— proto/
|—— google/
|—— api/
|—— annotations.proto
|—— annotations.pb.go
|—— http.proto
|—— http.pb.go
|—— prod/
|—— prod.proto
|—— prod.pb.go
|—— prod.pb.gw.go
|—— server/
|—— server.go // 服务端
.proto
文件:
- google.api
proto目录中有google/api目录,它用到了google官方提供的两个api描述文件,直接复制自 $GOPATH/grpc-gateway/third_party/googleapis/google/api,以便于import引用
这些文件主要是针对grpc-gateway的http转换提供支持,定义了Protocol Buffer所扩展的HTTP Option
同时,为了直接将编译得到的.go
文件生成在 proto/google/api 文件下,修改 option go_package = "google/api;annotations"
annotations.proto文件:
// Copyright (c) 2015, Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package google.api;
import "google/api/http.proto";
import "google/protobuf/descriptor.proto";
//option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
option go_package = "google/api;annotations";
option java_multiple_files = true;
option java_outer_classname = "AnnotationsProto";
option java_package = "com.google.api";
option objc_class_prefix = "GAPI";
extend google.protobuf.MethodOptions {
// See `HttpRule`.
HttpRule http = 72295728;
}
http.proto文件:
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package google.api;
option cc_enable_arenas = true;
//option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
option go_package = "google/api;annotations";
option java_multiple_files = true;
option java_outer_classname = "HttpProto";
option java_package = "com.google.api";
option objc_class_prefix = "GAPI";
...
...
...
- 重新编写
prod.proto
文件:
syntax = "proto3";
package prodpb;
option go_package="./prod;prodpb";
import "google/api/annotations.proto";
message ProdRequest {
int64 prod_id = 1;
}
message ProdResponse {
int64 prod_stock = 1;
}
service ProdService {
// 定义一个方法
rpc GetProdStock (ProdRequest) returns (ProdResponse) {
// http option
option (google.api.http) = {
get: "/v1/prod/{prod_id}"
};
}
}
在文件中,引用了google/api/annotations.proto,达到支持HTTP Option的效果
在 service ProdService 服务内部定义了一个HTTP Option的GET方法,HTTP响应路径为 /v1/prod/{prod_id},其中的 {prod_id} 匹配 message ProdRequest 中的 prod_id
- 编译:
在编写 .proto
文件中,import 路径和 go_package 路径,我全部以 proto 目录为相对路径,因此,编译 .proto
文件需切换到 proto 目录进行
cd grpc_demo/demo04/proto
# 编译google.api
protoc -I . --go_out=plugins=grpc,Mgoogle/protobuf/descriptor.proto=github.com/golang/protobuf/protoc-gen-go/descriptor:. google/api/*.proto
# 编译 prod.proto为 prod.pb.go
protoc -I . --go_out=plugins=grpc,Mgoogle/api/annotations.proto=grpc_demo/demo04/proto/google/api:. ./prod/prod.proto
# 编译 prod.proto 为 prod.pb.gw.go,以完成对grpc-gateway的功能支持
protoc --grpc-gateway_out=logtostderr=true:. ./prod/prod.proto
util: tls_support.go
在 tls_support.go文件中,新增 GetCATLSConfig 方法
// 封装基于 CA 的 TLS 认证服务端配置项
func GetCATLSConfig(serverCertFile, serverKeyFile, caFile string) (*tls.Config, error) {
// 从证书相关文件中读取和解析信息,得到证书公钥、密钥对
cert, err := tls.LoadX509KeyPair(serverCertFile, serverKeyFile)
if err != nil {
return nil, fmt.Errorf("tls.LoadX509KeyPair err: %v", err)
}
// 创建一个新的、空的 CertPool,并尝试解析 PEM 编码的证书,解析成功会将其加到 CertPool 中
certPool := x509.NewCertPool()
ca, err := ioutil.ReadFile(caFile)
if err != nil {
return nil, fmt.Errorf("ioutil.ReadFile err: %v", err)
}
if ok := certPool.AppendCertsFromPEM(ca); !ok {
return nil, fmt.Errorf("certPool.AppendCertsFromPEM err")
}
return &tls.Config{
// 设置证书链,允许包含一个或多个
Certificates: []tls.Certificate{cert},
// 要求客户端提供证书,但是如果客户端没有提供证书,服务端还是会继续处理请求
ClientAuth: tls.RequestClientCert,
// 设置根证书的集合,校验方式使用 ClientAuth 中设定的模式
ClientCAs: certPool,
// NextProtoTLS是谈判期间的NPN/ALPN协议,用于HTTP/2的TLS设置,如需支持HTTP/2需配置此项
NextProtos: []string{http2.NextProtoTLS},
}, nil
}
GetCATLSConfig函数用于获取TLS配置,处理HTTP服务的TLS认证相关问题
NextProtos: []string{http2.NextProtoTLS}
: 该配置用于支持 HTTP/2ClientAuth: tls.RequestClientCert
: 不检测 HTTP 请求的客户端证书,gRPC请求正常检测,此处检测证书会提示无效证书,暂不知如何处理,因此先关闭 HTTP 请求证书检测
server端:server.go
package main
import (
"crypto/tls"
"grpc_demo/demo04/pkg/util"
prodpb "grpc_demo/demo04/proto/prod"
"log"
"net"
"net/http"
"strings"
"golang.org/x/net/context"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"google.golang.org/grpc"
)
const (
endpoint = "127.0.0.1:8899"
serverCertFile = "demo03/conf/keys/server/server.pem"
serverKeyFile = "demo03/conf/keys/server/server.key"
clientCertFile = "demo03/conf/keys/client/client.pem"
clientKeyFile = "demo03/conf/keys/client/client.key"
caFile = "demo03/conf/keys/ca.pem"
certServerName = "localhost"
)
func main() {
// 开启 HTTP 服务
tlsConfig, err := util.GetCATLSConfig(serverCertFile, serverKeyFile, caFile)
if err != nil {
log.Fatalf("加载服务端 TLS 凭证失败,err=%v", err)
}
srv := &http.Server{
Addr: endpoint,
Handler: grpcHandlerFunc(newGRPCServer(), newHTTPHandler()),
TLSConfig: tlsConfig,
}
conn, err := net.Listen("tcp", srv.Addr)
if err != nil {
log.Fatal("监听连接失败:", err)
}
log.Printf("grpc and https on port: %s", srv.Addr)
log.Fatal(srv.Serve(tls.NewListener(conn, srv.TLSConfig)))
}
// grpcHandlerFunc 检查请求协议并返回对应的 http handler
func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
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)
}
})
}
// newGRPCServer 实例化标准grpc server
func newGRPCServer() *grpc.Server {
serverOpt, err := util.GetCATLSServerOption(serverCertFile, serverKeyFile, caFile)
if err != nil {
log.Fatalf("加载服务端 TLS 凭证失败,err=%v", err)
}
rpcServer := grpc.NewServer(serverOpt)
prodpb.RegisterProdServiceServer(rpcServer, CreateProdService())
return rpcServer
}
// newHTTPHandler 初始化 http-grpc gateway
func newHTTPHandler() http.Handler {
dialOpt, err := util.GetCATLSDialOption(clientCertFile, clientKeyFile, caFile, certServerName)
if err != nil {
log.Fatalf("加载客户端 TLS 凭证失败,err=%v", err)
}
ctx := context.Background()
// 新建gwmux,它是grpc-gateway的请求复用器。它将http请求与模式匹配,并调用相应的处理程序
gwMux := runtime.NewServeMux()
// 添加 dialOption
dOpts := []grpc.DialOption{dialOpt}
// 将服务的http处理程序注册到gwmux。处理程序通过endpoint转发请求到grpc端点
err = prodpb.RegisterProdServiceHandlerFromEndpoint(ctx, gwMux, endpoint, dOpts)
if err != nil {
log.Fatalf("Failed to register gw server: %v", err)
}
return gwMux
}
newGRPCServer:
该方法 实例化标准grpc server,用来处理 gRPC 请求
newHTTPHandler:
runtime.NewServeMux 创建一个新的 ServerMux,它的内部映射是空的;ServeMux是grpc-gateway的一个请求多路复用器。它将http请求与模式匹配,并调用相应的处理程序
函数 RegisterProdServiceHandlerFromEndpoint 注册ProdService服务的HTTP Handle到 serverMux,
serverMux 实现了 ServeHTTP 方法,因此可以作为 http.Handler 传给 grpcHandlerFunc 函数
grpcHandlerFunc:
grpcHandlerFunc函数是用于判断请求是来源于Rpc客户端还是Restful Api的请求,根据不同的请求注册不同的ServeHTTP服务;r.ProtoMajor == 2也代表着请求必须基于HTTP/2
client端:client.go
package main
import (
"context"
"grpc_demo/demo04/pkg/util"
prodpb "grpc_demo/demo04/proto/prod"
"log"
"google.golang.org/grpc"
)
const Address = "127.0.0.1:8899"
func main() {
dialOpt, err := util.GetCATLSDialOption("demo03/conf/keys/client/client.pem", "demo03/conf/keys/client/client.key", "demo03/conf/keys/ca.pem", "localhost")
if err != nil {
log.Fatalf("加载客户端 TLS 凭证失败,err=%v", err)
}
// 连接 rpc 服务器
conn, err := grpc.Dial(Address, dialOpt)
if err != nil {
panic("grpc.Dial err: " + err.Error())
}
defer conn.Close()
// 初始化客户端
client := prodpb.NewProdServiceClient(conn)
resp, err := client.GetProdStock(context.Background(), &prodpb.ProdRequest{ProdId: 4444})
if err != nil {
log.Print("调用失败,err=", err)
return
}
log.Printf("%+v \n", resp)
}
验证:
启动 Server:
cd demo04/server
go run server.go
grpc and https on port: 127.0.0.1:8899
启动 Client:
cd demo04/client
go run client.go
prod_stock:4008
测试Restful Api
curl -k https://localhost:8899/v1/prod/123
{"prod_stock":"4008"}
测试成功