gRPC是Google开源的一款RPC框架,跟具体语言无关,以protobuf作为IDL,通过protoc来编译框架代码。gRPC的Java实现的底层网络库是基于Netty开发而来,其Go实现是基于net库。

先说下Protobuf,它是一个纯粹的展示层协议,可以和各种传输层协议一起使用;Protobuf的文档也非常完善。 Protobuf具备了优秀的序列化协议所需的众多典型特征:

  1. 标准的IDL和IDL编译器,这使得其对工程师非常友好。
  2. 序列化数据非常简洁,紧凑,与XML相比,其序列化之后的数据量约为1/3到1/10。
  3. 解析速度非常快,比对应的XML快约20-100倍。
  4. 提供了非常友好的动态库,使用非常简介,反序列化只需要一行代码。
  5. 更好的兼容性,很好的支持向下或向上兼容。

数据类型

  • double: 浮点数
  • float: 单精度浮点
  • int32: int类型,使用可变长编码,编码负数不够高效,如果有负数那么使用sint32
  • sint32: int类型,使用可变长编码, 有符号的整形,比通常的int32高效;
  • uint32: 无符号整数使用可变长编码方式;
  • int64 long long , 使用可变长编码方式。编码负数时不够高效——如果有负数,可以使用sint64;
  • sint64 long long 使用可变长编码方式。有符号的整型值。编码时比通常的int64高效;
  • uint64: 无符号整数使用可变长编码方式;
  • fixed32 : 总是4个字节。如果数值总是比总是比2^28大的话,这个类型会比uint32高效。
  • fixed64: 总是8个字节。如果数值总是比总是比2^56大的话,这个类型会比uint64高效。
  • sfixed32: 总是4个字节。
  • sfixed64: 总是8个字节。
  • bool:bool值
  • string: 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。
  • bytes: 可能包含任意顺序的字节数据。类似java的ByteString以及 c++ string;

enum包

# 定义enum
enum Direction {
	LEFT = 1;
	RIGHT = 2;
	UP = 3;
	DOWN = 4;
};

IDL文件举例

message Address
{
    required string city=1;
    optional string postcode=2;
    optional string street=3;
}
message UserInfo
{
    required string userid=1;
    required string name=2;
    repeated Address address=3;
}

关于ProtoBuf的详细解析,参考这篇文章:https://blog.csdn.net/carson_ho/article/details/70568606

ProtoBuf文档:https://www.grpc.io/docs/

gRPC vs Restful API

gRPC和Restful API都提供了一套通信机制,用于server/client模型通信,而且它们都使用http作为底层的传输协议(严格地说,gRPC使用的http2.0,而Restful API则不一定)。不过gRPC还是有些特有的优势,如下:

  • gRPC可以通过protobuf来定义接口,从而可以有更加严格的接口约束条件。
  • 通过protobuf可以将数据序列化为二进制编码,减少传输的数据量,大幅提高性能。
  • gRPC可以方便地支持流式通信(通过http2.0可以使用streaming模式)。

使用场景

  • 需要对接口进行严格约束的情况。
  • 对于性能有更高的要求时。

通常我们不会去单独使用gRPC,而是将gRPC作为一个部件进行使用,这是因为在生产环境,我们面对大并发的情况下,需要使用分布式系统来去处理,而gRPC并没有提供分布式系统相关的一些必要组件。而且,真正的线上服务还需要提供包括负载均衡,限流熔断,监控报警,服务注册和发现等等必要的组件。

安装gRPC

gRPC的中文文档参考:http://doc.oschina.net/grpc?t=56831

Go版本的gRPC开源项目地址:https://github.com/grpc/grpc-go

go get -u google.golang.org/grpc

安装protoc编译器

gRPC默认使用ProtoBuf协议,需要借助protoc来为不同语言平台编译目标源代码。

首先,安装protoc编译器,通过这个https://github.com/protocolbuffers/protobuf/releases地址下载,选择适合自己操作系统的版本。下载后要把二进制protoc放在自己的$PATH/bin目录中,确保可以在终端执行

其次,因为ProtoBuf起初不支持Go语言,所以我们还得安装一个生成Golang代码的工具。安装方式也非常简单,通过如下代码即可:

go get -u github.com/golang/protobuf/protoc-gen-go

注意:有可能说还是找不到protoc-gen-go命令,请到$GOPATH\bin\bin目录中看看,如果有就把它放入上一个目录。

假设我们有一个配置文件user.proto,那么我们在终端下cd到存放user.proto文件的目录,执行如下代码即可生成对应的Golang代码

protoc --go_out=. user.proto

--go_out=.表示输出Golang代码文件到当前目录下,生成的文件名是user.pb.go,规则就是filename.pb.go

使用gogoproto三方库

有一个第三方库,编解码性能远高于官方标准库,他就是:github.com/gogo/protobuf,但这个库已经不维护了,推荐使用官方库。

如果想体验这个三方库的性能,可能需要用下面的配套工具生成代码。

protoc --gogofaster_out=. user.proto

gRPC示例1

编写ProtoBuf描述文件,定义接口和数据类型;然后编译:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 文件:grpc_demo/pro/spider.proto
syntax = "proto3";  // 协议为proto3

option go_package = ".;pro";
// package spider;  // 包名

// 发送请求
message SendAddress {
    // 发送的参数字段
    // 参数类型 参数名 标识号(不可重复)
    string address = 1;  // 要请求的地址
    string method = 2;  // 请求方式
}

// 返回响应
message GetResponse {
    // 接收的参数字段
    // 参数类型 参数名 标识号
    int32 httpCode = 1;  // http状态码
    string response = 2;  // 返回体
}

// 定义服务,可定义多个服务,每个服务可多个接口
service GoSpider {
    // rpc请求 请求的函数 (发送请求参数) returns (返回响应的参数)
    rpc GetAddressResponse (SendAddress) returns (GetResponse);
}

定位到grpc_demo/pro目录,执行命令(一定要加plugins=grpc的参数,否则无法正确生成RPC相关的代码):

protoc --go_out=plugins=grpc:. spider.proto

注意这里有个参数配置是Go语言平台特有的,否则编译的时候可能报错:

option go_package = ".;pro";

# . 表示生成的go文件的存放地址,会自动生成目录的;这里指定当前目录
# pro 表示生成的go文件所属的包名;这里指定 pro

当前项目grpc_demo的目录结构如下:

├─client
│      client.go
├─pro
│      spider.pb.go
│      spider.proto
└─server
        server.go

server/server.go的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package server

import (
	"context"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
	"io/ioutil"
	"net"
	"net/http"
	spider "yufa/demo/grpc_demo/pro"
)

type server struct{}

const (
	// Address 监听地址
	Address string = "localhost:8080"
	// Method 通信方法
	Method string = "tcp"
)

// 接收client端的请求,函数名需保持一致
// ctx参数必传
// 参数二为自定义的参数,需从pb文件导入,因此pb文件必须可导入,文件放哪里随意
// 返回值同参数二,为pb文件的返回结构体指针
func (s *server) GetAddressResponse(ctx context.Context, a *spider.SendAddress) 
	(*spider.GetResponse, error) {
	// 逻辑写在这里
	switch a.Method {
	case "get", "Get", "GET":
		// 演示微服务用,故只写get示例
		status, body, err := get(a.Address)
		if err != nil {
			return nil, err
		}
		res := spider.GetResponse{
			HttpCode: int32(status),
			Response: body,
		}
		return &res, nil
	}
	return nil, nil
}

func get(address string) (s int, r string, err error) {
	// get请求
	resp, err := http.Get(address)
	if err != nil {
		return
	}
	defer resp.Body.Close()
	s = resp.StatusCode
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return
	}
	r = string(body)
	return
}

func GRPCSpiderServer() {
	// 监听本地端口
	listener, err := net.Listen(Method, Address)
	if err != nil {
		return
	}
	s := grpc.NewServer()                       // 创建GRPC
	spider.RegisterGoSpiderServer(s, &server{}) // 在GRPC服务端注册服务

	reflection.Register(s) // 在GRPC服务器注册服务器反射服务
	// Serve方法接收监听的端口,每到一个连接创建一个ServerTransport和server的grroutine
	// 这个goroutine读取GRPC请求,调用已注册的处理程序进行响应
	err = s.Serve(listener)
	if err != nil {
		return
	}
}

client/client.go的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package client

import (
	"context"
	"google.golang.org/grpc"
	spider "yufa/demo/grpc_demo/pro"
)

import "fmt"

const (
	// Address server端地址
	Address string = "localhost:8080"
)

func GRPCSpiderClient() {
	// 连接服务器
	conn, err := grpc.Dial(Address, grpc.WithInsecure())
	if err != nil {
		fmt.Println(err)
		return
	}
	defer conn.Close()

	// 连接GRPC
	c := spider.NewGoSpiderClient(conn)
	// 创建要发送的结构体
	req := spider.SendAddress{
		Address: "http://www.baidu.com",
		Method:  "get",
	}
	// 调用server的注册方法
	r, err := c.GetAddressResponse(context.Background(), &req)
	if err != nil {
		fmt.Println(err)
		return
	}
	// 打印返回值
	fmt.Println(r)
}

分别启动server和client,就会在client的控制台中看到百度的首页html代码了。

gRPC示例2

下面是一个gRPC的官方示例。

IDL文件pro/helloworld.proto

syntax = "proto3";

option go_package = ".;pro";
package pro;

message HelloRequest {
  string name = 1;
}

message HelloReplay {
  string message = 1;
}

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReplay) {}
}

下面的命令编译IDL文件,生成gRPC框架代码:

protoc --go_out=plugins=grpc:. helloworld.proto

服务器端:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import (
	"context"
	"google.golang.org/grpc"
	"log"
	"net"
	"yufa/demo/grpc_demo/pro"
)

type serverHello struct{}

func (s *serverHello) SayHello(ctx context.Context, in *pro.HelloRequest)
	(*pro.HelloReplay, error) {
	log.Printf("Received Name: %v", in.GetName())
	return &pro.HelloReplay{Message: "Hello " + in.Name}, nil
}

const (
	port = ":8081"
)

func GRPCServerHello() {
	lis, err := net.Listen("tcp", port)
	if err != nil {
		log.Fatalf("Failed to listen: %v", err)
	}
	s := grpc.NewServer()
	pro.RegisterGreeterServer(s, &serverHello{})
	if err := s.Serve(lis); err != nil {
		log.Fatalf("Failed to serve: %v", err)
	}
}

客户端代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import (
	"context"
	"google.golang.org/grpc"
	"log"
	"os"
	"time"
	"yufa/demo/grpc_demo/pro"
)

const (
	address     = ":8081"
	defaultName = "chende"
)

func GRPCClientHello() {
	conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
	if err != nil {
		log.Fatalf("Did not connect: %v", err)
	}
	defer conn.Close()
	c := pro.NewGreeterClient(conn)

	name := defaultName
	if len(os.Args) > 1 {
		name = os.Args[1]
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	r, err := c.SayHello(ctx, &pro.HelloRequest{Name: name})
	if err != nil {
		log.Fatalf("Could not great: %v", err)
	}
	log.Printf("Greeting: %s", r.GetMessage())
}

运行结果如下:

// server端
2021/01/25 22:53:11 Received Name: chende
// client端
2021/01/25 22:53:11 Greeting: Hello chende

如何启动gRPC压缩传输

大家用网络抓包工具查看上面的例子,会发现在HTTP2.0协议上走的是明文的内容,根本没有启动协议压缩功能。如何压缩传输呢?其实很简单,只需要2步就好了。

第一步:在服务端和客户端import中都要加入_ "google.golang.org/grpc/encoding/gzip",因为gzip.go文件中默认有对压缩算法的初始化设置。

1
2
3
4
5
6
7
func init() {
	c := &compressor{}
	c.poolCompressor.New = func() interface{} {
		return &writer{Writer: gzip.NewWriter(ioutil.Discard), pool: &c.poolCompressor}
	}
	encoding.RegisterCompressor(c)
}

第二步:在客户端发送请求的时候,指定(CallOption)压缩传输标记即可。

c.SayHello(ctx, &pro.HelloRequest{Name: name}, grpc.UseCompressor(gzip.Name))

就拿上面的示例2来说,我故意把发送的内容复制重复N遍,改造成发送原文5184个字节。

未压缩,抓包截图:

image-20210126005658023

启用gzip压缩,抓包截图:

image-20210126005510680

看到没有,5263字节直接变成了152字节。看到压缩的威力了吗?

参考阅读:

https://www.zhihu.com/question/30027669/answer/2872058473

https://blog.csdn.net/zhoupenghui168/article/details/130923516

(完)