Skip to content

daheige/rs-grpc

Repository files navigation

rs-grpc

rust grpc microservices in action project https://github.com/daheige/rs-grpc

rust grpc crate

tonic https://crates.io/crates/tonic

grpc client support

  • rust grpc采用tokio,tonic,tonic-build 和 prost 代码生成,进行构建
  • grpc客户端支持go,nodejs,rust等不同语言调用服务端程序
  • 支持http gateway模式(http json请求到网关层后,转换为pb message,然后发起grpc service调用

tools installation before development

  1. 进入 https://go.dev/dl/ 官方网站,根据系统安装不同的go版本,这里推荐在linux或mac系统上面安装go。
  2. 设置Go GOPROXY 环境变量
go env -w GOPROXY=https://goproxy.cn,direct
  1. 安装protoc工具
  • mac系统安装方式如下:
brew install automake
brew install libtool
brew install protobuf
  • linux系统安装方式如下:
# Reference: https://grpc.io/docs/protoc-installation/
PB_REL="https://github.com/protocolbuffers/protobuf/releases"
curl -LO $PB_REL/download/v3.15.8/protoc-3.15.8-linux-x86_64.zip
unzip -o protoc-3.15.8-linux-x86_64.zip -d $HOME/.local
export PATH=~/.local/bin:$PATH # Add this to your `~/.bashrc`.
protoc --version
libprotoc 3.15.8
  1. 执行如下命令安装rust
# 下面两个环境变量,建议放在 ~/.bash_profile 或 ~/.bashrc 文件中
# 然后执行 source ~/.bash_profile 或 source ~/.bashrc 生效
export RUSTUP_DIST_SERVER=https://mirrors.ustc.edu.cn/rust-static
export RUSTUP_UPDATE_ROOT=https://mirrors.ustc.edu.cn/rust-static/rustup

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

这里也可以使用rsproxy代理(建议跟~/.cargo/config.toml文件中的replace-with配置保持一致),这里我使用的是ustc镜像源

export RUSTUP_DIST_SERVER="https://rsproxy.cn"
export RUSTUP_UPDATE_ROOT="https://rsproxy.cn/rustup"

通过 vim ~/.cargo/config.toml 文件添加如下内容:

[source.crates-io]
#registry = "https://github.com/rust-lang/crates.io-index"
# 指定镜像,这里可以根据实际情况选择不同的镜像
replace-with = 'ustc'

# 字节跳动的rsproxy,指定方式,只需要调整 [source.crates-io] 下面的 `replace-with = 'rsproxy-sparse'`
[source.rsproxy]
registry = "https://rsproxy.cn/crates.io-index"
[source.rsproxy-sparse]
registry = "sparse+https://rsproxy.cn/index/"

[registries.rsproxy]
index = "https://rsproxy.cn/crates.io-index"

# 清华大学
[source.tuna]
registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"

# 中国科学技术大学
[source.ustc]
registry = "sparse+https://mirrors.ustc.edu.cn/crates.io-index/"

# 上海交通大学
[source.sjtu]
registry = "https://mirrors.sjtug.sjtu.edu.cn/git/crates.io-index"

# rustcc社区
[source.rustcc]
registry = "git://crates.rustcc.cn/crates.io-index"

# xuanwu社区,指定方式,只需要调整 [source.crates-io] 下面的 `replace-with = 'xuanwu-sparse'` 即可
[source.xuanwu]
registry = "https://mirror.xuanwu.openatom.cn/crates.io-index"
[source.xuanwu-sparse]
registry = "sparse+https://mirror.xuanwu.openatom.cn/index/"
[registries.xuanwu]
index = "https://mirror.xuanwu.openatom.cn/crates.io-index"

[net]
git-fetch-with-cli=true
[http]
check-revoke = false
  1. 根据操作系统类型,在 https://nodejs.org/zh-cn/download 下载并安装nodejs

create a rust grpc project

   cargo new rs-grpc
  1. 新建src/client.rs
fn main() {}
  1. 在src同级目录新建build.rs文件,添加如下内容:
use std::ffi::OsStr;
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 推荐下面的方式生成grpc rust代码
    // 完成下面的步骤后,在main.rs中添加 mod rust_grpc;
    // 1.读取proto目录下的*.proto
    let proto_dir: PathBuf = "proto".into(); // proto文件所在目录
    let mut file_list = Vec::new(); // 存放proto文件名
    let lists = proto_dir.read_dir().expect("read proto dir failed");
    for entry_path in lists {
        if entry_path.as_ref().unwrap().path().is_file() {
            file_list.push(entry_path.unwrap().path())
        }
    }

    let out_dir = Path::new("src/rust_grpc"); // 存放grpc rust代码生成的目录
    // let _ = fs::remove_dir_all(out_dir); // 删除原来的pb目录,可以根据实际情况打开注释
    let _ = fs::create_dir(out_dir); // 创建目录

    // grpc reflection 描述信息这是一个二进制文件
    let descriptor_path = out_dir.join("rpc_descriptor.bin");

    // 2.生成rust grpc代码
    // 指定rust grpc 代码生成的目录
    tonic_prost_build::configure()
        .file_descriptor_set_path(&descriptor_path)
        .out_dir(out_dir)
        .compile_protos(&file_list, &[proto_dir])?;

    // 3.生成mod.rs文件
    // 用下面的rust方式生成mod.rs
    // 拓展名是proto的文件名写入mod.rs中,作为pub mod xxx;导出模块
    // 先清空crates/pb/src/lib.rs文件内容
    let mod_file = out_dir.join("mod.rs");
    let _ = fs::remove_file(mod_file);

    let ext: Option<&OsStr> = Some(&OsStr::new("proto"));
    let mut mod_file = fs::OpenOptions::new()
        .write(true)
        .create(true)
        .open(out_dir.join("mod.rs"))
        .expect("create mod.rs failed");

    let header = String::from("// @generated by tonic-build.Do not edit it!!!\n");
    let _ = mod_file.write(header.as_bytes());
    for file in file_list.iter() {
        if file.extension().eq(&ext) {
            if let Some(file) = file.file_name() {
                let f = file.to_str().unwrap();
                let filename = f.replace(".proto", "");
                println!("current filename: {}", f);
                let _ = mod_file.write(format!("pub mod {};\n", filename).as_bytes());

                // 实现message serde encode/decode
                let filename = out_dir.join(f.replace(".proto", ".rs"));
                let mut buffer = fs::read_to_string(&filename).unwrap();
                buffer = buffer.replace(
                    "prost::Message",
                    "prost::Message, serde::Serialize, serde::Deserialize",
                );
                fs::write(&filename, buffer).expect("write file content failed");
            }
        }
    }

    // 将生成的代码放在rust gateway目录中
    let gateway_dir = Path::new("gateway/rust_grpc");
    fs::create_dir_all(gateway_dir)?; // 创建gateway目录
    copy_dir_to(out_dir, gateway_dir)?;
    fs::remove_file(gateway_dir.join("rpc_descriptor.bin"))?;

    Ok(())
}

/// Copy the existing directory `src` to the target path `dst`.
fn copy_dir_to(src: &Path, dst: &Path) -> io::Result<()> {
    if !dst.is_dir() {
        fs::create_dir(dst)?;
    }
    for entry_result in src.read_dir()? {
        let entry = entry_result?;
        let file_type = entry.file_type()?;
        copy_to(&entry.path(), &file_type, &dst.join(entry.file_name()))?;
    }
    Ok(())
}

/// Copy whatever is at `src` to the target path `dst`.
fn copy_to(src: &Path, src_type: &fs::FileType, dst: &Path) -> io::Result<()> {
    if src_type.is_file() {
        fs::copy(src, dst)?;
    } else if src_type.is_dir() {
        copy_dir_to(src, dst)?;
    } else {
        return Err(io::Error::new(
            io::ErrorKind::Other,
            format!("don't know how to copy: {}", src.display()),
        ));
    }

    Ok(())
}
  1. 添加依赖 具体见Cargo.toml

  2. cargo run --bin rs-grpc 这一步就会安装好所有的依赖,并构建proto/hello.proto

  3. 在src/main.rs中添加rust grpc server代码

use autometrics::autometrics;
use infras::metrics::prometheus_init;
use rust_grpc::hello::greeter_service_server::{GreeterService, GreeterServiceServer};
use rust_grpc::hello::{HelloReply, HelloReq};
use std::net::SocketAddr;
use std::time::Duration;
use tonic::{transport::Server, Request, Response, Status};

mod infras;
/// 定义grpc代码生成的包名
mod rust_grpc;

// 这个file descriptor文件是build.rs中定义的descriptor_path路径
// 读取proto file descriptor bin二进制文件
pub(crate) const PROTO_FILE_DESCRIPTOR_SET: &[u8] = include_bytes!("rust_grpc/rpc_descriptor.bin");

/// 实现hello.proto 接口服务
#[derive(Debug, Default)]
pub struct GreeterImpl {}

#[async_trait::async_trait]
impl GreeterService for GreeterImpl {
    // 实现async_hello方法
    #[autometrics]
    async fn say_hello(&self, request: Request<HelloReq>) -> Result<Response<HelloReply>, Status> {
        // 获取request pb message
        let req = &request.into_inner();
        println!("got request.id:{}", req.id);
        println!("got request.name:{}", req.name);
        let reply = HelloReply {
            message: format!("hello,{}", req.name),
            name: format!("{}", req.name).into(),
        };

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

/// 采用 tokio 运行时来跑grpc server
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let address: SocketAddr = "0.0.0.0:50051".parse()?;
    println!("grpc server run on:{}", address);

    // grpc reflection服务
    let reflection_service = tonic_reflection::server::Builder::configure()
        .register_encoded_file_descriptor_set(PROTO_FILE_DESCRIPTOR_SET)
        .build_v1()
        .unwrap();

    // create http /metrics endpoint
    let metrics_server = prometheus_init(8090);
    let metrics_handler = tokio::spawn(metrics_server);

    // create grpc server
    let greeter = GreeterImpl::default();
    let grpc_handler = tokio::spawn(async move {
        Server::builder()
            .add_service(reflection_service)
            .add_service(GreeterServiceServer::new(greeter))
            .serve_with_shutdown(
                address,
                infras::shutdown::graceful_shutdown(Duration::from_secs(3)),
            )
            .await
            .expect("failed to start grpc server");
    });

    // run async tasks by tokio try_join macro
    let _ = tokio::try_join!(metrics_handler, grpc_handler);
    Ok(())
}

settings

配置文件读取,参考: infras/config.rssrc/app.rs

grpcurl tools usage

grpcurl工具主要用于grpcurl请求,可以快速查看grpc proto定义以及调用grpc service定义的方法。 https://github.com/fullstorydev/grpcurl

tonic grpc reflection使用需要注意的事项:

  • 使用这个操作必须将grpc proto的描述信息通过add_service添加才可以
  • tonic 和 tonic-reflection 以及 tonic-build 需要相同的版本,这个需要在Cargo.toml设置一样
  1. 安装grpcurl工具
brew install grpcurl

如果你本地安装了golang,那可以直接运行如下命令,安装grpcurl工具

go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
  1. 验证rs-grpc service启动的效果
grpcurl -plaintext 127.0.0.1:50051 list

执行上面的命令,输出结果如下:

Hello.GreeterService
grpc.reflection.v1alpha.ServerReflection
  1. 查看proto文件定义的所有方法
grpcurl -plaintext 127.0.0.1:50051 describe Hello.GreeterService

输出结果如下:

Hello.GreeterService is a service:
service GreeterService {
  rpc SayHello ( .Hello.HelloReq ) returns ( .Hello.HelloReply );
}
  1. 查看请求HelloReq请求参数定义
grpcurl -plaintext 127.0.0.1:50051 describe Hello.HelloReq

完整的HelloReq定义如下:

Hello.HelloReq is a message:
message HelloReq {
  int64 id = 1;
  string name = 2;
}
  1. 查看相应HelloReply响应结果定义
grpcurl -plaintext 127.0.0.1:50051 describe Hello.HelloReply

完整的HelloReply定义如下:

Hello.HelloReply is a message:
message HelloReply {
  string name = 1;
  string message = 2;
}
  1. 通过grpcurl调用rpc service method
grpcurl -d '{"name":"daheige"}' -plaintext 127.0.0.1:50051 Hello.GreeterService.SayHello

响应结果如下:

{
 "name": "daheige",
 "message": "hello,daheige"
}

multiplex service

由于tower steer抽象设计,可以将grpc service转换为axum http Router。因此,我们可以将grpc server和 http gateway在一个端口上运行 src/multiplex_server.rs。

# 添加如下依赖
# 用于将grpc服务和http服务运行在一个端口上面
tower = { version = "0.5.2", features = ["steer"] }

运行服务端:

cargo run --bin rs-multiplex-svc

成功运行后的效果:

Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/rs-multiplex-svc`
grpc server and http server run on:0.0.0.0:8081

验证其运行效果:

grpcurl -d '{"name":"daheige"}' -plaintext 127.0.0.1:8081 Hello.GreeterService.SayHello

输出结果如下:

{
  "name": "daheige",
  "message": "hello,daheige"
}

发送 multiplex http 请求:

curl --location --request POST 'localhost:8081/v1/greeter/say_hello' \
--header 'Content-Type: application/json' \
--data-raw '{"id":1,"name":"daheige"}'

响应结果:

{
  "code": 0,
  "message": "ok",
  "data": {
    "name": "daheige",
    "message": "hello,daheige"
  }
}
  • 这种将grpc service和http service同时启动的流程,借助的是tower steer特性。
  • 接入的路由,可以通过axum灵活配置处理,也就是说可以不用再额外再去实现grpc http gateway。

run grpc server

cargo run --bin rs-grpc

output:

 Finished dev [unoptimized + debuginfo] target(s) in 0.18s
 Running `target/debug/rs-grpc`
 grpc server run on:127.0.0.1:50051

run rust client

cargo run --bin rs-rpc-client

output:

    Finished dev [unoptimized + debuginfo] target(s) in 0.18s
     Running `target/debug/rs-rpc-client`
client:GreeterServiceClient { inner: Grpc { inner: Channel, origin: /, compression_encoding: None, accept_compression_encodings: EnabledCompressionEncodings } }
res:Response { metadata: MetadataMap { headers: {"content-type": "application/grpc", "date": "Fri, 17 Nov 2023 16:11:10 GMT", "grpc-status": "0"} }, message: HelloReply { name: "daheige", message: "hello,daheige" }, extensions: Extensions }
name:daheige
message:hello,daheige

run nodejs client

install nodejs grpc tools

sh bin/node-grpc-tools.sh

generate nodejs code

sh bin/nodejs-gen.sh

install nodejs package

sudo npm install -g yarn
cd clients/nodejs && yarn install

run node client

node clients/nodejs/hello.js

output:

{
  wrappers_: null,
  messageId_: undefined,
  arrayIndexOffset_: -1,
  array: [ 'heige', 'hello,heige' ],
  pivot_: 1.7976931348623157e+308,
  convertedPrimitiveFields_: {}
}
message:  hello,heige
name:  heige

run go client

# please install go before run it.
go mod tidy
sh bin/go-gen.sh #generate go grpc/http gateway code
cd clients/go && go build -o hello && ./hello

output:

2023/11/17 23:23:30 x-request-id:  56fde08ea70a4976bfcfd781ac8e8bba
2023/11/17 23:23:30 name:golang grpc,message:hello,rust grpc

rust http gateway

  1. 运行这个gateway/main.rs之前,请先启动src/main.rs启动rust grpc service
  2. 修改app-gw.yaml中的配置内容
app_name: "rs-grpc-gateway"
app_debug: true # 是否开启调试模式
# 该grpc_addr可以是服务的ip地址和端口模式,也可以是k8s命名服务的地址,例如:http://rs-grpc.local.svc:50051
# 运行前请先启动rs-grpc,再运行该gateway
grpc_addr: http://192.168.1.101:50051
monitor_port: 8091
gateway_port: 8080

运行方式如下:

cargo run --bin rs-grpc-gateway

运行效果如下:

Finished dev [unoptimized + debuginfo] target(s) in 0.15s
Running `target/debug/rs-grpc-gateway`
rs-rpc http gateway
current process pid:34744
app run on:127.0.0.1:8080

验证http请求是否生效:

curl --location --request POST 'localhost:8080/v1/greeter/say_hello' \
--header 'Content-Type: application/json' \
--data-raw '{"id":1,"name":"daheige"}'

输出结果如下:

{
  "code": 0,
  "message": "ok",
  "data": {
    "name": "daheige",
    "message": "hello,daheige"
  }
}

http gateway运行机制(图片来自grpc-ecosystem/grpc-gateway):

prometheus metrics

src/main.rs代码片段如下:

// create http /metrics endpoint
let metrics_server = prometheus_init(8091);
let metrics_handler = tokio::spawn(metrics_server);

// create grpc server
let greeter = GreeterImpl::default ();
let grpc_handler = tokio::spawn( async move {
Server::builder()
.add_service(reflection_service)
.add_service(GreeterServiceServer::new(greeter))
.serve_with_shutdown(
address,
infras::shutdown::graceful_shutdown(Duration::from_secs(3)),
)
.await
.expect("failed to start grpc server");
});

// run async tasks by tokio try_join macro
let _ = tokio::try_join!(metrics_handler, grpc_handler);
Ok(())

运行rs-grpc服务端:

cargo run --bin rs-grpc

运行效果如下所示:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.23s
     Running `target/debug/rs-grpc`
grpc server run on:0.0.0.0:50051
prometheus at:0.0.0.0:8090/metrics

请求grpc服务接口:

 grpcurl -d '{"name":"daheige"}' -plaintext 127.0.0.1:50051 Hello.GreeterService.SayHello

此时访问 metrics访问地址:http://localhost:8090/metrics 效果如下图所示: 你可以根据实际情况接入grafana控制控制面板,实时观察prometheus指标。

logger

  1. 日志记录使用env_loggerlog库实现
  2. 如果想在启动时改变日志级别,可以通过指定环境变量启动应用
  3. 日志level 优先级 error > warn > info > debug > trace
  4. 启动方式:RUST_LOG=info cargo run --bin rs-grpc

本地运行方式

rpc服务运行方式如下:

RUST_LOG=info cargo run --bin rs-grpc

rpc服务运行方式如下:

RUST_LOG=info cargo run --bin rs-grpc-gateway

测试环境和线上环境运行方式

rpc服务运行方式如下:

RUST_LOG=info /app/rs-grpc

rpc服务运行方式如下:

RUST_LOG=info /app/rs-grpc-gateway

makefile

为了方便开发和运行,提供了makefile文件,可以快速通过make命令构建docker镜像后,再启动容器。 运行方式如下:

make rpc-build
make rpc-run

如果想重新构建和运行,直接运行make rs-rebuild-run即可。

go grpc framework demo

https://github.com/daheige/hephfx

go grpc http gateway

https://github.com/grpc-ecosystem/grpc-gateway

About

rust grpc tonic usage

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors