gRPC是Google开源的一款RPC框架,跟具体语言无关,以protobuf作为IDL,通过protoc来编译框架代码。gRPC的Java实现的底层网络库是基于Netty开发而来,其Go实现是基于net库。
先说下Protobuf,它是一个纯粹的展示层协议,可以和各种传输层协议一起使用;Protobuf的文档也非常完善。 Protobuf具备了优秀的序列化协议所需的众多典型特征:
- 标准的IDL和IDL编译器,这使得其对工程师非常友好。
- 序列化数据非常简洁,紧凑,与XML相比,其序列化之后的数据量约为1/3到1/10。
- 解析速度非常快,比对应的XML快约20-100倍。
- 提供了非常友好的动态库,使用非常简介,反序列化只需要一行代码。
- 更好的兼容性,很好的支持向下或向上兼容。
数据类型
- 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
。
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个字节。
未压缩,抓包截图:
启用gzip压缩,抓包截图:
看到没有,5263字节直接变成了152字节。看到压缩的威力了吗?
参考阅读:
https://www.zhihu.com/question/30027669/answer/2872058473
https://blog.csdn.net/zhoupenghui168/article/details/130923516
(完)