GRPC框架


源代码:https://gitee.com/g_night/grpc-demo

安装

go get github.com/golang/protobuf/proto
go get google.golang.org/grpc
go get github.com/golang/protobuf/protoc-gen-go
go get google.golang.org/protobuf/reflect/protoreflect
go get google.golang.org/protobuf/runtime/protoimpl
  • 需要 protoc-gen-go.exeprotoc.exe ,在源代码的目录中有个protobuf工具,可以将这些可执行文件放到GOPATH/bin下

入门案例

  1. 编写中间文件:

创建 hello.proto

syntax = "proto3";
package service;

// 用于指定golang package的名字,新版本要求必须包含 /
option go_package = "service/";

message HelloRequest {
  int64 id = 1;
}

message HelloResponse {
  string name = 1;
}

service HelloService {
  rpc SayHello(HelloRequest) returns (HelloResponse);
}
  1. 核心服务代码:

运行指令,生成hello.pb.go

protoc --go_out=plugins=grpc:../ hello.proto
# 规则是: --go_out=目录 proto文件
# 注意:这里需要添加 plugins=grpc: 这个用于指定使用的服务

创建文件hello_service.go,编写核心服务:

package service

import (
	"context"
	"strconv"
)

// hello服务具体实现

type HelloService struct{}

// 方法名&参数  --> 就在 hello.pb.go 的 HelloServiceServer
func (server *HelloService) SayHello(ctx context.Context, req *HelloRequest) (*HelloResponse, error) {
	number := strconv.Itoa(int(req.Id))
	resp := &HelloResponse{Name: "hello word no." + number}
	return resp, nil
}
  1. 运行服务:

创建文件server.go

package main

import (
	"google.golang.org/grpc"
	"google.golang.org/grpc/grpclog"
	"grpc-demo/service"
	"net"
)

// 具体server代码
func main() {

	// 创建新服务
	server := grpc.NewServer()
	// 注册服务
	service.RegisterHelloServiceServer(server, &service.HelloService{})

	// 设置端口监听服务
	listen, err := net.Listen("tcp", "127.0.0.1:9091")
	if err != nil {
		grpclog.Fatalf("Failed to listen: %v", err)
	}
	server.Serve(listen)
}
  1. 测试文件调用服务:

创建文件client_test.go

package main

// 客户端示例

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/grpclog"
	"grpc-demo/service"
	"testing"
)

func TestClient(t *testing.T) {
	// 连接 127.0.0.1:9091,地址要一致
	conn, err := grpc.Dial("127.0.0.1:9091", grpc.WithInsecure())
	if err != nil {
		grpclog.Fatalln(err)
	}
	defer conn.Close()

	// 初始化客户端
	c := service.NewHelloServiceClient(conn)

	// 构造请求
	req := &service.HelloRequest{Id: 1}
	// 调用服务
	resp, err := c.SayHello(context.Background(), req)

	// 服务调用失败
	if err != nil {
		grpclog.Fatalln(err)
	}

	fmt.Println("resp.Name = ", resp.Name)
}

首先运行server.gomain方法,确保服务开启。然后运行client_test.go,就能看到显示:

=== RUN   TestClient
resp.Name =  hello word no.1 # 返回预期结果
--- PASS: TestClient (0.02s)

目录结构(源代码中位于demo1-helloword文件夹):

image-20211119144044235

步骤总结:

  1. 编写中间文件:xxx.proto

  2. 生成代码:protoc --go_out=plugins=grpc:../ xxx.proto

  3. 编写服务核心代码,声明对象,实现方法接口(Name):

    func (s *XXX) Name(ctx context.Context, req *XXXRequest) (*XXXResponse, error)

  4. 运行服务:

    // 创建新服务
    server := grpc.NewServer()
    // 注册服务
    service.RegisterXXXServer(server, &service.XXXService{})
    
    // 设置端口监听服务
    listen, err := net.Listen("tcp", "127.0.0.1:9091")
    if err != nil {
    	grpclog.Fatalf("Failed to listen: %v", err)
    }
    server.Serve(listen)
    

进阶案例

Token鉴权

我们在demo1-helloword的基础上加入token校验,项目文件在源代码的demo2-token

  1. 引入认证包:

    go get google.golang.org/grpc/credentials
    
  2. 客户端加入token信息:

    编写customCredential自定义认证,实现 PerRPCCredentials接口的两个方法:

    • GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)

    • RequireTransportSecurity() bool

package main

// 客户端示例

import (
	"context"
	"demo2-token/service"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/grpclog"
	"testing"
)

// customCredential 自定义认证
type customCredential struct{}

// GetRequestMetadata 实现自定义认证接口
func (c customCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
	return map[string]string{
		"token": "real_token",
	}, nil
}

// RequireTransportSecurity 自定义认证是否开启TLS
func (c customCredential) RequireTransportSecurity() bool {
	return false
}

func TestClient(t *testing.T) {
	var opts []grpc.DialOption
	opts = append(opts, grpc.WithInsecure())
	opts = append(opts, grpc.WithPerRPCCredentials(new(customCredential)))
	normalClient(opts)
}

// 常规请求操作
func normalClient(opts []grpc.DialOption) {
	// 连接 127.0.0.1:9091,地址要一致
	// 加入 token
	conn, err := grpc.Dial("127.0.0.1:9091", opts...)
	if err != nil {
		grpclog.Fatalln(err)
	}
	defer conn.Close()

	// 初始化客户端
	c := service.NewHelloServiceClient(conn)

	// 构造请求
	req := &service.HelloRequest{Id: 1}
	// 调用服务
	resp, err := c.SayHello(context.Background(), req)

	// 服务调用失败
	if err != nil {
		grpclog.Fatalln(err)
	}

	fmt.Println("resp.Name = ", resp.Name)
}
  1. 编写服务端,取出token进行校验:
package main

// 客户端示例

import (
	"context"
	"demo2-token/service"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/grpclog"
	"testing"
)

// customCredential 自定义认证
type customCredential struct{}

// GetRequestMetadata 实现自定义认证接口
func (c customCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
	return map[string]string{
		"token": "real_token",
	}, nil
}

// RequireTransportSecurity 自定义认证是否开启TLS
func (c customCredential) RequireTransportSecurity() bool {
	return false
}

func TestClient(t *testing.T) {
	var opts []grpc.DialOption
	opts = append(opts, grpc.WithInsecure())
	opts = append(opts, grpc.WithPerRPCCredentials(new(customCredential)))
	normalClient(opts)
}

// 常规请求操作
func normalClient(opts []grpc.DialOption) {
	// 连接 127.0.0.1:9091,地址要一致
	// 加入token
	conn, err := grpc.Dial("127.0.0.1:9091", opts...)
	if err != nil {
		grpclog.Fatalln(err)
	}
	defer conn.Close()

	// 初始化客户端
	c := service.NewHelloServiceClient(conn)

	// 构造请求
	req := &service.HelloRequest{Id: 1}
	// 调用服务
	resp, err := c.SayHello(context.Background(), req)

	// 服务调用失败
	if err != nil {
		grpclog.Fatalln(err)
	}

	fmt.Println("resp.Name = ", resp.Name)
}

HTTP网关

通过protobuf的自定义option实现了一个网关,服务端同时开启gRPC和HTTP服务,HTTP服务接收客户端请求后转换为grpc请求数据,获取响应后转为json数据返回给客户端。

  1. 安装服务

    go get github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
    
  2. 修改protoc文件:

    • import "google/api/annotations.proto";
    • 添加 http option

    用到了google官方Api中的两个proto描述文件:annotations.protohttp.proto。注意不要修改!

    # 执行命令
    
    

Protobuf语法总结

本文以 “proto3” 为依据!

  • 文件以.proto做为文件后缀,除结构定义外的语句以分号结尾

  • 结构定义可以包含:message、service、enum

  • 末尾的数字表示在序列化数组里面的顺序。可以定义为任何数字(不能重复),不需要总是从1或者0开始

  • Service、Rpc方法名、Message命名采用驼峰命名方式(首字母大写),字段命名采用 小写字母 + 下划线 分隔方式

  • Enum类型命名采用 驼峰命名 ,而字段命名采用 大写字母 + 下划线 分隔方式

    message HelloRequest {
      required string name = 1;
    }
    enum MyEnums {
      FIRST_VALUE = 1;
    }
    
  • Protobuf定义了一套基本数据类型,对应规则:

.proto C++ Java Go
double double double float64
float float float float32
int32 int32 int int32
int64 int64 long int64
uint32 uint32 int uint32
uint64 uint64 long uint64
sint32 int32 int int32
sint64 int64 long int64
fixed32 uint32 int uint32
fixed64 uint64 long uint64
sfixed32 int32 int int32
sfixed64 int64 long int64
bool bool boolean bool
string string String string
bytes string ByteString []byte

Message

message就相当于结构体,一般定义请求体、响应体。

字段修饰符(其实除非是repeated,否则不用写了):

  • Required: 表示是一个必须字段。(proto3弃用,不要使用了!)
  • Optional:表示是一个可选字段,可以有选择性的设置或者不设置该字段的值。(默认如此,不用声明)
  • Repeated:表示该字段可以包含0~N个元素。可以类比成一个数组的值。

声明细节:

声明的结构体可以嵌套使用:

message SearchResponse {
  Result result = 1;
}
message Result {
  string url = 1;
  string title = 2;
}

如果是内部声明的message类型,则只能内部直接使用

message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
  }
  Result result = 1;
}

Enum

  • 枚举类型中必须包含值为0的元素且为第一个元素,兼容 proto2 语法。
  • 枚举常量必须在32位整型值的范围内。因为enum值是使用可变编码方式的,对负数不够高效,因此不推荐在enum中使用负数。
message SearchRequest {
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
  }
  Corpus corpus = 1;
}

Service

定义RPC服务接口,protocol buffer编译器将会根据所选择的不同语言生成服务接口代码

service HelloService {
  rpc Name(XXXRequest) returns (XXXResponse);
}

Map

如果你希望创建一个关联映射,protocol buffer提供了一种快捷的语法:

map<key_type, value_type> map_field = N;

其中key_type可以是任意Integer或者string类型(所以,除了floating和bytes的任意标量类型都是可以的)value_type可以是任意类型。

例如,如果你希望创建一个project的映射,每个Projecct使用一个string作为key,你可以像下面这样定义:

message SearchRequest {
  map<string, string> mp = 3;
}
  • 从序列化中解析或者融合时,如果有重复的key则后一个key不会被使用。
  • 序列化后的顺序和map迭代器的顺序是乱序的。
  • Map的字段不能被repeated修饰。