gRPC入门

栏目: 服务器 · 发布时间: 5年前

内容简介:—— 时间飞逝 如一名携带信息的邮差 但那只不过是我们的比喻 人物是杜撰的 匆忙是假装的 携带的也不是人的讯息主要包括以下两点原因:很对人经常拿

—— 时间飞逝 如一名携带信息的邮差 但那只不过是我们的比喻 人物是杜撰的 匆忙是假装的 携带的也不是人的讯息

为什么使用 grpc

主要包括以下两点原因:

protocl buffer
http 2.0

很对人经常拿 thrift grpc 比较,现在先不发表任何看法,后续会深入 thrift 进行介绍。

http/2

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 的介绍,具体包括:

  1. 安装 Go 的开发环境,因为后续是基于 Go 语言的开发项目
  2. 安装 protocol-buffers
  3. 安装 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 赋予了它丰富的功能,比如 importpackage 、消息嵌套等等。 import 用于引入别的 .proto 文件; package 用于定义命名空间,转换到 go 源码中就是包名; repeated 用于定义重复的数据; enum 用于定义枚举类型等。

.proto 内字段的基本定义:

type name = tag;

Protocol buffer 本身不包含类型的描述信息,因此获取了没有 .proto 描述文件的二进制信息是毫无用处的,我们很难提取出非常有用的信息。 Go 语言 complier 生成的文件后缀是 .pb.go ,它自动生成了 setget 以及 readwrite 方法,我们可以很方便的序列化数据。

下面我们定义一个创建订单的 .proto 文件,概括的描述: buyerIDdevice 上支付 amount sku 商品。

  1. 声明版本为 proto3packageorder
  2. 设备类型定义为枚举类型,包括 ANDROIDIOS 两种,而且类型被嵌套声明在 OrderParams 内。
  3. sku 声明为 repeated ,因为用户可能购买多个商品。
  4. OrderResult 为响应的消息体结构,包括生成的订单号和处理的响应码。
  5. 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 clientserver 端实现的接口类型。生成的内容主要包括:

  1. protocol buffer 各种消息类型的序列化操作
  2. grpc client 实现的接口类型,以及 client 实现的 grpc 方法
  3. grpc server 待实现的接口类型

service 处理流程

第一步. 服务端为每个接收的连接创建单独的 goroutine 进行处理。

第二步. 自动生成的代码中,声明了服务的具体描述,也是该服务的“路由”。包括服务名称 ServiceNameMethodsStreams 。当 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 ,获取对应的 serviceMethodName 进行处理。

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/2gRPC 封装了很多东西,对于一般场合,我们只需要指定配置,实现接口就可以了,非常简单。

在入门的介绍里,大家会觉得 gRPC 不就跟 RESTFUL 请求一样吗?确实是,我也这样觉得。但存在一个最直观的优点:通过使用 gRPC ,可以将复杂的接口调用关系封装在 SDK 中,直接提供给第三方使用,而且还能有效避免错误调用接口的情况。

如果 gRPC 只能这样的话,它就太失败了,他用 HTTP/2 简直就是用来打蚊子的,让我们后续继续深入了解吧。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Verilog数字系统设计教程

Verilog数字系统设计教程

夏宇闻 / 北京航空航天大学出版社 / 2003-7-1 / 38.0

《Verilog数字系统设计教程》可作为电子工程类、自动控制类、计算机类的大学本科高年级及研究生教学用书,亦可供其他工程人员自学与参考。一起来看看 《Verilog数字系统设计教程》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具