在这篇文章中,我们将使用gRPC创建一个基本的Todo应用程序。首先,我们将非常快速的概述一下gRPC和Protocol Buffers。
什么是gRPC?
gRPC是一个现代的开源的高性能远程过程调用(RPC)框架,可以在任何环境下运行。RPC代表远程过程调用(Remote Procedure Call),开头的g代表通用(General Purpose),对某些人来说,它代表谷歌。
我假设你熟悉常见的REST api,它们通过JSON对象进行通信。在gRPC中,我们使用Protocol Buffers来序列化数据,而不是JSON。
Protocol Buffers
Protocol Buffers是谷歌定制的用于语言无关、平台无关、可扩展的,序列化结构性数据的一种协议。
在gRPC中,传输的(序列化的)数据是二进制的。这意味着它比JSON或XML更快,因为它占用更少的空间,而更少的空间带来更小的带宽。
Rust gRPC开发
首先,创建一个新的工程:
cargo new rust-grpc
现在,在创建工程之后,我们为gRPC添加一些依赖项,并在Cargo.toml中定义二进制程序,用于我们的服务器和客户端:
[[bin]]
name = "grpc-server"
path = "src/server.rs"
[[bin]]
name = "grpc-client"
path = "src/client.rs"
[dependencies]
tonic = "0.7"
prost = "0.10"
tokio = { version = "1.19", features = ["rt-multi-thread", "macros"] }
[build-dependencies]
tonic-build = "0.7"
tonic:一个基于HTTP/2的Rust gRPC库
tonic-build:编译proto文件成Rust代码
prost:Rust的Protocol Buffers实现库
tokio:Rust的异步运行时
创建proto文件
在项目路径下创建 proto/todo.proto:
syntax = "proto3";
import "google/protobuf/empty.proto";
package todo;
message TodoItem {
string name = 1;
string description = 2;
int32 priority = 3;
bool completed = 4;
}
message GetTodosResponse {
repeated TodoItem todos = 1;
}
message CreateTodoRequest {
string name = 1;
string description = 2;
int32 priority = 3;
}
message CreateTodoResponse {
TodoItem todo = 1;
bool status = 2;
}
service Todo {
rpc GetTodos(google.protobuf.Empty) returns (GetTodosResponse);
rpc CreateTodo(CreateTodoRequest) returns (CreateTodoResponse);
}
这是Proto文件的语法。让我们大概看一下:
syntax:定义了proto文件的版本
import:允许使用来自其他proto文件的定义。例如,由于GetTodos函数没有请求参数,所以导入empty.proto。
message:将message看作定义请求和响应的接口。分配给字段的数字表示字段编号。字段号是Protobuf的重要组成部分。它们用于标识二进制编码数据中的字段,这意味着它们不能在service的不同版本之间进行更改。
service:可以将其视为一个接口,在service中定义了函数、它们需要什么参数以及它们返回什么值。
编译proto文件
在项目目录下创建 build.rs :
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::configure(
.compile(&["proto/todo.proto"], &["proto"])
.unwrap();
Ok(())
}
目录结构如下:
Server端代码
src/server.rs
use std::sync::Mutex;
use todo::{
todo_server::{Todo, TodoServer},
CreateTodoRequest, CreateTodoResponse, GetTodosResponse, TodoItem,
};
use tonic::transport::Server;
pub mod todo {
tonic::include_proto!("todo");
}
#[derive(Debug, Default)]
pub struct TodoService {
todos: Mutex<Vec<TodoItem>>,
}
#[tonic::async_trait]
impl Todo for TodoService {
async fn get_todos(
&self,
_: tonic::Request<()>,
) -> Result<tonic::Response<GetTodosResponse>, tonic::Status> {
let message = GetTodosResponse {
todos: self.todos.lock().unwrap().to_vec(),
};
Ok(tonic::Response::new(message))
}
async fn create_todo(
&self,
request: tonic::Request<CreateTodoRequest>,
) -> Result<tonic::Response<CreateTodoResponse>, tonic::Status> {
let payload = request.into_inner();
let todo_item = TodoItem {
name: payload.name,
description: payload.description,
priority: payload.priority,
completed: false,
};
self.todos.lock().unwrap().push(todo_item.clone());
let message = CreateTodoResponse {
todo: Some(todo_item),
status: true,
};
Ok(tonic::Response::new(message))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "0.0.0.0:50051".parse().unwrap();
let todo_service = TodoService::default();
Server::builder()
.add_service(TodoServer::new(todo_service))
.serve(addr)
.await?;
Ok(())
}
让我们快速看看发生了什么:
首先,我们引入了来自tonic, Request, Response等所需的结构体。
同样,我们也引入了从todo.proto编译的Rust struct,可以在/target/debug/build目录中看到它们。
现在我们需要创建TodoService并为该服务实现Todo trait。
然后,实现我们的函数,get_todos将简单地返回带有todo项的GetTodosResponse。create_todo函数将获取request请求,从TodoItem struct创建todo项,将其加入到self.todos中并返回CreateTodoResponse,创建todo并且状态为true。
最后,我们在0.0.0.0:50051上启动服务器
Client端代码
src/client.rs
use crate::todo::{todo_client::TodoClient, CreateTodoRequest};
pub mod todo {
tonic::include_proto!("todo");
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut client = TodoClient::connect("http://0.0.0.0:50051").await?;
let request = tonic::Request::new(());
let response = client.get_todos(request).await?;
println!("{:?}", response.into_inner().todos);
let create_request = tonic::Request::new(CreateTodoRequest {
name: "test name".to_string(),
description: "test description".to_string(),
priority: 1,
});
let create_response = client.create_todo(create_request).await?;
println!("{:?}", create_response.into_inner().todo);
Ok(())
}
同样,让我们快速看看这里发生了什么:
首先,我们引入了从todo.proto编译的TodoClient和CreateTodoRequest。
现在,我们在main函数中,连接到0.0.0.0:50051,并为get_todos创建一个空请求(你应该记得,我们在proto文件中没有GetTodos的请求参数)。然后发送请求,等待响应,最后打印;对于create_todo几乎相同,但这次我们将CreateTodoRequest struct传递给请求,然后再次打印。
运行
cargo build
cargo run --bin grpc-server
cargo run --bin grpc-client
本文翻译自:
https://betterprogramming.pub/how-to-create-grpc-server-client-in-rust-4e37692229f0