I recently mentioned the gRPC protocol in a discussion with a friend, and I’ve written about gRPC before. People usually use the official grpc library to communicate directly. But as far as I can see, most of them are Unary requests and very few are stream calls. Unary requests of gRPC are not much different from ordinary http/2 calls, so we can write a simple gRPC client by ourselves. Today, we will share our thoughts on the go language as an example. For those of you who insist on using the official SDK, don’t miss this article as it will also provide some help in understanding the operation of the gRPC protocol.
Before introducing the client, we need a server-side program that can be used for testing. This part can be implemented directly using the official SDK.
First, we define the proto file.
|
|
Since we need to import the generated grpc code in the go language, we need to set the package name of the generated code using go_package
.
Then, we use protoc to generate the corresponding pb and grpc files.
After successful execution, we will see the following file.
hello.pb.go
contains the definition of the interface input and output messages, and hello_grpc.pb.go
defines the interface to be implemented by the server and the client, as well as the implementation code for the client. All the server side has to do is to implement the corresponding interface first. We can create a hello.go
file with the following contents.
|
|
After implementing the interface logic, we still have to write the service startup code with the following core logic.
Well, here we have the simplest gRPC service. After running the code the service will listen on port 8080.
Next, we’re going to write our own client code. But before we start, we need to briefly review the gRPC communication protocol.
First of all, gRPC uses the http2 protocol at the bottom. http2 uses frames to transfer data, with different frames for headers and data, so that they can be crossed over, rather than having to pass headers and then data, as in http1. The structure of a Unary request is as follows.
There are several points to note here.
:path
holds the path to the interface, taking the value corresponding to the definition of the proto, with the structure/package name. service/method name
content-type
indicates the data encoding type, pb corresponds toapplication/grpc+proto
, or you can use jsontrailers
is fixed toTE
to indicate that the trailer headers can be handled correctly. Some gRPC libraries require this field to be passed
The last thing to note is the structure of the data as a Length-Prefixed
message. This is simply a five-byte prefix before the pb data, where the first byte indicates whether the pb data is compressed or not, and the next four bytes hold a thirty-two-bit integer in big-endian order, which is used to record the length of the pb data.
After the request message, let’s look at the response message.
Note here that the server first sends a header frame with :status
and content-type
information, then sends a data frame, and finally sends another header frame with grpc-status
and grpc-message
. The header after the data is sent is called the trailer header. gRPC is designed in such a way that we will not expand on it, if you are interested, you can see the link to the article given in my header.
Well, everything is ready, we can design our own gRPC client.
From the above analysis, we can conclude that we just need to serialize the request object into a pb byte stream, then insert a five-byte prefix in front, set the length of the pb data, and finally use the http standard library to send the data to the gRPC server. After receiving the response from the gRPC server, we deserialize the body into a response object, and we are done.
Wait, can we use the go language http standard library? Theoretically, yes, because the http standard library already supports the http2 protocol. However, there is a catch: the http standard library requires the server to use tls encryption when making http2 requests. In order to promote the popularity of https and protect user privacy, major browsers force http2 communication to be encrypted with tls. So go’s standard library also has this requirement. But generally speaking, intranet services run in a controlled environment, and tls configuration and certificate management are troublesome, so most intranet services do not use tls encryption (which is not true). So, we need to investigate a way to initiate http2 communication over plaintext tcp connections.
In fact, the method is very simple, we just need to prepare an http2.Transport
ourselves.
In the net/http
framework, Transport
is responsible for handling the real network communication and maintaining the underlying TCP connection. The plainTextH2Transport
we declare here is a global variable at the package level, which is the only way to reuse TCP connections.
The plaintext http2 client we described earlier can be used by simply declaring it as follows.
It’s that simple and uncomplicated! To facilitate expansion, we can design the following structure.
Since we have embedded http.Client
in GrpcClient
, GrpcClient
itself is also an http.Client
. Next we need to implement the Unary request logic. The interface signature is as follows.
Apart from ctx
, there are three core inputs, api/in/out
. In fact, api
is the full URL of the interface, in our case http://127.0.0.1:8080/hello.Greeter/SayHello
, which corresponds to the previous proto and the address the server listens to. in
and out
correspond to the request and return messages of the gRPC interface. Since we want to write a generic client, we set the type to proto.Message
uniformly. If we could generate the code automatically, we could determine each interface and the incoming and outgoing parameters from the proto file, and then generate the corresponding DoUnary methods separately. But we are designing a simple client today, and we don’t want to use a heavyweight solution like auto-generated code. So, we have to set it to proto.Message
type uniformly. But this design has a drawback, it requires the caller to initialize both in and out before calling. But it can’t be helped, it’s the only solution that doesn’t require code generation.
There are two return values, the first one is the underlying http response. Through it we can read the http protocol status code and various headers (gRPC metadata). The second one returns an error. Generally, the caller only needs to check for errors and does not need to access the first return value.
Okay, let’s analyze the code implementation of DoUnary. First, we need to construct the so-called Length-Prefixed message.
At this point, we have the Length-Prefixed message ready, and we have wrapped it in an io. Then, we have to make the http2 request.
Finally, we read all the returned data, take it and decode it into response objects.
|
|
The full code is available from here. The above is a simplest gRCP client.
Finally, we start the server side and run the following code.
The program will output “Hello 涛叔”.