rust grpc microservices in action project https://github.com/daheige/rs-rpc
tonic https://crates.io/crates/tonic
- rust grpc采用tokio,tonic,tonic-build 和 prost 代码生成,进行构建
- grpc客户端支持go,nodejs,rust等不同语言调用服务端程序
- 支持http gateway模式(http json请求到网关层后,转换为pb message,然后发起grpc service调用
1、下载https://github.com/protocolbuffers/protobuf/archive/v3.15.8.tar.gz
cd /usr/local/src
sudo wget https://github.com/protocolbuffers/protobuf/archive/v3.15.8.tar.gz
2、开始安装
sudo mv v3.15.8.tar.gz protobuf-3.15.8.tar.gz
sudo tar zxvf protobuf-3.15.8.tar.gz
cd protobuf-3.15.8
sudo yum install gcc-c++ cmake libtool
# 对于ubuntu系统 sudo apt install gcc cmake make libtool
$ sudo mkdir /usr/local/protobuf
需要编译, 在新版的 PB 源码中,是不包含 .configure 文件的,需要生成
此时先执行 sudo ./autogen.sh
脚本说明如下:
# Run this script to generate the configure script and other files that will
# be included in the distribution. These files are not checked in because they
# are automatically generated.
此时生成了 .configure 文件,可以开始编译了
sudo ./configure --prefix=/usr/local/protobuf
sudo make && make install
安装完成后,查看版本:
$ cd /usr/local/protobuf/bin
$ ./protoc --version
libprotoc 3.15.8
建立软链接
$ sudo ln -s /usr/local/protobuf/bin/protoc /usr/bin/protoc
$ sudo chmod +x /usr/bin/protoc
brew install automake
brew install libtool
brew install protobuf
cargo new rs-rpc
- 新建src/client.rs
fn main() {}
- 在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::create_dir(out_dir); // 创建目录
// grpc reflection 描述信息这是一个二进制文件
let descriptor_path = out_dir.join("rpc_descriptor.bin");
// 2.生成rust grpc代码
// 指定rust grpc 代码生成的目录
tonic_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;导出模块
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");
// 先清空crates/pb/src/lib.rs文件内容
mod_file.set_len(0).expect("failed to clear mod.rs");
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.into_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(())
}
-
添加依赖 具体见
Cargo.toml
-
cargo run --bin rs-rpc 这一步就会安装好所有的依赖,并构建proto/hello.proto
-
在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(2338);
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(())
}
cargo run --bin rs-rpc
output:
Finished dev [unoptimized + debuginfo] target(s) in 0.18s
Running `target/debug/rs-rpc`
grpc server run on:127.0.0.1:50051
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
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
# 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
composer install
php hello.php daheige
output:
check App\Hello\HelloReq exist
bool(true)
status code: 0
name:daheige
hello,daheige
please gateway/main.go
cd gateway && go build -o gateway && ./gateway
output:
2023/11/18 00:21:29 grpc server endpoint run on: localhost:50051
2023/11/18 00:21:29 http gateway run on: localhost:8090
curl http gateway
curl http://localhost:8090/v1/greeter/say_hello -d '{"name":"daheige"}'
output:
{"name":"daheige", "message":"hello,daheige"}
http gateway运行机制(图片来自grpc-ecosystem/grpc-gateway):
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设置一样
- 安装grpcurl工具
brew install grpcurl
如果你本地安装了golang,那可以直接运行如下命令,安装grpcurl工具
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
- 验证rs-rpc service启动的效果
grpcurl -plaintext 127.0.0.1:50051 list
执行上面的命令,输出结果如下:
Hello.GreeterService
grpc.reflection.v1alpha.ServerReflection
- 查看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 );
}
- 查看请求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;
}
- 查看相应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;
}
- 通过grpcurl调用rpc service method
grpcurl -d '{"name":"daheige"}' -plaintext 127.0.0.1:50051 Hello.GreeterService.SayHello
响应结果如下:
{
"name": "daheige",
"message": "hello,daheige"
}
由于tower steer抽象设计,可以将grpc service转换为axum http Router,所以可以将grpc server和 http gateway在一个端口上运行 src/multiplex.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。
// 运行这个gateway/main.rs之前,请先启动src/main.rs启动rust grpc service
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:8090
验证http请求是否生效:
curl --location --request POST 'localhost:8090/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"
}
}
src/main.rs代码片段如下:
// create http /metrics endpoint
let metrics_server = prometheus_init(2338);
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-rpc服务端:
cargo run --bin rs-rpc
运行效果如下所示:
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.23s
Running `target/debug/rs-rpc`
grpc server run on:0.0.0.0:50051
prometheus at:0.0.0.0:2338/metrics
请求grpc服务接口:
grpcurl -d '{"name":"daheige"}' -plaintext 127.0.0.1:50051 Hello.GreeterService.SayHello
此时访问 metrics访问地址:http://localhost:2338/metrics 效果如下图所示: 你可以根据实际情况接入grafana控制控制面板,实时观察prometheus指标。
https://github.com/daheige/gmicro
https://github.com/daheige/gmicro-demo
https://github.com/grpc-ecosystem/grpc-gateway