gRPC & Protocol Buffer 构建高性能接口实践

介绍如何使用 gRPC 和 ProtoBuf,快速了解 gRPC 可以参考这篇文章第一段:gRPC quick Start

接口开发是软件开发占据举足轻重的地位,是现代软件开发之基石。体现在无论是前后端分离的 Web 前端还是移动客户端,乃至基于不同系统、编程语言构建的软件系统之间,API 都是充当桥梁的作用把不同端的系统链接在一起从而形成了一套稳固的商用系统。

基于 Web 的接口通常都是 RESTful API 结合 JSON 在前后端之间传递信息,这种模式比较适合于前后端分离及移动客户端于后端通信;但对于承载大规模并发、高性能要求的微服务架构,基于 JSON 传输的 RESTful 是否还适用于高并发、伸缩性强及业务逻辑复杂的软件架构吗?基于 RESTful 架构是否能够简单是想双向流 (bidrectional stream) 的接口。gRPC 和 protocol buffer 就是解决上述问题。

关于 gRPC 和 Protobuf 的简介可以看看这篇文章:Google Protocol Buffer 和 gRPC 简介

gRPC & Protocol Buffer 实践

我本地的 GOPATH 目录为 /Users/hww/work/go ,给我们的 demo 项目新建一个目录 cd $GOPATH/src && mkdir rpc-protobuf

定义 Protocol Buffer 的消息类型和服务

在项目根目录 rpc-protobuf 新建文件目录 customer。首先给 Protocol Bufffer 文件定义服务接口和 paylaod 信息的数据结构,$GOPATH/scr/rpc-protobuf/customer/customer.proto:

syntax = "proto3";
package customer;

// The Customer sercie definition
service Customer {
    // Get all Customers with filter - A server-to-client streaming RPC.
    rpc GetCustomers(CustomerFilter) returns (stream CustomerRequest) {}

    // Create a new Customer - A simple RPC
    rpc CreateCustomer (CustomerRequest) returns (CustomerResponse) {}
}

message CustomerRequest {
    int32 id = 1;   // Unique ID number for a Customer.
    string name = 2;
    string email = 3;
    string phone = 4;

    message Address {
        string street = 1;
        string city = 2;
        string state = 3;
        string zip = 4;
        bool isShippingAddress = 5;
    }

    repeated Address addresses = 5;
}

message CustomerResponse {
    int32 id = 1;
    bool success = 2;
}

message CustomerFilter {
    string keyword = 1;
}

.proto 文件,第一行代码为版本号,在这里我们使用了 proto3 ;第二行代码为包名,通过该文件生成的 Go 源码包名和这里的一致为 customer

我们定义了消息类型和服务接口。标准数据类型有 int32, float, double, 或 string 这些常见的类型。一种消息类型就是多个字段的集合,每个字段都被一个在该消息中唯一的整数标记;Customer 服务中有两个 RPC 方法:

service Customer {
// Get all Customers with filter - A server-to-client streaming RPC.
rpc GetCustomers(CustomerFilter) returns (stream CustomerRequest) {}

// Create a new Customer - A simple RPC
rpc CreateCustomer (CustomerRequest) returns (CustomerResponse) {}
}

解释 Customer 服务之前,我们首先来大概了解一下 gRPC 中的三种类型的 RPC 方法。

  • simple RPC应用于常见的典型的 Request/Response 模型。客户端通过 stub 请求 RPC 的服务端并等待服务端的响应。
  • Server-side streaming RPC客户端给服务端发送一个请求并获取服务端返回的流,用以读取一连串的服务端响应。stream 关键字在响应类型的前面。

    // 例子
    rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){}
    
  • Client-side streaming RPC客户端发送的请求 payload 有一连串的的信息,通过流给服务端发送请求。stream 关键字在请求类型前面。

    // 例子
    rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {}
    
  • Bidirectional streaming RPC服务端和客户端之间都使用 read-write stream 进行通信。stream 关键字在请求类型和响应类型前面。

    // 例子
    rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){}
    

理解 gRPC 提供的四种类型 RPC 方法之后,回到 Customer 的例子中。在 Customer 服务提供了两种类型的 RPC 方法,分别是 simple RPC(CreateCustomer) 和 server-side streaming(GetCustomers) 。CreateCustomer 遵循标准 Request/Response 规范新建一个用户;GetCustomers 方法中,服务端通过 stream 返回多个消费者信息的列表。

基于 proto 文件生成服务端和客户端的 Go 代码

定义好 proto 文件之后,然后生成你需要的编程语言源代码,这些源代码是服务端和客户端业务逻辑代码的接口。客户端代码通过消息类型和服务接口调用 RPC 方法。protocol buffer 编译器通过 gRPC 的 Go 插件生成客户端和服务端的代码。在项目根目录下运行命令:

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

在 customer 目录下生成了 customer.pb.go 文件。该源码包含三大类功能:

  • 读写和序列化请求和响应消息类型
  • 提供定义在 proto 文件中定义的客户端调用方法接口
  • 提供定义在 proto 文件中定义的服务端实现方法接口

新建 gRPC 服务

以下代码片段新建依据 proto 文件中定义的服务新建 gRPC 服务端。

// server/main.go
package main

import (
    "log"
    "net"
    "strings"

    "golang.org/x/net/context"
    "google.golang.org/grpc"

    pb "rpc-protobuf/customer"
)

const (
    port = ":50051"
)

// server is used to implement customer.CustomerServer.
type server struct {
    savedCustomers []*pb.CustomerRequest
}

// CreateCustomer creates a new Customer
func (s *server) CreateCustomer(ctx context.Context, in *pb.CustomerRequest) (*pb.CustomerResponse, error) {
    s.savedCustomers = append(s.savedCustomers, in)
    return &pb.CustomerResponse{Id: in.Id, Success: true}, nil
}

// GetCustomers returns all customers by given filter
func (s *server) GetCustomers(filter *pb.CustomerFilter, stream pb.Customer_GetCustomersServer) error {
    for _, customer := range s.savedCustomers {
        if filter.Keyword != "" {
            if !strings.Contains(customer.Name, filter.Keyword) {
                continue
            }
        }
        if err := stream.Send(customer); err != nil {
            return err
        }
    }
    return nil
}

func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatal("failed to listen: %v", err)
    }

    //Create a new grpc server
    s := grpc.NewServer()
    pb.RegisterCustomerServer(s, &server{})
    s.Serve(lis)
}

服务端源码中,server 结构体定义在 customer.pb.go 中的 CustomerServer 接口;CreateCustomerGetCustomers 两个方法定义在 customer.pb.go 文件的 CustomerClient 接口中。

CreateCustomer 是一个 simple rpc 类型的 RPC 方法,在这里它接受两个参数,分别是 context objct 和客户端的请求信息,返回值为 proto 文件定义好的 CustomerResponse 对象;GetCustomers 是一个 server-side streaming 类型的 RPC 方法,接受两个参数:CustomerRequest 对象、以及用来作为服务端对客户端响应 stream 的 对象 Customer_GetCustomersServer 。看看 customer.pb.go 中对 CustomerServer 接口的定义:

// Server API for Customer service
type CustomerServer interface {
    // Get all Customers with filter - A server-to-client streaming RPC.
    GetCustomers(*CustomerFilter, Customer_GetCustomersServer) error
    // Create a new Customer - A simple RPC
    CreateCustomer(context.Context, *CustomerRequest) (*CustomerResponse, error)
}

对比理解服务端代码对两个方法的实现,我们就可以理解参数的传递原理。

服务端代码中 GetCustomers 方法内部有一行代码 stream.Send(customer) 这个 Send 方法是 customer.pb.go 给 Customer_GetCustomersServer 接口定义并好的方法,表示给客户端返回 stream

最后看看服务端代码中的 main 方法。首先 grpc.NewServer 函数新建一个 gRPC 服务端;然后调用 customer.pb.go 中的 RegisterCustomerServer(s *grpc.Server, srv CustomerServer) 函数注册该服务:pb.RegisterCustomerServer(s, &server{}) ;最后通过 gRPC 的 Golang API Server.Serve 监听指定的端口号:s.Serve(lis),新建一个 ServerTransportservice goroutine处理监听的端口收到的请求。

新建 gRPC 客户端

首先看 customer.pb.go 生成的客户端调用方法接口部分的代码:

// Client API for Customer service
type CustomerClient interface {
    // Get all Customers with filter - A server-to-client streaming RPC.
    GetCustomers(ctx context.Context, in *CustomerFilter, opts ...grpc.CallOption) (Customer_GetCustomersClient, error)
    // Create a new Customer - A simple RPC
    CreateCustomer(ctx context.Context, in *CustomerRequest, opts ...grpc.CallOption) (*CustomerResponse, error)
}
type customerClient struct {
    cc *grpc.ClientConn
}
func NewCustomerClient(cc *grpc.ClientConn) CustomerClient {
    return &customerClient{cc}
}

*grpc.ClientConn 表示连接到 RPC 服务端的客户端,NewCustomerClient 函数返回一个 customerClient 结构体对象。CustomerClient 接口定义了两个能够被客户端服务调用的方法,另外我们可以在 customer.pb.go 看到给 customerClient 类型的结构体实现这两个函数的方法,故客户端对象能够调用 GetCustomersCreateCustomer 方法:

func (c *customerClient) GetCustomers(ctx context.Context, in *CustomerFilter, opts ...grpc.CallOption) (Customer_GetCustomersClient, error) {
...
}

...

func (c *customerClient) CreateCustomer(ctx context.Context, in *CustomerRequest, opts ...grpc.CallOption) (*CustomerResponse, error) {
...
}

接着回到实现客户端的源码:

// client/main.go
package main

import (
    "io"
    "log"

    "golang.org/x/net/context"
    "google.golang.org/grpc"

    pb "rpc-protobuf/customer"
)

const (
    address = "localhost:50051"
)

// createCustomer calls the RPC method CreateCustomer of CustomerServer
func createCustomer(client pb.CustomerClient, customer *pb.CustomerRequest) {
    resp, err := client.CreateCustomer(context.Background(), customer)
    if err != nil {
        log.Fatalf("Could not create Customer: %v", err)
    }
    if resp.Success {
        log.Printf("A new Customer has been added with id: %d", resp.Id)
    }
}

// GetCustomers calls the RPC method GetCustomers of CustomerServer
func getCustomers(client pb.CustomerClient, filter *pb.CustomerFilter) {
    // calling the streaming API
    stream, err := client.GetCustomers(context.Background(), filter)
    if err != nil {
        log.Fatal("Error on get customers: %v", err)
    }
    for {
        customer, err := stream.Recv()
        if err == io.EOF {
            break
        }

        if err != nil {
            log.Fatal("%v.GetCustomers(_) = _, %v", client, err)
        }
        log.Printf("Customer: %v", customer)
    }
}

func main() {
    // Set up a connection to the RPC server
    conn, err := grpc.Dial(address, grpc.WithInsecure())
    if err != nil {
        log.Fatal("did not connect: %v", err)
    }
    defer conn.Close()
    // creates a new CustomerClient
    client := pb.NewCustomerClient(conn)

    customer := &pb.CustomerRequest{
        Id:    101,
        Name:  "Shiju Varghese",
        Email: "[email protected]",
        Phone: "732-757-2923",
        Addresses: []*pb.CustomerRequest_Address{
            &pb.CustomerRequest_Address{
                Street:            "1 Mission Street",
                City:              "San Francisco",
                State:             "CA",
                Zip:               "94105",
                IsShippingAddress: false,
            },
            &pb.CustomerRequest_Address{
                Street:            "Greenfield",
                City:              "Kochi",
                State:             "KL",
                Zip:               "68356",
                IsShippingAddress: true,
            },
        },
    }

    // Create a new customer
    createCustomer(client, customer)

    customer = &pb.CustomerRequest{
        Id:    102,
        Name:  "Irene Rose",
        Email: "[email protected]",
        Phone: "732-757-2924",
        Addresses: []*pb.CustomerRequest_Address{
            &pb.CustomerRequest_Address{
                Street:            "1 Mission Street",
                City:              "San Francisco",
                State:             "CA",
                Zip:               "94105",
                IsShippingAddress: true,
            },
        },
    }

    // Create a new customer
    createCustomer(client, customer)
    //Filter with an empty Keyword
    filter := &pb.CustomerFilter{Keyword: ""}
    getCustomers(client, filter)

}

客户端需要建立 gRPC 通道(channel) 才可与服务端建立通信,调用 RPC 方法。grpc.Dial 函数表示新建与 RPC 服务端的连接。Dial函数在 gRPC golang 实现的库中声明代码如下:

func Dial(target string, opts ...DialOption) (*ClientConn, error)

除了连接地址作为第一个参数外,还可以传多个可选参数。这些可选参数表示鉴权校验,例如 TLS 或者 JWT 。在这里的 grpc.WithInsecure 表示客户端连接的安全传输被禁用。

调用服务端的 RPC 方法前,首先需要新建客户端 stub :

// creates a new CustomerClient
client := pb.NewCustomerClient(conn)

在例子中,通过调用 RPC CreateCustomer 方法新增了两个 customer 数据 : createCustomer(client, customer) ;调用 RPC GetCustomers 方法获取所有 customers 数据。

至此,我们已经简单地实现了一套 gRPC 客户端和服务端代码。在项目根目录下运行命令:

➜  rpc-protobuf (nohup go run server/main.go &) && go run client/main.go
appending output to nohup.out
2017/10/28 18:08:02 A new Customer has been added with id: 101
2017/10/28 18:08:02 A new Customer has been added with id: 102
2017/10/28 18:08:02 Customer: id:101 name:"Shiju Varghese" email:"[email protected]" phone:"732-757-2923" addresses:<street:"1 Mission Street" city:"San Francisco" state:"CA" zip:"94105" > addresses:<street:"Greenfield" city:"Kochi" state:"KL" zip:"68356" isShippingAddress:true >
2017/10/28 18:08:02 Customer: id:102 name:"Irene Rose" email:"[email protected]" phone:"732-757-2924" addresses:<street:"1 Mission Street" city:"San Francisco" state:"CA" zip:"94105" isShippingAddress:true >

参考来源

Building High Performance APIs In Go Using gRPC And Protocol Buffers

推荐阅读

0 条评论
您想说点什么吗?