gRPC Introduction

gRPC Overview

gRPC is a language-neutral RPC framework developed and open-sourced by Google, currently supporting C, Java, and Go languages. The C version supports languages such as C, C++, Node.js, C#. In gRPC, client applications can directly call methods on a server on a different machine as if they were calling local methods.

The simple steps to use gRPC are as follows, taking Go language as an example:

  1. Write the proto file and use protoc to generate the .pb.go file.
  2. On the server side, define a Server, create a Function to implement the interface -> net.Listen -> grpc.NewServer() -> pb.RegisterXXXServer(server, &Server{}) -> server.Serve(listener).
  3. On the client side, grpc.Dial to create a gRPC connection -> pb.NewXXXClient(conn) to create a client -> context.WithTimeout to set a timeout -> call the interface with client.Function -> If it is stream transmission, loop to read data.

gRPC Concepts

Stream

Unary RPC

The client sends a request to the server and gets a response from the server, just like a normal function call.

Server streaming RPC

The client sends a request to the server and can get a data stream to read a series of messages. The client reads from the returned data stream until there are no more messages.
The server needs to send messages into the stream, for example:

1
2
3
4
5
6
7
8
for n := 0; n < 5; n++ {
err := server.Send(&pb.StreamResponse{
StreamValue: req.Data + strconv.Itoa(n),
})
if err != nil {
return err
}
}

The client gets a stream transmission object ‘stream’ through the grpc call and needs to loop to receive data, for example:

1
2
3
4
5
6
7
8
9
10
11
12
for {
res, err := stream.Recv()
// Determine whether the message stream has ended
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("ListStr get stream err: %v", err)
}
// Print the return value
log.Println(res.StreamValue)
}

Client streaming RPC

The client writes and sends a series of messages to the server using a provided data stream. Once the client finishes writing messages, it waits for the server to read these messages and return a response.

The server uses stream.Recv() to loop to receive the data stream, and SendAndClose indicates that the server has finished receiving messages and sends a correct response to the client, for example:

1
2
3
4
5
6
7
8
9
10
11
for  {
res,err := stream.Recv()
// Message reception ends, send the result, and close
if err == io.EOF {
return stream.SendAndClose(&proto.UploadResponse{})
}
if err !=nil {
return err
}
fmt.Println(res)
}

The client needs to call CloseAndRecv when it has finished sending data, for example:

1
2
3
4
5
6
7
8
9
10
11
12
13
for i := 1; i <= 10; i++ {
img := &proto.Image{FileName:"image"+strconv.Itoa(i),File:"file data"}
images := &proto.StreamImageList{Image:img}
err := stream.Send(images)
if err != nil {
ctx.JSON(map[string]string{
"err": err.Error(),
})
return
}
}
// Finish sending, close and get the message returned by the server
resp, err := stream.CloseAndRecv()

Bidirectional streaming RPC

Both sides can separately send a series of messages via a read-write data stream. These two streams operate independently, so the client and server can read and write in any order they wish, for example: the server can wait for all client messages before writing a response, or it can read a message then write a message, or use other combinations of reading and writing.

The server sends messages while receiving messages, for example:

1
2
3
4
5
6
7
8
9
10
11
for {
res, err := stream.Recv()
if err == io.EOF {
return nil
}
err = stream.Send(&proto.StreamSumData{Number: int32(i)})
if err != nil {
return err
}
i++
}

The client needs a flag to disconnect, CloseSend(), but the server doesn’t need it because the server disconnects implicitly. We just need to exit the loop to disconnect, for example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
for i := 1; i <= 10; i++ {
err = stream.Send(&proto.StreamRequest{})
if err == io.EOF {
break
}
if err != nil {
return
}
res, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return
}
log.Printf("res number: %d", res.Number)
}
stream.CloseSend()

Synchronous

A Channel provides a connection established with the host and port of a specific gRPC server. A Stub is created based on the Channel, and the RPC request can be actually called through the Stub.

Asynchronous based on CQ

  • CQ: Notification queue for completed asynchronous operations
  • StartCall() + Finish(): Create asynchronous tasks
  • CQ.next(): Get completed asynchronous operations
  • Tag: Identifiers marking asynchronous actions

Multiple threads can operate on the same CQ. CQ.next() can receive not only the completion events of the current request being processed but also the events of other requests. Suppose the first request is waiting for its reply data transmission to complete, and a new request arrives. CQ.next() can get the events generated by the new request and start processing the new request in parallel without waiting for the first request’s transmission to complete.

Asynchronous based on Callback

On the client side, send a single request, when calling the Function, in addition to passing the pointers of Request and Reply, a callback function receiving Status is also needed.

On the server side, the Function doesn’t return a status, but a ServerUnaryReactor pointer. Get the reactor through CallbackServerContext and call the Finish function of the reactor to handle the return status.

Context

  • Transfers some custom Metadata between the client and server.
  • Similar to HTTP headers, it controls call configurations such as compression, authentication, and timeout.
  • Assists observability, such as Trace ID.

gRPC Communication Protocol

The gRPC communication protocol is based on standard HTTP/2 design, supports bidirectional streams, multiplexing of single TCP (an HTTP request can be initiated in advance without waiting for the result of the previous HTTP request, and multiple requests can share the same HTTP connection without interfering with each other) and features such as message header compression and server push. These features make gRPC more power-saving and network traffic-saving on mobile devices.

gRPC Serialization Mechanism

Introduction to Protocol Buffers

gRPC serialization supports Protocol Buffers. ProtoBuf is a lightweight and efficient data structure serialization method, ensuring the high performance of gRPC calls. Its advantages include:

  • The volume of ProtoBuf after serialization is much smaller than JSON, XML, and the speed of serialization/deserialization is faster.
  • Supports cross-platform and multi-language.
  • Easy to use because it provides a set of compilation tools that can automatically generate serialization and deserialization boilerplate code.

However, ProtoBuf is a binary protocol, the readability of the encoded binary data stream is poor, and debugging is troublesome.

The scalar value types supported by ProtoBuf are as follows:

Why is ProtoBuf fast?

  • Because each field is stored continuously in the form of tag+value, the tag is a number, usually only occupying one byte, and the value is the value of the field, so there are no redundant characters.
  • In addition, for relatively small integers, ProtoBuf defines a Varint variable integer, which does not need to be stored in 4 bytes.
  • If the value is of string type and the specific length of the value cannot be known from the tag, ProtoBuf will add a leg field between the tag and the value to record the length of the string, so that string matching operations do not need to be performed, and the parsing speed is very fast.

Definition of IDL file

Define the data structure of RPC requests and responses in the proto file according to the syntax of Protocol Buffers, for example:

1
2
3
4
5
6
7
8
9
10
11
12
syntax = "proto3";
option go_package = "../helloworld";
package helloworld;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}

Here, syntax proto3 indicates the use of version 3 of Protocol Buffers. There are many changes in syntax between v3 and v2, so pay special attention when using it. go_package indicates the storage path of the generated code (package path). The data structure is defined by the message keyword, and the syntax of the data structure is:

Data_type Field_name = Tag

The message supports nesting, that is, A message references B message as its own field, which represents the aggregation relationship of objects, that is, A object aggregates (references) B object.

For some common data structures, such as common Header, you can define the proto file of the common data structure separately, and then import it for use, for example:

import "other_protofile.proto";

Import also supports cascading references, that is, a.proto imports b.proto, b.proto imports c.proto, then a.proto can directly use the message defined in c.proto.

References

  1. https://www.zhihu.com/zvideo/1427014658797027328
  2. https://zhuanlan.zhihu.com/p/389328756
  3. http://doc.oschina.net/grpc
Author

王亮

Posted on

2021-09-30

Licensed under