1、介绍

正当有这个需求的时候,就看到了这个实现姿势。源自coreos的一篇博客,转载到了grpc官方博客 gRPC with REST and Open APIs

etcd3 改用 grpc 后为了兼容原来的api,同时要提供 http/json 方式的API,为了满足这个需求,要么开发两套API,要么实现一种转换机制,他们选择了后者,而我们选择跟随他们的脚步。

他们实现了一个协议转换的网关,对应github上的项目 grpc-gateway ,这个网关负责接收客户端请求,然后决定直接转发给grpc 服务还是转给http服务,当然,http服务也需要请求grpc服务获取响应,然后转为 json 响应给客户端。

结构如图:

基于hello-tls项目扩展,客户端改动不大,服务端和proto改动较大。

2、安装grpc-gateway

# 如这个命令不起作用或者报错
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway

# 使用这个
go install github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway@latest

备注:目前 grpc-gateway 已经升级到 v2

v2 的安装如下:

go install \
    github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \
    github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 \
    google.golang.org/protobuf/cmd/protoc-gen-go \
    google.golang.org/grpc/cmd/protoc-gen-go-grpc

3、代码示例

当前还是以非v2版本示例。

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

在项目根目录创建 third_party 目录,用于存放 第三方扩展。

上述文件可以在一下GitHub库找到:

https://github.com/googleapis/googleapis

此存储库包含支持 REST 和 gRPC 协议的公共 Google API 的原始接口定义。

由于 annotations.proto 中还 引用了 google/protobuf/descriptor.proto 故最好 descriptor.proto 也拷贝进去

具体效果如下图:

Step 1. 编写proto描述文件

pb/hello.proto文件如下:

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

syntax = "proto3";

package pb;  // 当前包名


option go_package = "./;pb";

import "google/api/annotations.proto";

message HelloRequest{
  string  name = 1;
}

message HelloResponse{
  string  result = 1;
}

service HelloService {
  rpc SayHello(HelloRequest) returns (HelloResponse) {
    // 这里在原来的SayHello方法定义中增加了http option, POST方式,路由为”/example/echo”。
    option (google.api.http) = {
      post: "/example/echo",
      body: "*"
    };
  }
}

Step 2. 编译proto

protoc -I ./pb --proto_path=./third_party --go_out ./pb --go_opt paths=source_relative --go-grpc_out ./pb --go-grpc_opt paths=source_relative --grpc-gateway_out ./pb --grpc-gateway_opt paths=source_relative ./pb/hello.proto

备注:查看百度很多其他文档,都报错,只有这个才有效果

这里在编译的 时候会引入 third_party 目录中 proto文件,最后使用grpc-gateway编译生成hello.pb.gw.go文件。

这个文件就是用来做协议转换的,查看文件可以看到里面生成的http handler,处理proto文件中定义的路由example/echo接收POST参数,调用 HelloHTTP 服务的客户端请求grpc服务并响应结果。

Step 3: 实现服务端和客户端

这里主要是服务端的修改

服务端代码如下:

package main

import (
    "context"
    "crypto/tls"
    "fmt"
    "github.com/grpc-ecosystem/grpc-gateway/runtime"
    "go_study/framework/grpc/pb"
    "golang.org/x/net/http2"
    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc/grpclog"
    "google.golang.org/grpc/metadata"
    "google.golang.org/grpc/status"
    "io/ioutil"
    "net"
    "net/http"
    "strings"
)


// 定义 UserInfoService 服务
type UserInfoService struct{
    pb.UnimplementedUserInfoServiceServer
}

// GetUserInfo 实现方法
func (s *UserInfoService) GetUserInfo(ctx context.Context, req *pb.UserRequest) (resp *pb.UserResponse, err error) {
    //err = tokenAuth(ctx)
    //if err != nil {
    //    return nil, err
    //}
    // 通过用户名查询用户信息
    name := req.Name
    // 数据里查用户信息
    if name == "zs" {
        resp = &pb.UserResponse{
            Id:    1,
            Name:  name,
            Age:   22,
            Hobby: []string{"Sing", "Run"},
        }
    }
    return
}

type HelloService struct {
    pb.UnimplementedHelloServiceServer
}

func (h *HelloService) SayHello(ctx context.Context, in *pb.HelloRequest) (resp *pb.HelloResponse, err error) {
    //err = tokenAuth(ctx)
    //if err != nil {
    //    return nil, err
    //}
    resp = new(pb.HelloResponse)
    resp.Result  = fmt.Sprintf("hello %s", in.Name)
    return resp, nil
}

// 使用这种方式验证token 每个服务端每个服务中都验证一次,比较繁琐
func tokenAuth(ctx context.Context) error {
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return status.Errorf(codes.Unauthenticated, "无Token认证信息")
    }

    var (
        appid  string
        appkey string
    )

    if val, ok := md["appid"]; ok {
        appid = val[0]
    }

    if val, ok := md["appkey"]; ok {
        appkey = val[0]
    }

    if appid != "101010" || appkey != "i am key" {
        return status.Errorf(codes.Unauthenticated, "Token认证信息无效: appid=%s, appkey=%s", appid, appkey)
    }

    return nil
}

// interceptor 拦截器
// grpc.UnaryServerInfo 包含有关服务器端一元 RPC 的各种信息。所有 per-rpc 信息都可能被拦截器改变
func interceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    err := tokenAuth(ctx)
    if err != nil {
        return nil, err
    }
    // 继续处理请求
    return handler(ctx, req)
}

// grpcHandlerFunc 检查请求协议并返回http handler
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)
        }
    })
}

func getTLSConfig() *tls.Config {
    cert, _ := ioutil.ReadFile("./framework/grpc/keys/server.pem")
    key, _ := ioutil.ReadFile("./framework/grpc/keys/server.key")
    var demoKeyPair *tls.Certificate
    pair, err := tls.X509KeyPair(cert, key)
    if err != nil {
        grpclog.Fatalf("TLS KeyPair err: %v\n", err)
    }
    demoKeyPair = &pair
    return &tls.Config{
        Certificates: []tls.Certificate{*demoKeyPair},
        NextProtos:   []string{http2.NextProtoTLS}, // HTTP2 TLS支持
    }
}


func main(){

    // 地址
    //addr := "127.0.0.1:8180"
    endpoint := "127.0.0.1:8180"

    // 1.监听
    conn, err := net.Listen("tcp", endpoint)
    if err != nil {
        grpclog.Fatalf("Failed to listen: %v", err)
    }
    fmt.Printf("监听端口:%s\n", endpoint)

    // tls认证
    creds,err := credentials.NewServerTLSFromFile("./framework/grpc/keys/server.pem", "./framework/grpc/keys/server.key")
    if err != nil {
        grpclog.Fatalf("Failed to generate credentials %v", err)
    }

    var opts []grpc.ServerOption

    // 添加 tls 认证
    opts = append(opts, grpc.Creds(creds))

    // 注册拦截器
    opts = append(opts, grpc.UnaryInterceptor(interceptor))

    // 2.实例化gRPC
    s := grpc.NewServer(opts...)

    // 3.在gRPC上注册微服务
    var u = UserInfoService{}
    pb.RegisterUserInfoServiceServer(s, &u)

    var h = HelloService{}
    pb.RegisterHelloServiceServer(s, &h)

    // gw server
    ctx := context.Background()
    dcreds, err := credentials.NewClientTLSFromFile("./framework/grpc/keys/server.pem", "www.admincms.com")
    if err != nil {
        grpclog.Fatalf("Failed to create client TLS credentials %v", err)
    }
    dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)}
    gwmux := runtime.NewServeMux()

    if err = pb.RegisterHelloServiceHandlerFromEndpoint(ctx, gwmux, endpoint, dopts); err != nil {
        grpclog.Fatalf("Failed to register gw server: %v\n", err)
    }

    if err = pb.RegisterHelloServiceHandlerFromEndpoint(ctx, gwmux, endpoint, dopts); err != nil {
        grpclog.Fatalf("Failed to register gw server: %v\n", err)
    }

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

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

    grpclog.Infof("gRPC and https listen on: %s\n", endpoint)

    if err = srv.Serve(tls.NewListener(conn, srv.TLSConfig)); err != nil {
        grpclog.Fatal("ListenAndServe: ", err)
    }

    return
}

客户端代码不变:

package main

import (
    "context"
    "fmt"
    "go_study/framework/grpc/pb" // 导入proto
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc/grpclog"
    "time"
)

// OpenTLS 是否开启TLS认证
var OpenTLS = true

type customCredential struct {}

func (c *customCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
    return map[string]string{
        "appid":  "101010",
        "appkey": "i am key",
    }, nil
}

func (c *customCredential) RequireTransportSecurity() bool {
    return OpenTLS
}

// interceptor 客户端拦截器
func interceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
    start := time.Now()
    err := invoker(ctx, method, req, reply, cc, opts...)
    grpclog.Infof("method=%s req=%v rep=%v duration=%s error=%v\n", method, req, reply, time.Since(start), err)
    return err
}


func main(){
    addr := "127.0.0.1:8180"
    var err error
    var opts []grpc.DialOption

    if OpenTLS {
        // TLS连接  记得把server name改成你写的服务器地址
        creds, err := credentials.NewClientTLSFromFile("./framework/grpc/keys/server.pem", "www.admincms.com")
        if err != nil {
            grpclog.Fatalf("Failed to create TLS credentials %v", err)
        }
        opts = append(opts, grpc.WithTransportCredentials(creds))

    }else{
        opts = append(opts, grpc.WithInsecure())
    }

    // 使用自定义认证
    opts = append(opts, grpc.WithPerRPCCredentials(new(customCredential)))

    // 使用客户端拦截器
    opts = append(opts, grpc.WithUnaryInterceptor(interceptor))

    // 1.连接
    conn, err := grpc.Dial(addr, opts...)
    if err != nil {
        grpclog.Fatalln(err)
    }
    defer conn.Close()

    // 2. 实例化gRPC客户端
    client := pb.NewUserInfoServiceClient(conn)

    // 3.组装请求参数
    req := new(pb.UserRequest)
    req.Name = "zs"


    // 4. 调用接口
    response, err := client.GetUserInfo(context.Background(), req)
    if err != nil {
        grpclog.Fatalln(err)
    }
    fmt.Printf("响应结果: %v\n", response)

    // 客户端调用HelloService
    hClient := pb.NewHelloServiceClient(conn)
    res1,err := hClient.SayHello(context.Background(), &pb.HelloRequest{
        Name: "grpc",
    })
    if err != nil {
        grpclog.Fatalln(err)
    }
    fmt.Println("响应结果:", res1)
}

运行结果

开启服务端:

API server listening at: 127.0.0.1:6334
监听端口:127.0.0.1:8180

运行客户端:

API server listening at: 127.0.0.1:6612
响应结果: id:1  name:"zs"  age:22  hobby:"Sing"  hobby:"Run"
响应结果: result:"hello grpc"

HTTP 请求(有token验证的,注意在请求头中配置metaData):

备注:HTTP方式的 metaData 数据 放在 header 中,使用 Grpc-Metadata- 前缀。

作者:joker.liu  创建时间:2023-05-23 15:49
最后编辑:joker.liu  更新时间:2023-05-24 14:59