1 前言

本文使用的认证方式为 双向认证,也可改为其他认证方式,可参考 gRPC教程 — TLS单向认证、双向认证、Token认证、拦截器 进行改造,本文不再赘述。

代码:https://gitee.com/XiMuQi/go-grpc

2 Grpc网关介绍

2.1 原因

etcd3 API全面升级为gRPC后,同时还要提供REST API服务,维护两个版本的服务就显得不太合理,所以 grpc-gateway 诞生了。通过使用protobuf的自定义option实现了一个网关,服务端可以同时开启Grpc服务和HTTP服务,HTTP服务负责接收客户端请求,然后将请求信息转换protobuf格式作为 Grpc 请求数据,再发送至Grpc服务,HTTP服务等从Grpc服务获取响应后再转为JSON数据返回给客户端。

2.2 补充

Grpc-Gateway是Protocol Buffers编译器协议的一个插件。它读取Protobuf服务定义并生成一个反向代理服务器,该服务器将RESTful HTTP API转换为gRPC。这种服务是根据google.api.http annotations注解生成的,所以在项目中会使用到该注解。

2.3 流程

请求流程,当客户端发送http请求时候,grpc-gateway接受请求,生成grpc的client去请求grpc的server端;grpc-gateway实际上做了反向代理的作用。因此会产生两个服务,一个是grpc-gateway产生的http服务,负责接受客户端的http请求,一个grpc的server服务,负责处理client端的请求。

2.4 流程图

在这里插入图片描述

3 环境配置

3.1 需要的依赖

3.1.1 proto转go

go get -u google.golang.org/protobuf/cmd/protoc-gen-go 

3.1.2 grpc

go get -u google.golang.org/grpc/cmd/protoc-gen-go-grpc

3.1.3 grpc-gateway

go get -u github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway

3.1.4 对客户端提供服务的API依赖

go get -u github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2

3.2 下载结果

在这里插入图片描述

4 代码

4.1 加入google/api/annotations.proto

在1.2中我们讲到需要 google/api/annotations.proto,所以还需要将这些文件到项目中,在前面环境配置完成后,去项目的 External Libraries 下 将grpc-gateway包中google包下的内容全部拷贝到项目的一个单独的目录下,注意是用 v1.16.0版本下的,v2.10.2下没有 。

图一
在这里插入图片描述
图二
在这里插入图片描述

4.2 在代码中的位置及整个项目结构

google 文件夹放到了 pbfiles 下:
在这里插入图片描述

4.3 创建protobuf文件

新建的 ProdGrpcGateWay.proto 在 pbfiles 目录下,与google是同级。有的 讲解文档 创建的是 Prod.proto ,其实都一样,但是在本项目中会出现一个小问题,等后面运行时可能会报:

{"code":12, "message":"method GetProdStock not implemented", "details":[]}

出现这个问题的原因是在本项目中集成了 gRPC教程 — TLS单向认证、双向认证、Token认证、拦截器 的代码,多个示例中可能会出现 protobuf 文件内容相同的情况,在实现 GetProdStock 方法时有可能实现的是别的示例接口中的,所以报这个错误。

解决办法
1、修改Prod.proto 里的方法名;(本文使用的是方法)
2、单独一个项目;

ProdGrpcGateWay.proto 内容:

syntax = "proto3"; //proto3的语法,不写会默认为proto2
package services; //包名,通过protoc生成go文件时使用

option go_package = "../service"; //添加生成go文件的路径

//必须要导入
import "google/api/annotations.proto";

message GrpcGateWayRequest {
    int32 goods_id = 1; //传入的商品id
}
message GrpcGateWayResponse {
    int32 goods_stock =1; //商品库存
}
service GrpcGateWayService {

    rpc GetGrpcGateWayStock (GrpcGateWayRequest) returns (GrpcGateWayResponse){
        option (google.api.http) = {
            get: "/v1/prod/{goods_id}" //注意这里的路径参数要和上面 GrpcGateWayRequest 中定义的保持一致
        };
    }
}

4.4 将 ProdGrpcGateWay.proto 转成相关go文件

在 pbfiles目录下执行以下命令,注意不要写错!

4.4.1 生成 ProdGrpcGateWay.pb.goProdGrpcGateWay_grpc.pb.go 的命令:

protoc --go_out=. --go-grpc_out=. *.proto

4.4.2 生成 grpc-gateway使用的 ProdGrpcGateWay.pb.gw.go 命令:

protoc --grpc-gateway_out=logtostderr=true:../service ProdGrpcGateWay.proto

4.5 添加认证证书

将在 双向认证 示例中的相关证书复制一份过来,本文是复制到了 keys2 下,关于生成相关证书的讲解可看前言中的相关链接。

4.6 服务端代码

server.go

package main

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"go-grpc/grpc-gateway/service"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/grpclog"
	"io/ioutil"
	"log"
	"net"
)

const (
	// Address gRPC服务地址
	ServerAddress = "127.0.0.1:8888"
)

//1、声明一个server,里面是未实现的字段
type server struct {
	service.UnimplementedGrpcGateWayServiceServer
}

//2、必须要实现在 ProdGrpcGateWay.proto 里声明的远程调用接口,否则客户端会报:
//rpc error: code = Unimplemented desc = method GetGrpcGateWayStock not implemente
func (s *server) GetGrpcGateWayStock(ctx context.Context, in *service.GrpcGateWayRequest) (*service.GrpcGateWayResponse, error) {
	return &service.GrpcGateWayResponse{GoodsStock: in.GetGoodsId()}, nil
}

func main() {

	//1、创建带ca证书验证的服务端
	rpcServer := grpc.NewServer(grpc.Creds(GetServeCreds()))

	//2、注册服务
	service.RegisterGrpcGateWayServiceServer(rpcServer, &server{})

	//3、设置传输协议和监听地址
	listen, err := net.Listen("tcp", ServerAddress)
	if err != nil {
		log.Fatal("服务监听端口失败", err)
	}
	log.Println("Server listen on " + ServerAddress + " with TLS")

	//4、启动服务
	rpcServer.Serve(listen)
}

//加入服务端认证证书
func GetServeCreds() credentials.TransportCredentials {
	// TLS认证
	//从证书相关文件中读取和解析信息,得到证书公钥、密钥对
	cert, err := tls.LoadX509KeyPair("grpc-gateway/keys2/server.pem", "grpc-gateway/keys2/server.key")
	if err != nil {
		grpclog.Fatalf("Failed to find server credentials %v", err)
	}

	certPool := x509.NewCertPool() //初始化一个CertPool
	ca, err := ioutil.ReadFile("grpc-gateway/keys2/ca.pem")

	if err != nil {
		grpclog.Fatalf("Failed to find root credentials %v", err)
	}

	certPool.AppendCertsFromPEM(ca) //解析传入的证书,解析成功会将其加到池子中
	creds := credentials.NewTLS(&tls.Config{ //构建基于TLS的TransportCredentials选项
		Certificates: []tls.Certificate{cert},        //服务端证书链,可以有多个
		ClientAuth:   tls.RequireAndVerifyClientCert, //要求必须验证客户端证书
		ClientCAs:    certPool,                       //设置根证书的集合,校验方式使用 ClientAuth 中设定的模式
	})
	return creds
}

4.7 客户端服务代码

clientServer.go

package main

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"go-grpc/grpc-gateway/service"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/grpclog"
	"io/ioutil"
	"log"
	"net/http"
)

const (
	// ServerAddress gRPC服务地址
	ServerAddress = "127.0.0.1:8888"

	//ClientAddress 是浏览器等发送http请求时的地址
	ClientAddress = "127.0.0.1:8080"
)

func main() {
	ctx := context.Background()
	ctx, cancelFunc := context.WithCancel(ctx)
	defer cancelFunc()

	//1、创建路由
	mux := runtime.NewServeMux()

	//2、加入认证证书
	opt := []grpc.DialOption{grpc.WithTransportCredentials(GetClientCreds())}

	//3、注册请求服务端的方法
	err := service.RegisterGrpcGateWayServiceHandlerFromEndpoint(ctx, mux, ServerAddress, opt)
	if err != nil {
		log.Fatalf("cannot start grpc gateway: %v", err)
	}

	//4、启动并监听http请求
	err = http.ListenAndServe(ClientAddress, mux)
	if err != nil {
		log.Fatalf("cannot listen and server: %v", err)
	}
	log.Println("ClientServer listen on " + ServerAddress + " with TLS")

}

//加入客户端认证证书
func GetClientCreds() credentials.TransportCredentials {
	// TLS连接
	//从证书相关文件中读取和解析信息,得到证书公钥、密钥对
	cert, err := tls.LoadX509KeyPair("grpc-gateway/keys2/client.pem", "grpc-gateway/keys2/client.key")
	if err != nil {
		grpclog.Fatalf("Failed to find client credentials %v", err)
	}

	certPool := x509.NewCertPool()
	ca, err := ioutil.ReadFile("grpc-gateway/keys2/ca.pem")
	if err != nil {
		grpclog.Fatalf("Failed to find root credentials %v", err)
	}

	certPool.AppendCertsFromPEM(ca)

	creds := credentials.NewTLS(&tls.Config{
		Certificates: []tls.Certificate{cert}, //客户端证书
		ServerName:   "ximu.info",           //注意这里的参数为配置文件中所允许的ServerName,也就是其中配置的DNS...
		RootCAs:      certPool,
	})
	return creds
}


附: client.go(测试与服务端通信是否正常的客户端)

package main

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"go-grpc/grpc-gateway/service"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"io/ioutil"
	"log"
)

const (
	// Address gRPC服务地址
	Address = "127.0.0.1:8888"
)

func main() {

	// 证书认证-双向认证
	// 从证书相关文件中读取和解析信息,得到证书公钥、密钥对
	cert, _ := tls.LoadX509KeyPair("grpc-gateway/keys2/client.pem", "grpc-gateway/keys2/client.key")
	// 创建一个新的、空的 CertPool
	certPool := x509.NewCertPool()
	ca, _ := ioutil.ReadFile("grpc-gateway/keys2/ca.pem")

	//注意这里只能解析pem类型的根证书,所以需要的是ca.pem
	// 尝试解析所传入的 PEM 编码的证书。如果解析成功会将其加到 CertPool 中,便于后面的使用
	certPool.AppendCertsFromPEM(ca)

	// 构建基于 TLS 的 TransportCredentials 选项
	creds := credentials.NewTLS(&tls.Config{
		// 设置证书链,允许包含一个或多个
		Certificates: []tls.Certificate{cert},
		ServerName:   "ximu.info", //注意这里的参数为配置文件中所允许的ServerName,也就是其中配置的DNS...
		RootCAs:      certPool,
	})

	//1、 建立连接
	conn, err := grpc.Dial(Address, grpc.WithTransportCredentials(creds))
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()

	request := &service.GrpcGateWayRequest{
		GoodsId:   123,
	}

	// 2. 调用 ProdGrpcGateWay_grpc.pb.go 中的 NewGrpcGateWayServiceClient 方法建立客户端
	query := service.NewGrpcGateWayServiceClient(conn)

	//3、调用rpc方法
	res, err := query.GetGrpcGateWayStock(context.Background(), request)

	if err != nil {
		log.Fatal("调用gRPC方法错误: ", err)
	}
	fmt.Println("grpc-gateway:调用gRPC方法成功,GoodsStock = ", res)

}

5 测试

5.1 启动服务

先启动 server.go,再启动 clientServer.go

5.2 访问clientServer服务的接口

http://localhost:8080/v1/prod/25

5.3 测试结果

在这里插入图片描述

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐