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

shell 复制代码
$ 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 proto编写

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

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

annotations.proto文件的内容:

protobuf 复制代码
// ./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;

}

http.proto文件的内容:

protobuf 复制代码
// ./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;
}

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

protobuf 复制代码
// ./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;
}

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

1.3 编译proto

shell 复制代码
$ 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

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

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

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

1.4 实现服务端

server.go的内容:

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.5 客户端实现

client.go的内容:

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.6 http server

server_http.go的内容:

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)
}

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

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

1.7 测试

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

elixir 复制代码
[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

调用grpc客户端:

shell 复制代码
[root@zsx demo]# go run client.go
2023/02/12 09:39:37 Hello gRPC.
shell 复制代码
# 发送 HTTP 请求
[root@zsx demo]# curl -X POST -k http://localhost:8080/example/echo -d "{\"name\":\"gRPC-HTTP\"}"
{"message":"Hello gRPC-HTTP."}
shell 复制代码
# 项目结构
$ 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.8 升级版服务端(gRPC转换HTTP)

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

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

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

1.8.1 服务端实现

server.go的内容:

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)
		}
	})
}

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的内容:

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.8.3 测试
shell 复制代码
[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.
shell 复制代码
# 发送 HTTP 请求
[root@zsx demo]# curl -X POST -k https://localhost:50052/example/echo -d "{\"name\":\"gRPC-HTTP\"}"
{"message":"Hello gRPC-HTTP."}
shell 复制代码
# 项目结构
$ 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 小时前
Istio Gateway发布服务
云原生·gateway·istio
柳叶寒2 天前
医院信息化与智能化系统(17)
java·nacos·gateway·全栈·项目
cyt涛2 天前
SpringCloudGateway — 网关路由
java·开发语言·网关·gateway·路由·断言·转发
代码魔法师Sunny4 天前
502 Bad Gateway 错误详解:从表现推测原因,逐步排查直至解决
gateway
RedCong5 天前
通过route访问Openshift上的HTTP request报错504 Gateway Time-out【已解决】
http·gateway·openshift
飞升不如收破烂~6 天前
包括 Nginx、Gateway、Nacos、Dubbo、Sentinel、RocketMQ 和 Seata 的调用链路描述:
nginx·gateway·dubbo
还是转转6 天前
Kong Gateway 指南
gateway·kong
攒了一袋星辰11 天前
微服务网关gateway过滤器工厂与自定义过滤器
微服务·架构·gateway
猫猫不是喵喵.12 天前
Gateway 统一网关
java·网络·微服务·gateway
12 天前
504 Gateway Time-outopenresty
gateway