A gRPC service is essentially a group of related RPC endpoints—exactly how they’re related is up to you. A common example is a RESTful grouping where the relation is that the endpoints operate on the same resource, but the grouping could be looser than that. In general, it’s just a group of endpoints needed to solve some problem. In our case, the goal is to enable people to write to and read from their log.
Creating a gRPC service involves defining it in protobuf and then compiling your protocol buffers into code comprising the client and server stubs that you then implement. To get started, open log.proto, the file where we defined our Record message, and add the following service definition above those messages:
| service Log { |
| rpc Produce(ProduceRequest) returns (ProduceResponse) {} |
| rpc Consume(ConsumeRequest) returns (ConsumeResponse) {} |
| rpc ConsumeStream(ConsumeRequest) returns (stream ConsumeResponse) {} |
| rpc ProduceStream(stream ProduceRequest) returns (stream ProduceResponse) {} |
| } |
The service keyword says that this is a service for the compiler to generate, and each RPC line is an endpoint in that service, specifying the type of request and response the endpoint accepts. The requests and responses are messages that the compiler turns into Go structs, like the ones we saw in the previous chapter.
We have two streaming endpoints:
ConsumeStream—a server-side streaming RPC where the client sends a request to the server and gets back a stream to read a sequence of messages.
ProduceStream—a bidirectional streaming RPC where both the client and server send a sequence of messages using a read-write stream. The two streams operate independently, so the clients and servers can read and write in whatever order they like. For example, the server could wait to receive all of the client’s requests before sending back its response. You’d order your calls this way if your server needed to process the requests in batches or aggregate a response over multiple requests. Alternatively, the server could send back a response for each request in lockstep. You’d order your calls this way if each request required its own corresponding response.
Below your service definition, add the following code to define our requests and responses:
| message ProduceRequest { |
| Record record = 1; |
| } |
| |
| message ProduceResponse { |
| uint64 offset = 1; |
| } |
| |
| message ConsumeRequest { |
| uint64 offset = 1; |
| } |
| |
| message ConsumeResponse { |
| Record record = 2; |
| } |
The request includes the record to produce to the log, and the response sends back the record’s offset, which is essentially the record’s identifier. Similarly with consuming: the user specifies the offset of the logs they want to consume, and the server responds back with the specified record.
To generate the client- and server-side code with our Log service definition, we need to tell the protobuf compiler to use the gRPC plugin.