Golang——gRPC gateway网关

发布于:2024-06-16 ⋅ 阅读:(17) ⋅ 点赞:(0)

前言

        etcd3 API全面升级为gRPC后,同时要提供REST API服务,维护两个版本的服务显然不大合理,所以gRPC-gateway诞生了。通过protobuf的自定义option实现了一个网关。服务端同时开启gRPC和HTTP服务,HTTP服务接收客户端请求后转换为grpc请求数据,获取响应后转为json数据放回给客户端。

 安装gRPC-gateway:go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway

目录结构

        这里用到了google官方Api中的两个proto描述文件,直接拷贝不需要做修改,里面定义了protocol buffer扩展的HTTP option,为grpc的http转换提供了支持。

 Proto文件

annotations.proto:

// ./proto/google/api/annotations.proto
syntax = "proto3";

package google.api;

option go_package = "google_api";

import "http.proto";
import "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:

// ./proto/google/api/http.proto
syntax = "proto3";

package google.api;

option go_package = "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;
}

编写自定义的hello_http.proto:

        这里在SayHello方法定义中增加了http option,POST方法,路由为"/example/echo"。

syntax="proto3";
package hello_http;

import "annotations.proto";

service HelloHTTP {
    rpc SayHello(HelloHTTPRequest) returns (HelloHTTPResponse){
        //http option
        option(google.api.http) = {
            post:"/example/echo"
            body:"*"
        };
    }

}

message HelloHTTPRequest{
    string name = 1;
}

message HelloHTTPResponse{
    string message = 1;
}

 在生成go对应的proto文件时,报了错,仅供参考:

         这个是因为找不到import的proto文件。可以使用-I或--proto_path。protoc命令中的proto_path参数用于指定proto文件的搜索路径。可以设置多个。

         生成对应的*.pb.go文件:

#编译hello_http.proto文件
#D:\gocode\src\grpc_gateway\proto\google\api地址为import的annotations.proto和http.proto文件位置
#D:\gocode\pkg\mod\google.golang.org\protobuf@v1.34.2\src\google\protobuf为annotations.proto文件import的descriptor.proto文件位置
protoc --go_out=plugins=grpc:. -I=. .\hello_http.proto -I=D:\gocode\src\grpc_gateway\proto\google\api -I=D:\gocode\pkg\mod\google.golang.org\protobuf@v1.34.2\src\google\protobuf

#编译annotations.proto文件
#D:\gocode\src\grpc_gateway\proto\google\api地址为import的http.proto文件位置
#D:\gocode\pkg\mod\google.golang.org\protobuf@v1.34.2\src\google\protobuf为annotations.proto文件import的descriptor.proto文件位置
protoc --go_out=. -I=.\ .\annotations.proto  -I=D:\gocode\src\grpc_gateway\proto\google\api  -I=D:\gocode\pkg\mod\google.golang.org\protobuf@v1.34.2\src\google\protobuf

#编译http.proto文件
protoc --go_out=. -I=.\ .\http.proto

#编译hello_http.proro gateway
protoc --grpc-gateway_out=logtostderr=true:. -I=. .\hello_http.proto -I=D:\gocode\src\grpc_gateway\proto\google\api -I=D:\gocode\pkg\mod\google.golang.org\protobuf@v1.34.2\src\google\protobuf

         注意这里需要编译google/api中的两个proto文件,同时在编译hello_http.proto时使用了grpc-gateway编译生成hello_http.pb.gw.go文件,这个文件时用来协议转换的,查看文件可以看到里面生成的http handler,处理proto文件中定义的路由"example/echo"接收POST参数,调用HelloHTTP服务的客户端请求grpc服务并响应结果。

实现服务器

package main

import (
	"context"
	"fmt"
	"log"
	"net"
	"sample-app/grpc_gateway/proto/hello_http"

	"google.golang.org/grpc"
)

// gRPC服务地址
var addr = "127.0.0.1:8080"

type helloService struct{}

var HelloService = helloService{}

// 实现约定接口
func (h helloService) SayHello(ctx context.Context, in *hello_http.HelloHTTPRequest) (*hello_http.HelloHTTPResponse, error) {
	resp := new(hello_http.HelloHTTPResponse)
	resp.Message = fmt.Sprintf("Hello %s\n", in.Name)
	return resp, nil
}

func main() {
	//监听连接
	ls, err := net.Listen("tcp", addr)
	if err != nil {
		return
	}
	//实例化grpc Server
	s := grpc.NewServer()
	//组成HelloService服务
	hello_http.RegisterHelloHTTPServer(s, HelloService)

	log.Println("Listen on " + addr)
	s.Serve(ls)
}

实现客户端

package main

import (
	"context"
	"fmt"
	"sample-app/grpc_gateway/proto/hello_http"

	"google.golang.org/grpc"
)

var addr = "127.0.0.1:8080"

func main() {
	//连接
	conn, err := grpc.Dial(addr, grpc.WithInsecure())
	if err != nil {
		return
	}
	//初始化客户端
	c := hello_http.NewHelloHTTPClient(conn)
	//发送请求
	resp, err := c.SayHello(context.Background(), &hello_http.HelloHTTPRequest{Name: "gRPC"})
	if err != nil {
		return
	}

	fmt.Println(resp.Message)
}

实现http server

package main

import (
	"context"
	"fmt"
	"net/http"
	"sample-app/grpc_gateway/proto/hello_http"

	"github.com/grpc-ecosystem/grpc-gateway/runtime"
	"google.golang.org/grpc"
)

func main() {
	//定义上下文
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	endpoint := "127.0.0.1:8080"
	mux := runtime.NewServeMux()
	var opts = []grpc.DialOption{grpc.WithInsecure()}
	//HTTP转grpc
	err := hello_http.RegisterHelloHTTPHandlerFromEndpoint(ctx, mux, endpoint, opts)
	if err != nil {
		return
	}

	fmt.Println("http listen on 8081")
	http.ListenAndServe(":8081", mux)
}

         就是这么简单。开启了一个http server,收到请求后根据路由转发请求到对应的RPC接口获得结果。grpc-gateway做的事情就是帮我们自动生成了转换过程的实现。

运行结果

1. 启动服务器:

 2. 启动server http

3. 启动客户端

升级版服务端 

         上面的使用方式已经实现了我们最初的需求,grpc-gateway项目中提供的示例也是这种方式,这样后台需要开启两个服务两个端口。其实我们也可以只开启一个服务,同时提供http和gRPC调用方式。

        新建一个项目hello_http2。目录结构:

        proto文件和上面的一样。

        生成私钥和秘钥:Golang——gRPC认证和拦截器-CSDN博客 

  • 服务端代码 
package main

import (
	"context"
	"crypto/tls"
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"sample-app/grpc_gateway/proto/hello_http"
	"strings"

	"github.com/grpc-ecosystem/grpc-gateway/runtime"
	"golang.org/x/net/http2"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
)

type helloService struct{}

var HelloService = helloService{}

func (h helloService) SayHello(c context.Context, req *hello_http.HelloHTTPRequest) (*hello_http.HelloHTTPResponse, error) {
	resp := new(hello_http.HelloHTTPResponse)
	resp.Message = fmt.Sprintf("Hello %s", req.Name)
	return resp, nil
}

func main() {
	endpoint := "127.0.0.1:8080"
	//监听连接
	conn, err := net.Listen("tcp", endpoint)
	if err != nil {
		log.Println("listen fail ", err)
		return
	}
	//grpc服务
	cred, err := credentials.NewServerTLSFromFile("..\\..\\key\\server.pem", "..\\..\\key\\server_private.key")
	if err != nil {
		log.Println("credentials fail ", err)
		return
	}
	s := grpc.NewServer(grpc.Creds(cred))
	hello_http.RegisterHelloHTTPServer(s, HelloService)

	//gateway 服务
	ctx := context.Background()
	//与grpc服务交互时,需要TLS认证
	dcred, err := credentials.NewClientTLSFromFile("..\\..\\key\\server.pem", "www.wy.com")
	if err != nil {
		log.Println("NewClientTLSFromFile fail ", err)
		return
	}

	dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcred)}
	gwmux := runtime.NewServeMux()
	//注册http转grpc
	if err = hello_http.RegisterHelloHTTPHandlerFromEndpoint(ctx, gwmux, endpoint, dopts); err != nil {
		log.Println("RegisterHelloHTTPHandlerFromEndpoint fail", err)
		return
	}

	//http服务
	mux := http.NewServeMux()
	mux.Handle("/", gwmux)

	srv := &http.Server{
		Addr:      endpoint,
		Handler:   grpcHanderFunc(s, mux),
		TLSConfig: getTLSConfig(),
	}

	if err = srv.Serve(tls.NewListener(conn, srv.TLSConfig)); err != nil {
		log.Println("srv server fail ", err)
	}
}

func getTLSConfig() *tls.Config {
	cert, _ := ioutil.ReadFile("..\\..\\key\\server.pem")
	key, _ := ioutil.ReadFile("..\\..\\key\\server_private.key")
	var demoKeyPair *tls.Certificate
	pair, err := tls.X509KeyPair(cert, key)
	if err != nil {
		log.Println("X509KeyPair fail ", err)
		return nil
	}

	demoKeyPair = &pair
	return &tls.Config{
		Certificates: []tls.Certificate{*demoKeyPair},
		NextProtos:   []string{http2.NextProtoTLS}, //http2 TLS支持
	}
}

// grpcHanderFunc return a http.Handler that delegates to grpcServer on incoming gRPC
// connections or otherHandler otherwise. Copied from cockroachdb
func grpcHanderFunc(grpcServer *grpc.Server, otherHandle *http.ServeMux) http.Handler {
	if otherHandle == nil {
		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
			grpcServer.ServeHTTP(w, req)
		})
	}

	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		if req.ProtoMajor == 2 && strings.Contains(req.Header.Get("Content-Type"), "application/grpc") {
			grpcServer.ServeHTTP(w, req)
		} else {
			otherHandle.ServeHTTP(w, req)
		}
	})
}

        gRPC服务端接口的实现没有区别,重点在于HTTP服务的实现。gRPC是基于http2实现的,net/http包也实现了http2,所以我们可以开启一个HTTP服务同时服务两个版本的协议,在注册http handler的时候,在方法grpcHandlerFunc中检测请求头信息,决定是直接使用调用gRPC服务还是使用gateway的HTTP服务。net/http中对http2的支持要求开启https,所以这里要求使用http服务。

步骤:

  • 注册开启TLS的grpc服务
  • 注册开启TLS的gateway服务,地址指向grpc服务。
  • 开启HTTP server

运行结果:

 


网站公告

今日签到

点亮在社区的每一天
去签到