In gRPC development, the first step is to define the communication interfaces, which is typically done using Protocol Buffers (Protobuf for short). Protobuf is a flexible and efficient serialization format widely used for defining gRPC message formats and service interfaces. Below is the proto
file we defined, which describes the gRPC interface for user login and retrieving user information:
syntax = "proto3";
import "google/protobuf/timestamp.proto";
package user;
option go_package = "abi/grpc_user";
message SigninRequest {
string email = 1;
string password = 2;
}
message SigninResponse {
string AccessToken = 1;
}
message GetUserInfoRequest {}
message GetUserInfoResponse {
enum Gender {
GENDER_UNKNOWN = 0;
GENDER_GIRL = 1;
GENDER_BOY = 2;
}
enum UserStatus {
USER_STATUS_NORMAL = 0;
USER_STATUS_BAN = 1;
USER_STATUS_BAN_NOT_ACTIVATED = 2;
}
int64 uid = 4;
string user_code = 5;
string nickname = 1;
string email = 2;
Gender gender = 3;
string avatar = 6;
google.protobuf.Timestamp birth = 7;
string phone_number = 8;
UserStatus status = 9;
google.protobuf.Timestamp created_at = 10;
}
service User {
rpc Signin(SigninRequest) returns(SigninResponse);
rpc GetUserInfo(GetUserInfoRequest) returns(GetUserInfoResponse);
}
Installing and Using Protobuf to Compile gRPC Code
Before generating code from the Protobuf file, you need to install the protobuf compiler. It can compile proto files into code for the target programming language, allowing the language to directly use these definitions. You can install it using Homebrew:
brew install protobuf
After installing the protobuf compiler, you need to use the tonic-build
crate in your Rust project to compile and generate gRPC client code. tonic-build
is a tool that helps you build gRPC client and server code in Rust projects. Add the following to the [build-dependencies]
section of your Cargo.toml
:
cargo add tonic-build --build
Create or modify the build.rs file in the root directory of the project. This file is used to run custom build scripts when building the Rust project, and it can perform additional tasks during compilation, such as code generation, dependency checks, external library compilation, or adjusting build configurations based on environment variables. Here, we will use tonic-build
to compile the proto files:
use std::path::PathBuf;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let out_dir = PathBuf::from("src");
tonic_build::configure()
.out_dir(&out_dir)
.build_server(false)
.compile(&["proto/user/user.proto"], &["proto"])?;
Ok(())
}
Here, out_dir
specifies the output directory for the generated code, and we will generate the code into the src
directory. build_server(false)
indicates that only the client code is generated and not the server code (by default, both client and server code are generated). If you need server code, you can set it to true
. compile(&["proto/user/user.proto"], &["proto"])
specifies the proto
files to compile and their paths. It’s important to note that if the specified output directory out_dir
does not exist, the compiler will not automatically create it, so you need to ensure that the directory already exists.
Installing Dependency Libraries
After doing this, you need to add some dependencies:
First, tonic
, a Rust gRPC framework that provides client and server implementations, allowing you to easily build and use gRPC services in Rust projects.
cargo add tonic
Next, prost
, which handles Protobuf encoding and decoding. It can serialize Rust data structures into Protobuf format and deserialize Protobuf data into Rust data structures.
cargo add prost
Since google.protobuf.Timestamp
is used in the proto
file, you also need to add prost-types
, which provides support for some standard Protobuf types, such as Timestamp
, which is part of the Protobuf standard library.
cargo add prost-types
After doing all this, simply run cargo build
or cargo run
command to generate the gRPC code.
gRPC Client
Once the gRPC code is generated, you can write client code to initiate gRPC requests. Below is a simple client example that sends a login request to a gRPC server and receives a response:
use std::str::FromStr;
use proto_demo::user::{self, SigninRequest};
use tokio::runtime::Builder;
use tonic::{metadata::MetadataValue, Request};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut cli = user::user_client::UseClient::connect("http://[::1]:8081")
.await?;
// Construct SigninRequest request
let mut req = Request::new(SigninRequest {
email: "[email protected]".to_string(),
password: "123456".to_string(),
});
// Modify the metadata of the request
let metadata = req.metadata_mut();
// Add authentication token to the request metadata
let token = MetadataValue::from_str("authentication token")?;
metadata.append("authentication", token);
// Send Signin request to the server and wait for response
let res = cli.signin(req).await?;
let res = res.get_ref();
println!("gRPC response result: {:?}", res);
Ok(())
}
gRPC Server
If you need to create a gRPC server, you can refer to the following code. This code demonstrates how to implement a simple gRPC server in Rust:
use tonic::{transport::Server, Response};
use crate::user::{self, user_service_server::UserServiceServer};
#[derive(Default)]
pub struct UserService {}
#[tonic::async_trait]
impl user::user_service_server::UserService for UserService {
async fn signin(
&self,
_request: tonic::Request<user::SigninRequest>,
) -> Result<Response<user::SigninResponse>, tonic::Status> {
Ok(Response::new(user::SigninResponse {
access_token: "123".to_string(),
}))
}
async fn get_user_info(
&self,
request: tonic::Request<user::GetUserInfoRequest>,
) -> Result<Response<user::GetUserInfoResponse>, tonic::Status> {
let metadata = request.metadata();
let authentication = metadata.get("authentication").unwrap().to_str().unwrap();
println!("{}", authentication);
Ok(Response::new(user::GetUserInfoResponse {
..Default::default()
}))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "[::1]:8081".parse().unwrap();
Server::builder()
.add_service(UserServiceServer::new(UserService::default()))
.serve(addr)
.await?;
Ok(())
}