• gRPC之gRPC Gateway


    1、gRPC Gateway

    etcd3 API全面升级为gRPC后,同时要提供REST API服务,维护两个版本的服务显然不太合理,所以

    grpc-gateway 诞生了。通过protobuf的自定义option实现了一个网关,服务端同时开启gRPC和HTTP服务,

    HTTP服务接收客户端请求后转换为grpc请求数据,获取响应后转为json数据返回给客户端。结构如图:

    在这里插入图片描述

    grpc-gateway地址:https://github.com/grpc-ecosystem/grpc-gateway

    1.1 安装grpc-gateway

    $ go get github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway@v1.16.0
    $ go install github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway@v1.16.0
    
    • 1
    • 2

    1.2 proto编写

    这里用到了google官方Api中的两个proto描述文件,直接拷贝不要做修改,里面定义了protocol buffer扩展的

    HTTP option,为grpc的http转换提供支持。

    annotations.proto文件的内容:

    // ./proto/google/api/annotations.proto
    syntax = "proto3";
    
    package google.api;
    
    option go_package = "google/api;google_api";
    
    import "google/api/http.proto";
    import "google/protobuf/descriptor.proto";
    
    option java_multiple_files = true;
    option java_outer_classname = "AnnotationsProto";
    option java_package = "com.google.api";
    
    extend google.protobuf.MethodOptions {
    
        HttpRule http = 72295728;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    http.proto文件的内容:

    // ./proto/google/api/http.proto
    syntax = "proto3";
    
    package google.api;
    
    option go_package = "google/api;google_api";
    
    option cc_enable_arenas = true;
    option java_multiple_files = true;
    option java_outer_classname = "HttpProto";
    option java_package = "com.google.api";
    
    message Http {
    
        repeated HttpRule rules = 1;
    }
    
    message HttpRule {
    
        string selector = 1;
    
        oneof pattern {
            string get = 2;
    
            string put = 3;
    
            string post = 4;
    
            string delete = 5;
    
            string patch = 6;
    
            CustomHttpPattern custom = 8;
        }
    
        string body = 7;
    
        repeated HttpRule additional_bindings = 11;
    }
    
    message CustomHttpPattern {
    
        string kind = 1;
    
        string path = 2;
    }
    
    • 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

    编写自定义的proto描述文件hello_http.proto

    // ./proto/hello_http/hello_http.proto
    syntax = "proto3";
    package hello_http;
    option go_package = "./hello_http;hello_http";
    
    import "google/api/annotations.proto";
    
    // 定义Hello服务
    service HelloHTTP {
        // 定义SayHello方法
        rpc SayHello(HelloHTTPRequest) returns (HelloHTTPResponse) {
            // http option
            option (google.api.http) = {
                post: "/example/echo"
                body: "*"
            };
        }
    }
    
    // HelloRequest 请求结构
    message HelloHTTPRequest {
        string name = 1;
    }
    
    // HelloResponse 响应结构
    message HelloHTTPResponse {
        string message = 1;
    }
    
    • 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

    这里在原来的SayHello方法定义中增加了http optionPOST方式,路由为/example/echo

    1.3 编译proto

    $ cd proto
    
    # 编译google.api
    $ protoc -I . --go_out=plugins=grpc:. google/api/*.proto
    
    # 编译hello_http.proto
    $ protoc -I . --go_out=plugins=grpc:. hello_http/*.proto
    
    # 编译hello_http.proto gateway
    $ protoc --grpc-gateway_out=logtostderr=true:. hello_http/hello_http.proto
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    注意这里需要编译google/api中的两个proto文件,最后使用grpc-gateway编译生成hello_http_pb.gw.go

    件,这个文件就是用来做协议转换的,查看文件可以看到里面生成的http handler,处理proto文件中定义的路由

    example/echo接收POST参数,调用HelloHTTP服务的客户端请求grpc服务并响应结果。

    1.4 实现服务端

    server.go的内容:

    package main
    
    import (
    	"context"
    	"fmt"
    	"net"
    	// 引入编译生成的包
    	pb "demo/proto/hello_http"
    	"google.golang.org/grpc"
    	"log"
    )
    
    const (
    	// Address gRPC服务地址
    	Address = "127.0.0.1:50053"
    )
    
    // 定义helloService并实现约定的接口
    type helloService struct{}
    
    // HelloService Hello服务
    var HelloService = helloService{}
    
    // SayHello 实现Hello服务接口
    func (h helloService) SayHello(ctx context.Context, in *pb.HelloHTTPRequest) (*pb.HelloHTTPResponse, error) {
    	resp := new(pb.HelloHTTPResponse)
    	resp.Message = fmt.Sprintf("Hello %s.", in.Name)
    	return resp, nil
    }
    
    func main() {
    	listen, err := net.Listen("tcp", Address)
    	if err != nil {
    		log.Fatalf("Failed to listen: %v", err)
    	}
    	// 实例化grpc Server
    	s := grpc.NewServer()
    	// 注册HelloService
    	pb.RegisterHelloHTTPServer(s, HelloService)
    	log.Println("Listen on " + Address)
    	s.Serve(listen)
    }
    
    • 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

    1.5 客户端实现

    client.go的内容:

    package main
    
    import (
    	"context"
    	pb "demo/proto/hello_http"
    	"google.golang.org/grpc"
    	"log"
    )
    
    const (
    	// Address gRPC服务地址
    	Address = "127.0.0.1:50053"
    )
    
    func main() {
    	// 连接
    	conn, err := grpc.Dial(Address, grpc.WithInsecure())
    	if err != nil {
    		log.Fatalln(err)
    	}
    	defer conn.Close()
    	// 初始化客户端
    	c := pb.NewHelloHTTPClient(conn)
    	// 调用方法
    	req := &pb.HelloHTTPRequest{Name: "gRPC"}
    	res, err := c.SayHello(context.Background(), req)
    	if err != nil {
    		log.Fatalln(err)
    	}
    	log.Println(res.Message)
    }
    
    • 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

    1.6 http server

    server_http.go的内容:

    package main
    
    import (
    	"fmt"
    	"github.com/grpc-ecosystem/grpc-gateway/runtime"
    	gw "demo/proto/hello_http"
    	"golang.org/x/net/context"
    	"google.golang.org/grpc"
    	"log"
    	"net/http"
    )
    
    func main() {
    	ctx := context.Background()
    	ctx, cancel := context.WithCancel(ctx)
    	defer cancel()
    	// grpc服务地址
    	endpoint := "127.0.0.1:50053"
    	mux := runtime.NewServeMux()
    	opts := []grpc.DialOption{grpc.WithInsecure()}
    	// HTTP转grpc
    	err := gw.RegisterHelloHTTPHandlerFromEndpoint(ctx, mux, endpoint, opts)
    	if err != nil {
    		log.Fatalf("Register handler err:%v\n", err)
    	}
    	log.Println("HTTP Listen on 8080")
    	http.ListenAndServe(":8080", mux)
    }
    
    • 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

    就是这么简单。开启了一个http server,收到请求后根据路由转发请求到对应的RPC接口获得结果。grpc-

    gateway做的事情就是帮我们自动生成了转换过程的实现。

    1.7 测试

    依次开启gRPC服务和HTTP服务端:

    [root@zsx demo]# go run server.go
    2023/02/12 09:38:52 Listen on 127.0.0.1:50053
    [root@zsx demo]# go run server_http.go
    2023/02/12 09:39:07 HTTP Listen on 8080
    
    • 1
    • 2
    • 3
    • 4

    调用grpc客户端:

    [root@zsx demo]# go run client.go
    2023/02/12 09:39:37 Hello gRPC.
    
    • 1
    • 2
    # 发送 HTTP 请求
    [root@zsx demo]# curl -X POST -k http://localhost:8080/example/echo -d "{\"name\":\"gRPC-HTTP\"}"
    {"message":"Hello gRPC-HTTP."}
    
    • 1
    • 2
    • 3
    # 项目结构
    $ tree demo
    demo
    ├── client.go
    ├── go.mod
    ├── go.sum
    ├── proto
    │   ├── google # googleApi http-proto定义
    │   │   └── api
    │   │       ├── annotations.pb.go
    │   │       ├── annotations.proto
    │   │       ├── http.pb.go
    │   │       └── http.proto
    │   └── hello_http
    │       ├── hello_http.pb.go
    │       ├── hello_http.pb.gw.go # gateway编译后文件
    │       └── hello_http.proto
    ├── server.go # gRPC服务端
    └── server_http.go # HTTP服务端
    
    4 directories, 12 files
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    1.8 升级版服务端(gRPC转换HTTP)

    上面的使用方式已经实现了我们最初的需求,grpc-gateway 项目中提供的示例也是这种使用方式,这样后台需

    要开启两个服务两个端口。其实我们也可以只开启一个服务,同时提供http和gRPC调用方式。

    新建一个项目,基于上面的项目改造,客户端只要修改调用的proto包地址就可以了。

    1.8.1 服务端实现

    server.go的内容:

    package main
    
    import (
    	"crypto/tls"
    	"github.com/grpc-ecosystem/grpc-gateway/runtime"
    	pb "demo/proto/hello_http"
    	"golang.org/x/net/context"
    	"golang.org/x/net/http2"
    	"google.golang.org/grpc"
    	"google.golang.org/grpc/credentials"
    	"log"
    	"io/ioutil"
    	"net"
    	"net/http"
    	"strings"
    )
    
    // 定义helloHTTPService并实现约定的接口
    type helloHTTPService struct{}
    
    // HelloHTTPService Hello HTTP服务
    var HelloHTTPService = helloHTTPService{}
    
    // SayHello 实现Hello服务接口
    func (h helloHTTPService) SayHello(ctx context.Context, in *pb.HelloHTTPRequest) (*pb.HelloHTTPResponse, error) {
    	resp := new(pb.HelloHTTPResponse)
    	resp.Message = "Hello " + in.Name + "."
    	return resp, nil
    }
    
    func main() {
    	endpoint := "127.0.0.1:50052"
    	conn, err := net.Listen("tcp", endpoint)
    	if err != nil {
    		log.Fatalf("TCP Listen err:%v\n", err)
    	}
    	// grpc tls server
    	creds, err := credentials.NewServerTLSFromFile("./cert/server/server.pem", "./cert/server/server.key")
    	if err != nil {
    		log.Fatalf("Failed to create server TLS credentials %v", err)
    	}
    	grpcServer := grpc.NewServer(grpc.Creds(creds))
    	pb.RegisterHelloHTTPServer(grpcServer, HelloHTTPService)
    	// gw server
    	ctx := context.Background()
    	dcreds, err := credentials.NewClientTLSFromFile("./cert/server/server.pem", "test.example.com")
    	if err != nil {
    		log.Fatalf("Failed to create client TLS credentials %v", err)
    	}
    	dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)}
    	gwmux := runtime.NewServeMux()
    	if err = pb.RegisterHelloHTTPHandlerFromEndpoint(ctx, gwmux, endpoint, dopts); err != nil {
    		log.Fatalf("Failed to register gw server: %v\n", err)
    	}
    	// http服务
    	mux := http.NewServeMux()
    	mux.Handle("/", gwmux)
    	srv := &http.Server{
    		Addr:      endpoint,
    		Handler:   grpcHandlerFunc(grpcServer, mux),
    		TLSConfig: getTLSConfig(),
    	}
    	log.Printf("gRPC and https listen on: %s\n", endpoint)
    	if err = srv.Serve(tls.NewListener(conn, srv.TLSConfig)); err != nil {
    		log.Fatal("ListenAndServe: ", err)
    	}
    	return
    }
    
    func getTLSConfig() *tls.Config {
    	cert, _ := ioutil.ReadFile("./cert/server/server.pem")
    	key, _ := ioutil.ReadFile("./cert/server/server.key")
    	var demoKeyPair *tls.Certificate
    	pair, err := tls.X509KeyPair(cert, key)
    	if err != nil {
    		log.Fatalf("TLS KeyPair err: %v\n", err)
    	}
    	demoKeyPair = &pair
    	return &tls.Config{
    		Certificates: []tls.Certificate{*demoKeyPair},
    		NextProtos:   []string{http2.NextProtoTLS}, // HTTP2 TLS支持
    	}
    }
    
    // grpcHandlerFunc returns an http.Handler that delegates to grpcServer on incoming gRPC
    // connections or otherHandler otherwise. Copied from cockroachdb.
    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)
    		}
    	})
    }
    
    • 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
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100

    gRPC服务端接口的实现没有区别,重点在于HTTP服务的实现。gRPC是基于http2实现的,net/http包也实现了

    http2,所以我们可以开启一个HTTP服务同时服务两个版本的协议,在注册http handler的时候,在方法

    grpcHandlerFunc中检测请求头信息,决定是直接调用gRPC服务,还是使用gateway的HTTP服务。

    net/http中对http2的支持要求开启https,所以这里要求使用https服务。

    步骤:

    • 注册开启TLS的grpc服务
    • 注册开启TLS的gateway服务,地址指向grpc服务
    • 开启HTTP server
    1.8.2 客户端实现

    client.go的内容:

    package main
    
    import (
    	"context"
    	// 引入proto包
    	pb "demo/proto/hello_http"
    	"google.golang.org/grpc"
    	// 引入grpc认证包
    	"google.golang.org/grpc/credentials"
    	"log"
    )
    
    const (
    	// Address gRPC服务地址
    	Address = "127.0.0.1:50052"
    )
    
    func main() {
    	log.Println("客户端连接!")
    	// TLS连接
    	creds, err := credentials.NewClientTLSFromFile("./cert/server/server.pem", "test.example.com")
    	if err != nil {
    		log.Fatalf("Failed to create TLS credentials %v", err)
    	}
    	conn, err := grpc.Dial(Address, grpc.WithTransportCredentials(creds))
    	if err != nil {
    		log.Fatalln("err:", err)
    	}
    	defer conn.Close()
    	// 初始化客户端
    	c := pb.NewHelloHTTPClient(conn)
    	// 调用方法
    	req := &pb.HelloHTTPRequest{Name: "gRPC"}
    	res, err := c.SayHello(context.Background(), req)
    	if err != nil {
    		log.Fatalln(err)
    	}
    	log.Println(res.Message)
    }
    
    • 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
    1.8.3 测试
    [root@zsx demo]# go run server.go
    2023/02/12 09:57:44 gRPC and https listen on: 127.0.0.1:50052
    
    [root@zsx demo]# go run  client.go
    2023/02/12 09:59:46 客户端连接!
    2023/02/12 09:59:46 Hello gRPC.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    # 发送 HTTP 请求
    [root@zsx demo]# curl -X POST -k https://localhost:50052/example/echo -d "{\"name\":\"gRPC-HTTP\"}"
    {"message":"Hello gRPC-HTTP."}
    
    • 1
    • 2
    • 3
    # 项目结构
    $ tree demo/
    demo/
    ├── cert
    │   ├── ca.crt
    │   ├── ca.csr
    │   ├── ca.key
    │   ├── ca.srl
    │   ├── client
    │   │   ├── client.csr
    │   │   ├── client.key
    │   │   └── client.pem
    │   ├── openssl.cnf
    │   └── server
    │       ├── server.csr
    │       ├── server.key
    │       └── server.pem
    ├── client.go
    ├── go.mod
    ├── go.sum
    ├── proto
    │   ├── google
    │   │   └── api
    │   │       ├── annotations.pb.go
    │   │       ├── annotations.proto
    │   │       ├── http.pb.go
    │   │       └── http.proto
    │   └── hello_http
    │       ├── hello_http.pb.go
    │       ├── hello_http.pb.gw.go
    │       └── hello_http.proto
    └── 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
  • 相关阅读:
    住院病历的病历打印纸要求多大?
    【从零开始学习 SystemVerilog】3.1.3、SystemVerilog 控制流—— for 循环
    MacOS使用clang
    文生图的最新进展:从一致性模型CMs、LCM、SDXL到Stable Diffusion3、SDXL-Lightning
    M3 MacBook Pro 能提效?程序员、产品经理自证后,CTO:你赢了,新电脑在路上了...
    医疗健康产品展:联影医疗
    如何编写 Kubernetes 的 YAML 文件?
    C#的奇技淫巧:利用WinRM来远程操控其他服务器上的进程
    Redis7新特性简介及十大数据类型
    Spark 8:Spark SQL 执行流程、执行引擎
  • 原文地址:https://blog.csdn.net/qq_30614345/article/details/133831946