这篇文章是给别人写的,不是我要写的。我也来卖个萌,嘎嘎!!—— 时间飞逝 如一名携带信息的邮差 但那只不过是我们的比喻 人物是杜撰的 匆忙是假装的 携带的也不是人的讯息
为什么使用 grpc
主要包括以下两点原因:
protocl buffer 一种高效的序列化结构。
支持 http 2.0 标准化协议。
很对人经常拿 thrift 跟 grpc 比较,现在先不发表任何看法,后续会深入 thrift 进行介绍。
http/2
HTTP/2 enables a more efficient use of network resources and a reduced perception of latency by introducing header field compression and allowing multiple concurrent exchanges on the same connection… Specifically, it allows interleaving of request and response messages on the same connection and uses an efficient coding for HTTP header fields. It also allows prioritization of requests, letting more important requests complete more quickly, further improving performance.The resulting protocol is more friendly to the network, because fewer TCP connections can be used in comparison to HTTP/1.x. This means less competition with other flows, and longer-lived connections, which in turn leads to better utilization of available network capacity. Finally, HTTP/2 also enables more efficient processing of messages through use of binary message framing.
http/ 2 带来了网络性能的巨大提升,下面列举一些个人觉得比较重要的细节:
http/ 2 对每个源只需创建一个持久连接,在这一个连接内,可以并行的处理多个请求和响应,而且做到不相互影响。
允许客户端和服务端实现自己的数据流和连接流控制,这对我们传输大数据非常有帮助。
更多细节,请参考文章末尾的链接,当然,后续也会专门介绍。
准备工作
大家可以参考 protobuf 的介绍,具体包括:
安装 Go 的开发环境,因为后续是基于 Go 语言的开发项目
安装 protocol-buffers
安装 protoc-gen-go,用于自动生成源码
生成源码的命令如下,其中,–go_out 用于指定生成源码的保存路径;而 - I 是 -IPATH 的简写,用于指定查找 import 文件的路径,可以指定多个;最后的 order 是编译的 grpc 文件的存储路径。
protoc -I proto/ proto/order.proto –go_out=plugins=grpc:order
protocol buffer
google 开发的高效、跨平台的数据传输格式。当然,本质还是数据传输结构。但 google 赋予了它丰富的功能,比如 import、package、消息嵌套等等。import 用于引入别的.proto 文件;package 用于定义命名空间,转换到 go 源码中就是包名;repeated 用于定义重复的数据;enum 用于定义枚举类型等。
.proto 内字段的基本定义:
type name = tag;
Protocol buffer 本身不包含类型的描述信息,因此获取了没有.proto 描述文件的二进制信息是毫无用处的,我们很难提取出非常有用的信息。Go 语言 complier 生成的文件后缀是.pb.go,它自动生成了 set、get 以及 read、write 方法,我们可以很方便的序列化数据。
下面我们定义一个创建订单的.proto 文件,概括的描述:buyerID 在 device 上支付 amount 买 sku 商品。
声明版本为 proto3,package 是 order。
设备类型定义为枚举类型,包括 ANDROID 和 IOS 两种,而且类型被嵌套声明在 OrderParams 内。
sku 声明为 repeated,因为用户可能购买多个商品。
OrderResult 为响应的消息体结构,包括生成的订单号和处理的响应码。
service 声明了 order 要提供的服务。当前仅仅实现一个 simple RPC:客户端使用 OrderParams 参数请求 RPC 服务器,收到 OrderResult 作为响应。
syntax = “proto3”;
package order;
service Order {
//a simple RPC
//create new order
rpc Add (OrderParams) returns (OrderResult) {
}
}
message OrderParams {
string amount = 1; // 订单金额
int64 buyerID = 2; // 购买用户 ID
enum Device {
IOS = 0;
ANDROID = 1;
}
Device device = 3;
repeated Sku sku = 4;
}
message Sku {
int32 num = 1;
string skuId = 2;
int32 unitPrice = 3;
}
message OrderResult {
int32 statusCode = 1;
string orderID = 2;
}
grpc 接口
通过定义的.proto 文件生成 grpc client 和 server 端实现的接口类型。生成的内容主要包括:
protocol buffer 各种消息类型的序列化操作
grpc client 实现的接口类型,以及 client 实现的 grpc 方法
grpc server 待实现的接口类型
service 处理流程
第一步. 服务端为每个接收的连接创建单独的 goroutine 进行处理。
第二步. 自动生成的代码中,声明了服务的具体描述,也是该服务的“路由”。包括服务名称 ServiceName 以 Methods、Streams。当 rpc 接收到新的数据时,会根据路由执行对应的方法。因为我们的设定没有处理流的场景,所以 Streams 为空的结构体。
代码中的服务名称被指定为:order.Order,对应创建订单的方法是:Add。
var _Order_serviceDesc = grpc.ServiceDesc{
ServiceName: “order.Order”,
HandlerType: (*OrderServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: “Add”,
Handler: _Order_Add_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: “order.proto”,
}
第三步. 将路由注册到 rpc 服务中。如下所示,就是将上述的路由转换为 map 对应关系的过程。类比 restful 风格的接口定义,等价于 /order/ 这种请求都由这个 service 来进行处理。
最终将 service 注册到 gRPC server 上。同时,我们可以逆向猜出服务的处理过程:通过请求的路径获取 service,然后通过 MethodName 调用相应的处理方法。
srv := &service{
server: ss,
md: make(map[string]*MethodDesc),
sd: make(map[string]*StreamDesc),
mdata: sd.Metadata,
}
for i := range sd.Methods {
d := &sd.Methods[i]
srv.md[d.MethodName] = d
}
for i := range sd.Streams {
d := &sd.Streams[i]
srv.sd[d.StreamName] = d
}
s.m[sd.ServiceName] = srv
第四步. gRPC 服务处理请求。通过请求的:path,获取对应的 service 和 MethodName 进行处理。
service := sm[:pos]
method := sm[pos+1:]
if srv, ok := s.m[service]; ok {
if md, ok := srv.md[method]; ok {
s.processUnaryRPC(t, stream, srv, md, trInfo)
return
}
if sd, ok := srv.sd[method]; ok {
s.processStreamingRPC(t, stream, srv, sd, trInfo)
return
}
}
通过结合 protoc 自动生成的 client 端代码,无需抓包,我们就可以推断出 path 的格式,以及系统是如何处理路由的。代码中定义的:/order.Order/Add 就是依据。
func (c *orderClient) Add(ctx context.Context, in *OrderParams, opts …grpc.CallOption) (*OrderResult, error) {
out := new(OrderResult)
err := c.cc.Invoke(ctx, “/order.Order/Add”, in, out, opts…)
if err != nil {
return nil, err
}
return out, nil
}
创建订单
为了简单起见,我们只保证订单的唯一性。这里我们实现一个简易版本,而且也不做过多介绍。感兴趣的同学可以移步到另一篇文章:探讨分布式 ID 生成系统去了解,毕竟不应该是本节的重心。
// 上次创建订单使用的毫秒时间
var lastTimestamp = time.Now().UnixNano() / 1000000
var sequence int64
const MaxSequence = 4096
// 42bit 分配给毫秒时间戳
// 12bit 分配给序列号,每 4096 就重新开始循环
// 10bit 分配给机器 ID
func CreateOrder(nodeId int64) string {
currentTimestamp := getCurrentTimestamp()
if currentTimestamp == lastTimestamp {
sequence = (sequence + 1) % MaxSequence
if sequence == 0 {
currentTimestamp = waitNextMillis(currentTimestamp)
}
} else {
sequence = 0
}
orderId := currentTimestamp << 22
orderId |= nodeId << 10
orderId |= sequence
return strings.ToUpper(fmt.Sprintf(“%x”, orderId))
}
func getCurrentTimestamp() int64 {
return time.Now().UnixNano() / 1000000
}
func waitNextMillis(currentTimestamp int64) int64 {
for currentTimestamp == lastTimestamp {
currentTimestamp = getCurrentTimestamp()
}
return currentTimestamp
}
运行系统
创建服务端代码。注意:使用 grpc 提供的默认选项,其实是很危险的行为。在生产开发中,被不熟悉的默认选项坑到的情况比比皆是。这里的代码不要作为后续生产环境开发的参考。服务端的代码相比客户端要复杂一点,需要我们去实现处理请求的接口。
type Order struct {
}
func (o *Order) Add(ctx context.Context, in *order.OrderParams) (*order.OrderResult, error) {
return &order.OrderResult{
OrderID: util.CreateOrder(1),
}, nil
}
func main() {
lis, err := net.Listen(“tcp”, “127.0.0.1:10000”)
if err != nil {
log.Fatalf(“Failed to listen: %v”, err)
}
grpcServer := grpc.NewServer()
order.RegisterOrderServer(grpcServer, &Order{})
grpcServer.Serve(lis)
}
客户端的代码非常简单,构造参数,处理返回就 Ok 了。
func createOrder(client order.OrderClient, params *order.OrderParams) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
orderResult, err := client.Add(ctx, params)
if err != nil {
log.Fatalf(“%v.GetFeatures(_) = _, %v: “, client, err)
}
log.Println(orderResult)
}
func main() {
conn, err := grpc.Dial(“127.0.0.1:10000”)
if err != nil {
log.Fatalf(“fail to dial: %v”, err)
}
defer conn.Close()
client := order.NewOrderClient(conn)
orderParams := &order.OrderParams{
BuyerID: 10318003,
}
createOrder(client, orderParams)
}
总结
文章介绍了 gRPC 的入门知识,包括 protocol buffer 以及 http/2,gRPC 封装了很多东西,对于一般场合,我们只需要指定配置,实现接口就可以了,非常简单。
在入门的介绍里,大家会觉得 gRPC 不就跟 RESTFUL 请求一样吗?确实是,我也这样觉得。但存在一个最直观的优点:通过使用 gRPC,可以将复杂的接口调用关系封装在 SDK 中,直接提供给第三方使用,而且还能有效避免错误调用接口的情况。
如果 gRPC 只能这样的话,它就太失败了,他用 HTTP/ 2 简直就是用来打蚊子的,让我们后续继续深入了解吧。
参考文章:
gRPC:Google 开源的基于 HTTP/2 和 ProtoBuf 的通用 RPC 框架
GRPC
HTTP/2 简介
http2