Golang gRPC笔记04 同时提供 gRPC 服务和 HTTP 接口

一、 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 接口?

  1. 检测请求协议是否为 HTTP/2
  2. 判断 Content-Type 是否为 application/grpc(gRPC 的默认标识位)
  3. 根据协议的不同转发到不同的服务处理
    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文件:

  1. 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";
...
...
...
  1. 重新编写 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

  1. 编译:

在编写 .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/2
  • ClientAuth: 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"}

测试成功

你可能感兴趣的:(Golang gRPC笔记04 同时提供 gRPC 服务和 HTTP 接口)