gRPC入门
Posted by 付辉 on Saturday, January 26, 2019 共2916字
时间飞逝 如一名携带信息的邮差 但那只不过是我们的比喻 人物是杜撰的 匆忙是假装的 携带的也不是人的讯息
为什么使用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
简直就是用来打蚊子的,让我们后续继续深入了解吧。
参考文章: