gRPC入门

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

为何使用grpc

主要包括如下两点缘由:html

  1. protocl buffer一种高效的序列化结构。
  2. 支持http 2.0标准化协议。

很对人常常拿thriftgrpc比较,如今先不发表任何见解,后续会深刻thrift进行介绍。node

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.git

http/2带来了网络性能的巨大提高,下面列举一些我的以为比较重要的细节:github

  1. http/2对每一个源只需建立一个持久链接,在这一个链接内,能够并行的处理多个请求和响应,并且作到不相互影响。
  2. 容许客户端和服务端实现本身的数据流和链接流控制,这对咱们传输大数据很是有帮助。

更多细节,请参考文章末尾的连接,固然,后续也会专门介绍。golang

准备工做

你们能够参考protobuf的介绍,具体包括:web

  1. 安装Go的开发环境,由于后续是基于Go语言的开发项目
  2. 安装protocol-buffers
  3. 安装protoc-gen-go,用于自动生成源码

生成源码的命令以下,其中,--go_out用于指定生成源码的保存路径;而-I-IPATH的简写,用于指定查找import文件的路径,能够指定多个;最后的order是编译的grpc文件的存储路径。apache

protoc -I proto/ proto/order.proto --go_out=plugins=grpc:order

protocol buffer

google开发的高效、跨平台的数据传输格式。固然,本质仍是数据传输结构。但google赋予了它丰富的功能,好比importpackage、消息嵌套等等。import用于引入别的.proto文件;package用于定义命名空间,转换到go源码中就是包名;repeated用于定义重复的数据;enum用于定义枚举类型等。bash

.proto内字段的基本定义:服务器

type name = tag;

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

下面咱们定义一个建立订单的.proto文件,归纳的描述:buyerIDdevice上支付amountsku商品。

  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简直就是用来打蚊子的,让咱们后续继续深刻了解吧。


参考文章:

  1. gRPC:Google 开源的基于 HTTP/2 和 ProtoBuf 的通用 RPC 框架
  2. GRPC
  3. HTTP/2 简介
  4. http2
相关文章
相关标签/搜索