gRPC
————
————
rpc是远程过程调用的方之一,http调用是应用层协议,rpc的网络协议相对灵活且可以定制,并且提供更加贴近本地方法调用的远程方法调用形式,所以一般来说,**微服务之间往往使用rpc进行远程过程调用,**典型的rpc框架有dubbo、thrift、grpc。grpc是google的一个高性能、开源、通用的rpc框架,基于http/2协议标准,默认采用protocol buffers数据序列化协议,支持多种语言(python,ruby,go,java等)。在 gRPC 里客户端应用可以像调用本地方法一样直接调用另一台机器上服务端应用的方法,这样我们就很容易创建分布式应用和服务
![img](https://upload-images.jianshu.io/upload_images/3959253-76284b64125a8673.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)
-
首先定义一个服务,定义能够被远程调用的方法(包含参数和返回类型);
-
在服务端实现这个方法,并运行一个 gRPC 服务器来处理客户端调用
-
在客户端拥有一个存根,这个存根就是长得像服务端一样的方法(但是没有具体实现),客户端通过这个存根调用服务端的方法。
具体一点:
定义和编译proto文件
客户端发送RPC请求
服务端建立RPC
特性:
- 使用protobuf协议来定义服务、接口和数据类型,protobuf是一种数据序列化协议;
- 支持多种语言(C/C++/Nodejs/Python/Ruby/Objective-C/php/go/java)
- 基于HTTP/2标准设计,相比其他RPC框架,grpc拥有更多功能,如双向流、头部压缩、多复用请求
等
gRPC vs. Restful API
gRPC和restful API都提供了一套通信机制,用于server/client模型通信,而且它们都使用http作为底层的传输协议(严格地说, gRPC使用的http2.0,而restful api则不一定)。不过gRPC还是有些特有的优势,如下:
-
gRPC可以通过protobuf来定义接口,从而可以有更加严格的接口约束条件。
-
通过protobuf可以将数据序列化为二进制编码,这会大幅减少需要传输的数据量,从而大幅提高性能。
-
gRPC可以方便地支持流式通信
使用场景
-
需要对接口进行严格约束的情况,比如我们提供了一个公共的服务,很多人,甚至公司外部的人也可以访问这个服务,这时对于接口我们希望有更加严格的约束,我们不希望客户端给我们传递任意的数据,尤其是考虑到安全性的因素,我们通常需要对接口进行更加严格的约束。这时gRPC就可以通过protobuf来提供严格的接口约束。
-
对于性能有更高的要求时。有时我们的服务需要传递大量的数据,而又希望不影响我们的性能,这个时候也可以考虑gRPC服务,因为通过protobuf我们可以将数据压缩编码转化为二进制格式,通常传递的数据量要小得多,而且通过http2我们可以实现异步的请求,从而大大提高了通信效率。
protobuf协议
protocol buffers (ProtoBuf)是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。Protocol Buffers 是一种灵活,高效,自动化机制的结构数据序列化方法-可类比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。json\xml都是基于文本格式,protobuf是二进制格式
// 指定protobuf的版本,proto3是最新的语法版本
syntax = "proto3";
// 定义数据结构,message 你可以想象成java的class,c语言中的struct
message Response {
string data = 1; // 定义一个string类型的字段,字段名字为data, 序号为1
int32 status = 2; // 定义一个int32类型的字段,字段名字为status, 序号为2
}
enum PhoneType //枚举消息类型,使用enum关键词定义,一个电话类型的枚举类型
{
MOBILE = 0; //proto3版本中,首成员必须为0,成员不应有相同的值
HOME = 1;
WORK = 2;
}
// 定义一个电话消息
message PhoneNumber
{
string number = 1; // 电话号码字段
PhoneType type = 2; // 电话类型字段,电话类型使用PhoneType枚举类型
}
// 整数数组
message Msg {
// 只要使用repeated标记类型定义,就表示数组类型。
repeated int32 arrays = 1;
}
//字符串数组
message Msg {
repeated string names = 1;
}
//1.引用其他消息类型的用法
// 定义Result消息
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3; // 字符串数组类型
}
// 定义SearchResponse消息
message SearchResponse {
// 引用上面定义的Result消息类型,作为results字段的类型
repeated Result results = 1; // repeated关键词标记,说明results字段是一个数组
}
//2.消息嵌套
message SearchResponse {
// 嵌套消息定义
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
// 引用嵌套的消息定义
repeated Result results = 1;
}
//3.import导入其他proto文件定义的消息
// 导入Result消息定义
import "result.proto";
// 定义SearchResponse消息
message SearchResponse {
// 使用导入的Result消息
repeated Result results = 1;
}
//其中result.proto文件
syntax = "proto3";
// Result消息定义
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3; // 字符串数组类
Map 字段不能使用 repeated 关键字修饰。
key_type 可以是任何整数或字符串类型(除浮点类型和字节之外的任何标量类型)
value_type 可以是除另一个映射之外的任何类型
message Product
{
string name = 1; // 商品名
// 定义一个k/v类型,key是string类型,value也是string类型
map<string, string> attrs = 2; // 商品属性,键值对
}
message 类型的使用: message 在定义过程中是可以声明自己定义的 message 类型,示例:
syntax = "proto3";
message Employee {
int64 id = 1;
string name = 2;
Skill skills = 3; //这里声明的为自定义的 Skill 类型
}
message Skill {
string name = 1;
}
map 类型的使用:message 定义时可以使用 map 类型,示例:
syntax = "proto3";
message Employee {
int64 id = 1;
string name = 2;
map<string, Skill> skills = 3;
}
message Skill {
string name = 1;
}
gRPC缺乏服务治理的功能,开发者可以通过go-kit结合gRPC来实现我们的完成需求。go-kit抽象的endpoint设计让开发者很容易包装其他微服务框架使用的协议。gRPC集成到go-kit的transport层,transport层用于接收用户网络请求并将其转为endpoint可以处理的对象,然后交给endpoint层执行,最后将处理结果转为响应对象返回给客户
gRPC HelloWorld实例详解
gRPC的使用通常包括如下几个步骤:
- 通过protobuf来定义接口和数据类型
- 编写gRPC server端代码
- 编写gRPC client端代码
通过protobuf来定义接口和数据类型
message
是代表数据结构(里面可以包括不同类型的成员变量,包括字符串、数字、数组、字典……),service
代表 RPC 接口。变量后面的数字是代表进行二进制编码时候的提示信息,1~15 表示热变量,会用较少的字节来编码。另外,支持导入。
默认所有变量都是可选的(optional),repeated 则表示数组。主要 service rpc 接口只能接受单个 message 参数,返回单个 message;
syntax = "proto3";
package hello;
message HelloRequest {
string greeting = 1;
}
message HelloResponse {
string reply = 1;
repeated int32 number=4;
}
service HelloService {
rpc SayHello(HelloRequest) returns (HelloResponse){}
}
编译最关键参数是指定输出语言格式,例如,python 为 --python_out=OUT_DIR
。
一些还没有官方支持的语言,可以通过安装 protoc 对应的 plugin 来支持。例如,对于 Go 语言,可以安装
$ go get -u github.com/golang/protobuf/{protoc-gen-go,proto} // 前者是 plugin;后者是 go 的依赖库
之后,正常使用 protoc --go_out=plugins=grpc:./ ./hello.proto
来生成 hello.pb.go,会自动调用 protoc-gen-go
插件。
ProtoBuf 提供了 Marshal/Unmarshal
方法来将数据结构进行序列化操作。所生成的二进制文件在存储效率上比 XML 高 3~10 倍,并且处理性能高 1~2 个数量级。
server端
const (
port = ":50051"
)
type server struct {}
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}
func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatal("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
s.Serve(lis)
}
client端
const (
address = "localhost:50051"
defaultName = "world"
)
func main() {
conn, err := grpc.Dial(address, grpc.WithInsecure())
if err != nil {
log.Fatal("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)
name := defaultName
if len(os.Args) >1 {
name = os.Args[1]
}
r, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: name})
if err != nil {
log.Fatal("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.Message)
}
gRPC 与微服务
protoc --go_out=plugins=grpc:./ ./hello.proto
protoc --go_out=plugins=grpc:./ ./hello.proto