In this tutorial, we will demonstrate how to use gRPC by building a chat app. We will describe in detail how you can set up a gRPC service and then communicate with it from a React app. The chat messages sending back and forth will be handled by a gRPC service. Let's see how it's done, but first, let's learn what gRPC is.
gRPC at a glance
gRPC is an inter-process communication technology that is used to execute remote sub-routines in a different address space. It uses the concept of message passing to signal a sub-routine residing in another system to execute. gRPC is awesome in that it creates a server service in a language and the service can be consumed from another platform in another language. This server service contains methods that can be called by the gRPC clients from anywhere in the world from any machine or platform.
gRPC has three components: Protocol Buffer, server, and client. Protocol Buffer is an open-source serialization tool built by Google. gRPC uses this to serialize the request and response message format between the server and the client. This is also where the service interface between the server and the client is defined. According to Kasun Indrasiri and Danesh Kuruppu in gRPC: Up and Running, The service interface definition contains information on how your service can be consumed by consumers, what methods you allow the consumers to call remotely, what method parameters and message formats to use when invoking those methods, and so on.
The server as we already know contains methods/sub-routines/procedures, anything you choose to call it. These methods perform an action on the server. The client is where the methods in the gRPC server are called from. We have known what gRPC and how it works, now let's proceed to build our chat app.
The chat app
Our chat app will be a group chat like Discord where people can join in and discuss matters with the members. The UI will look like this:
This is the join page where users will have to input their username before joining the chat.
This is the chat UI where the group can read and send messages. The UI is simple and so is the project. This will aim to introduce how we can use the gRPC protocol to create a chat app. Let's first create the server. The server will be built in Nodejs.
gRPC - Setup the chat service
Before we begin scaffolding projects and touching files, let's see the model and structure of our chat app. This will be done in the Protobuf file. We will create the file, chat.proto. We will need methods that:
- users will call to join the group chat
- a method that will be called by a user to send a message to the group.
- a method that will open a stream of data on the server, then the client will listen to the stream of data to get them. This stream of data will be messages sent by the users in the group, so we need to stream them to all client's listening.
- a method that will return all users in the group.
First, in the chat.proto we will have a message format for a chat message:
The fields, from, msg, and time will hold the name of the user in the group that sent the chat, the text or message sent, and the time when the message was sent respectively. The numbers in the fields are unique numbers. They are used for identification when the fields are encoded to message binary format. The field numbers are what is used to retrieve the values of the fields from their binary format. Field numbers that can be assigned range from 1 to 536,870,911. More on binary format here.
Next, we will have a message format for a user:
The fields id and name hold the unique identifier of a user and the name of the user respectively. We create an empty message because we will be needing it for methods that require no args or returns.
Since we will be returning a list of users, so we will have a message format for it.
The repeated keyword means that the field is a list or an array. So the users field will be an array of users User. We will have a join method that users will call to join a group, this should have a response message format to hold the status of the join attempt:
The error will hold a value to indicate whether the join request was successful. The msg will hold the response message for the join request. Now, our proto will be this:
The keyword service denotes the gRPC service the proto will be exporting. gRPC services in a proto file will have methods that will be available to the clients.
These methods are set using the rpc keyword. So above we have four methods. The join method will take a user User as a parameter so to get the user details from there and add the user to the group chat. It will respond with a JoinResponse type. The sendMsg sends messages to the group chat. It takes the ChatMessage message format as arg so it can get the message and the sender from the fields, it returns nothing, an empty object. The receiveMsg sets up a stream of chats on the server. The stream keyword denotes a stream of data being created and streamed to the end-user. Here, the receiveMsg will create and emit streams of chat messages ChatMessage, the client will listen to the stream to receive the chats streamed to the channel. This makes it possible to receive texts sent by users in real-time. The getAllUsers returns the users in the chat.
Now, we scaffold a Node project and write this chat.proto with its contents to it.
Move into the directory:
Init a Node environ:
Create chat.proto file:
Copy and paste the contents of the proto file we created earlier into it.
Now, we install dependencies:
- grpc: gRPC library for Nodejs runtime.
- @grpc/proto-loader: A utility package for loading .proto files for use with gRPC, using the latest Protobuf.js package.
Run the below command to install the dependencies:
Now, we create server.js file:
This file will contain the gRPC server code. We will import the gRPC dependencies, then set up our chat.proto path and our server URL address to point at "0.0.0.0:9090":
We will be using an in-memory database which will be arrays for now. We can actually connect a gRPC server to databases like MongoDB, MySQL, etc, but thatโs out of scope. So we create an empty array that will hold users in the group usersInchat and the user's server writable stream observers. Then, we load the chat.proto and load its package definitions.
Next, we set up the function handlers for the methods in the ChatService.
The call argument contains request parameters from the client and other parameters set by gRPC. The parameters set by the client resides in the request object. So we pull the user object from the request object and add it to the usersInChat array.
This returns the users in the usersInChat array to the client. The callback is a function that when called sends back a response to the client that called the method. The first param of the callback function denotes whether any error occurred, the second param is the response payload. So here, the client will get the users in the users property in the object returned.
This is called to set up a stream in the server so the client can listen to the data emitted in the stream. Here, the call argument will be a ServerWritableStream instance. It has a write that we call to emit data inside the stream. This will be done in the server here because the client that called this method will be listening to data emitted. So when the write method is called, the data is sent down to the client's "data" event handler function. We push the call argument to the observers array.
This method sends a message to the group chat. This pulls the message object from the call.request object and, loops over the observers array. The observers array contains ServerWritableStream instance from the receiveMsg method call.
Now, the write method on each ServerWritableStream instance in the observers array with the chat message in the chatObj as param. This will make the ServerWritableStream instance emit the chat message so the client's listening in on the emission event will get the chat messages. In the frontend part, the client will get the emitted chat message and we will use it to update the UI to see the message and who sent it.
We are done with our ChatService method handlers. Now, we set up the gRPC server:
The server holds the grpc.Server instance. Next, we add the ChatService service to the server using the addService() method. The first param to the addService is the service descriptor, in this case, we will get the ChatService descriptor from the protoDescriptor. The second param will be a map of method names to method implementation for the provided service.\
Next, we bind and start our server at port 9090
That's it. We can run the server now:
The full server code:
Our client will be a React.js application which means will be in the browser.
Browsers have to connect to gRPC services via a special proxy. This proxy is a process that can send HTTP/2 calls. So we send an HTTP 1.1 call to the proxy from the browser, the proxy gets it and calls the gRPC server via HTTP/2 sending the request URL and parameters with it. Then, it receives a response from the gRPC server via HTTP/2, the response is now sent to the client via HTTP 1.1 by the proxy. The ideal proxy process for this is Envoy.
Set up Envoy proxy
We will set up the Envoy proxy in a Docker image. But before we do that let's set up our React project because that is where Envoy config files will reside.
Scaffold React project
Run the below command to scaffold a React project:
Install the dependencies:
- grpc-web: provides a Javascript library that lets browser clients access a gRPC service.
- google-protobuf: contains the JavaScript Protocol Buffers runtime library.
Now, copy the chat.proto from the server project grpc-chat to this React.js project. Copy it in the src/ folder. We need the file because we will need it to generate gRPC client .js files so we can use them to call the gRPC methods. First, create an envoy.yaml in the root of this React.js folder. This is Envoy's config file that Docker will use to set up an Envoy process running. In the envoy.yaml files paste the config code:
That's a lot there, we will go over the necessary configurations.
This sets the URL address and port of the Docker image.
This sets the URL address where the gRPC browser client will direct its HTTP/1.1 calls. For example, to call the join method the URL will be this:
So 0.0.0.0:8080 is the address of the Envoy process running in the Docker image.
This sets the URL address of the gRPC server where the Envoy proxy will direct its HTTP/2 calls. See that the address port is the same as the port our gRPC server is running on 9090. The address is set to host.docker.internal because Docker will set its address to the gRPC server so we use the Docker's address. So the mental image of the HTTP calls will be this:
- browser call ChatService methods on port 8080 via HTTP 1.1.
- Docker running on 9091 receives the call and makes HTTP/2 call to gRPC running on 9090 passing the URL address and request params.
- gRPC server sends a response to the Envoy via HTTP/2 on 9091, Envoy sends the response to the browser client via HTTP 1.1 on port 3000.
Let's create a Dockerfile in the same place as envoy.yaml
Next, we build the Docker image:
This builds a Docker image with the name grpc-web-react. Now, we run the Docker:
We will see our Docker image running:
Setting the gRPC web protoc
Now we will have to compile the chat.proto file using proto compiler. This is to generate the JavaScript code equivalent of the Protobuf code we have in the chat.proto, this is done so we can access the message types, services and call the methods using JavaScript. Each language has its own proto compiler, that we can use to generate the language's client gRPC source code. We need to install the Proto plugin and Protoc compiler for JavaScript.
For more info on how to install the Proto tools, visit the grpc-web GitHub page. After all, is done, we will have the protoc compiler globally accessible from our Terminal.
So we compile the chat.proto file, run the below command:
This will compile the chat.proto file and generate the files:
- chat_pb.js: This will export the message formats in the chat.proto file.
- chat_grpc_web_pb.js: This will export the ChatServiceClient. The services in the proto file are exported here. The services are exported with the Client attached to their names.
Now, we build the UI components.
Build the UI
We will have presentational and page components. Presentational components are dumb components that merely displays data passed to it. Page components are components attached to a route, they are displayed when the route they are attached is navigated to in the browser.
Presentational components:
- Header: This will display the header.
- Chat: This component will display the chat messages.
- UsersList: This will display the list of users in the group.
Page components:
- ChatPage: This page will render the components that will display the UI for typing, sending, displaying the messages.
Let's touch the components and mkdir the folders:
Our base component is the App.js so we start from there.
First, we do the imports. The styling in App.css is imported. Next, we imported the Header component, then, User message type is imported as defined in the proto is exported from chat_pb.js. ChatServiceClient is imported from the chat_grpc_web_pb.js. Further down, we imported the ChatPage component and the useState hook.
We set up a gRPC client by instantiating the ChatServiceClient and passing localhost:8080 in the constructor. The instance is held in the client variable. This is what we will use to call the methods in the ChatService. Inside the App component, we have a submitted state, it holds a boolean state that indicates whether the join request is successful or not. We have two methods renderChatPage and renderJoinPage. The renderChatPage method renders the UI for the chat UI, while the renderJoinPage will render the UI to take input for joining the group.
See that in the renderJoinPage UI, there is an input box that will take the name of the user, the Join button when clicked will call the joinHandler function. The joinHandler function, will call the join() in the client. This will invoke the join method in the gRPC server. See, that the User instance with the id and name set is passed as param to the join() call. In the callback function, the submitted state is set if successful and the ChatPage component is rendered.
Open the App.css, clear the contents and save. Let's see the ChatPage component.
ChatPage
Open the ChatPage/index.js and paste the code:
We have states to hold all users in the group, and the messages sent. The name of the user is retrieved from the localStorage and stored in the username variable.
The callback in the useEffect hook calls the receiveMsg method, as this method returns a stream from the server. We listen to the stream of data to be emitted from the server by calling the then on() method and passing the "data" text in the first param and a callback function in the next. This listens for data on the server-side streaming, the data emitted by the server is retrieved from the response arg in the callback function. This response will be a ChatMessage so we retrieve who sent the message from, the message msg and time it was sent time by calling getFrom(), getMsg(), and getTime() respectively. To differentiate a message by the user from the users in the chat, we check the from to against the current user in the username and add mine prop to the object, then, we set the message object to the msgList array state.
The getAllUsers function gets all the users in the chat, see it calls the getAllUsers() in the gRPC service and sets the response to the msgList state. The sendMessage function sends the chat message to the server. The message arg holds the message to be sent. It constructs its ChatMessage object and calls the sendMsg with it. This invokes the sendMsg method in the gRPC server. The UI renders a button we can click to refresh the users' list, displays the name of the user, and renders the UsersList and Chat components passing the users to the UsersList component via users input, and the msgList and sendMessage function to the Chat component as input props.
Add the styling in ChatPage.css
UsersList
Open the UsersList/index.js file and paste the code:
It destructures the users array from its props. Then, renders it. Add, the styling to UsersList.css:
Chat
Open Chat/index.js and paste the code:
It destructures the msgList array, sendMesgage function from its props. The UI has an input box and button for sending messages. The button when clicked calls the handler function, it extracts the chat text from the input box and calls the sendMessage function passing in the chat text as a parameter.
The UI also displays the messages in the msgList array. It loops through them and displays each message in the ChatCard component. See that the ChatCard component uses the mine prop in the chat to know how to display chats belonging to the user and the ones not belonging to the user. The chatcard-mine CSS class is set for chats sent by the user, while chatcard-friend CSS class is set for chats by other users.
Open Chat.css and paste the below styling code:
Header
This simply displays a header for the chat app. Add the below code to Header/index.js:
Set the styling in its Header.css
Set the global styles
Open index.css and paste the below code:
If your create-react-app server is not running, launch it:
Go to your favorite browser and navigate to localhost:3000. Open another browser side-by-side with the first browser. Then, join in on the chat with different usernames and start chatting. You will see how fast the messages are delivered in real-time. That's the power of gRPC.
Source code
Find the repos of the gRPC server and React.js client source code below:
Conclusion
We learned what gRPC is all about. We started by looking at a bit of its history and then, how it works. We learned about gRPC streaming which enables us to stream and listen to a sequence of data in real-time. We demonstrated all this by building a chat app in React.js, in doing so, we also learned about proxying requests from a browser client using Envoy, we learned a little bit about Envoy configurations and we were able to communicate with a gRPC server from the browser. That's a whole lot we learned in this tutorial.
gRPC opens up a whole new world of possibilities and at the same time harnessing the awesome power of HTTP/2.