< 返回版块

0x5F3759DF 发表于 2020-07-24 01:15

Tags:gRPC, grpc, rpc, remote procedure call

Tonic

gRPC的rust实现,高性能,开源,为移动设备与HTTP/2准备的通用RPC框架

tonic是基于HTTP/2的gRPC实现,专注于高性能,互通性和灵活性。 创建该库的目的是为了对async/await具有一流的支持,并充当用Rust编写的生产系统的核心构建块。

特性

  • 双向流传输
  • 高性能异步io
  • 互通性
  • 通过rustls进行TLS加密支持
  • 负载均衡
  • 自定义元数据
  • 身份认证
  • 健康检查

入门

本教程作为Tonic的入门指导,前提是你对Rust有基础了解并且使用过protocol buffers

先决条件

运行此教程中的代码,唯一需要安装的是Rust,如果你还没有安装rustup,可以尝试使用它快速方便的安装Rust.

项目设置

首先,我们需要使用Cargo创建一个新的Rust项目:

$ cargo new helloworld-tonic
$ cd helloworld-tonic

tonic需要rust1.39及以上版本,因为它需要async_await特性的支持。

$ rustup update
$ rustup component add rustfmt

定义HelloWorld服务

我们第一步需要做的是使用protocol buffers定义gRPC服务以及请求和相应的类型。我们将这个定义文件.proto放进crate根目录下的一个子目录中。需要注意的是,Tonic对.proto定义文件的位置并没有严格的要求。

$ mkdir proto
$ touch proto/helloworld.proto

然后你需要在服务定义中定义RPC方法,并且指定它们的请求与相应类型。gRPC允许你定义4种不同的服务方式,Tonic都可支持。在这个入门指导中,我们只使用简易的RPC

首先,我们需要定义包的名称,当在你的客户端——服务器应用中引用proto文件时,Tonic会通过这个包名称来搜索。这里,我们叫它helloworld

syntax = "proto3";
package helloworld;

下一步,我们来定义服务。这个服务中将包含我们的应用中会使用到的RPC调用。每个RPC包含一个标识符,一个请求类型,并返回一个响应类型。 这是我们的Greeter服务,它提供SayHello RPC方法。

service Greeter {
    // SayHello rpc 接受 HelloRequests 并返回 HelloReplies
    rpc SayHello (HelloRequest) returns (HelloReply);
}

最后,我们需要定义上面SayHello RPC方法中的请求与返回的类型。RPC类型定义为包含类型化字段的消息,如下:

message HelloRequest {
    // 请求消息中包含要问候的名称
    string name = 1;
}

message HelloReply {
    // 回复包含问候语
    string message = 1;
}

最终,我们入门指导中的.proto文件完整的样子:

syntax = "proto3";
package helloworld;

service Greeter {
    rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
   string name = 1;
}

message HelloReply {
    string message = 1;
}

应用设置

现在,在完成了protobuf的定义之后,我们可以用Tonic来完成我们应用的逻辑。首先,我们需要在Cargo.toml中加入必要的依赖项。

[package]
name = "helloworld-tonic"
version = "0.1.0"
edition = "2018"

[[bin]] # 用来运行 HelloWorld gRPC 服务器的可执行文件
name = "helloworld-server"
path = "src/server.rs"

[[bin]] # 用来运行 HelloWorld gRPC 客户端的可执行文件
name = "helloworld-client"
path = "src/client.rs"

[dependencies]
tonic = "0.3"
prost = "0.6"
tokio = { version = "0.2", features = ["macros"] }

[build-dependencies]
tonic-build = "0.3"

我们加进了tonic-build是为了可以使用它在构建过程中很方便的生成gRPC的客户端和服务器端代码。以下是设置这个构建过程的方式:

生成服务器端以及客户端代码

在crate的根目录下,创建一个build.rs文件,然后添加以下代码:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_build::compile_protos("proto/helloworld.proto")?;
    Ok(())
}

这个方程的目的是在你构建Rust项目时利用tonic-build来编译protobuf文件。配置这个构建过程的方式不止此一种,不过其他方式我们就不再这里赘述了。

编写服务器端代码

至此,我们的构建过程已经完备,依赖项也已经配置完成。我们可以开始编写有意思的部分了。我们需要引用服务器需要的库,包括protobuf。从在/src目录下创建一个叫server.rs的文件并加入以下代码开始:

use tonic::{transport::Server, Request, Response, Status};

use hello_world::greeter_server::{Greeter, GreeterServer};
use hello_world::{HelloReply, HelloRequest};

pub mod hello_world {
    tonic::include_proto!("helloworld"); // 这里指定的字符串必须与proto的包名称一致
}

接下来,我们可以在代码中实现之前在.proto中定义的Greeter服务:

#[derive(Debug, Default)]
pub struct MyGreeter {}

#[tonic::async_trait]
impl Greeter for MyGreeter {
    async fn say_hello(
        &self,
        request: Request<HelloRequest>, // 接收以HelloRequest为类型的请求
    ) -> Result<Response<HelloReply>, Status> { // 返回以HelloReply为类型的示例作为响应
        println!("Got a request: {:?}", request);

        let reply = hello_world::HelloReply {
            message: format!("Hello {}!", request.into_inner().name).into(), // 由于gRPC请求和响应中的字段都是私有的,所以需要使用 .into_inner()
        };

        Ok(Response::new(reply)) // 发回格式化的问候语
    }
}

最后,我们需要使用Tokio运行时来跑我们的服务器。这里我们需要加入Tokio这个库作为依赖。

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "[::1]:50051".parse()?;
    let greeter = MyGreeter::default();

    Server::builder()
        .add_service(GreeterServer::new(greeter))
        .serve(addr)
        .await?;

    Ok(())
}

最后完成的代码:

use tonic::{transport::Server, Request, Response, Status};

use hello_world::greeter_server::{Greeter, GreeterServer};
use hello_world::{HelloReply, HelloRequest};

pub mod hello_world {
    tonic::include_proto!("helloworld");
}

#[derive(Debug, Default)]
pub struct MyGreeter {}

#[tonic::async_trait]
impl Greeter for MyGreeter {
    async fn say_hello(
        &self,
        request: Request<HelloRequest>,
    ) -> Result<Response<HelloReply>, Status> {
        println!("Got a request: {:?}", request);

        let reply = hello_world::HelloReply {
            message: format!("Hello {}!", request.into_inner().name).into(),
        };

        Ok(Response::new(reply))
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "[::1]:50051".parse()?;
    let greeter = MyGreeter::default();

    Server::builder()
        .add_service(GreeterServer::new(greeter))
        .serve(addr)
        .await?;

    Ok(())
}

现在,你应该可以通过命令:cargo run --bin helloworld-server来运行你的HelloWorld gRPC服务器。这里使用到了之前我们在Cargo.toml中定义的[[bin]]来运行服务器。

你可以使用类似Bloom RPC的图形化gRPC客户端发送请求用来测试是否能正常得到服务器返回的问候语。

你也可以用命令行工具比如grpcurl来发送请求进行测试:

$ grpcurl -plaintext -import-path ./proto -proto helloworld.proto -d '{"name": "Tonic"}' [::]:50051 helloworld.Greeter/SayHello

得到的服务器返回的相应应该是这样的:

{
  "message": "Hello Tonic!"
}

编写客户端代码

我们现在有一个可以运行的gRPC服务器,但如何让我们的程序和它进行通信呢?我们需要编写一个客户端。Tonic支持客户端与服务器端的实现,我们从在/src目录下创建一个叫client.rs的文件并引用所有需要的库开始:

use hello_world::greeter_client::GreeterClient;
use hello_world::HelloRequest;

pub mod hello_world {
    tonic::include_proto!("helloworld");
}

客户端相比服务器端来说,实现更加简洁,因为我们不需要在客户端中定义服务方法,只需要发送请求。这里我们使用Tokio运行时来发送我们的请求,并将返回的响应消息打印到终端中:

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut client = GreeterClient::connect("http://[::1]:50051").await?;

    let request = tonic::Request::new(HelloRequest {
        name: "Tonic".into(),
    });

    let response = client.say_hello(request).await?;

    println!("RESPONSE={:?}", response);

    Ok(())
}

完整的客户端源代码:

use hello_world::greeter_client::GreeterClient;
use hello_world::HelloRequest;

pub mod hello_world {
    tonic::include_proto!("helloworld");
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut client = GreeterClient::connect("http://[::1]:50051").await?;

    let request = tonic::Request::new(HelloRequest {
        name: "Tonic".into(),
    });

    let response = client.say_hello(request).await?;

    println!("RESPONSE={:?}", response);

    Ok(())
}

总结

目前我们编写了protobuf文件,一个构建文件来编译我们的protobuf文件,一个实现SayHello服务的服务器端程序,和一个用来向服务器发送请求的客户端。你应该在项目根目录下有以下文件: proto/helloworld.protobuild.rssrc/server.rssrc/client.rs

运行服务器:cargo run --bin helloworld-server。然后在另一个终端窗口中运行客户端:cargo run --bin helloworld-client

如果一切正常,你应该可以看到运行服务器端程序的终端窗口中的日志会打印出请求信息,而运行客户端的终端窗口中则会打印出响应信息。

恭喜你完成了Tonic的入门指导!希望这个入门指导能帮助你理解Tonic的基础,并帮助你开始使用gRPC在Rust中编写高性能,有互通性和灵活性的服务器。

评论区

写评论
DoubleZhangYH 2023-09-21 09:46

当电脑用户名是中文时,出现编译错误,提示用户tmp目录下NO such file or dir.

1 共 1 条评论, 1 页