文盘Rust -- tonic-Rust grpc初体验
gRPC 是开发中常用的开源高性能远程过程调用(RPC)框架,tonic 是基于 HTTP/2 的 gRPC 实现,专注于高性能、互操作性和灵活性。该库的创建是为了对 async/await 提供一流的支持,并充当用 Rust 编写的生产系统的核心构建块。今天我们聊聊通过使用tonic 调用grpc的的具体过程。
工程规划
rpc程序一般包含server端和client端,为了方便我们把两个程序打包到一个工程里面 新建tonic_sample工程
cargo new tonic_sample
Cargo.toml 如下
[package]
name = "tonic_sample"
version = "0.1.0"
edition = "2021"
[[bin]] # Bin to run the gRPC server
name = "stream-server"
path = "src/stream_server.rs"
[[bin]] # Bin to run the gRPC client
name = "stream-client"
path = "src/stream_client.rs"
[dependencies]
tokio.workspace = true
tonic = "0.9"
tonic-reflection = "0.9.2"
prost = "0.11"
tokio-stream = "0.1"
async-stream = "0.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rand = "0.7"
h2 = { version = "0.3" }
anyhow = "1.0.75"
futures-util = "0.3.28"
[build-dependencies]
tonic-build = "0.9"
tonic 的示例代码还是比较齐全的,本次我们参考 tonic 的 streaming example。
首先编写 proto 文件,用来描述报文。 proto/echo.proto
syntax = "proto3";
package stream;
// EchoRequest is the request for echo.
message EchoRequest { string message = 1; }
// EchoResponse is the response for echo.
message EchoResponse { string message = 1; }
// Echo is the echo service.
service Echo {
// UnaryEcho is unary echo.
rpc UnaryEcho(EchoRequest) returns (EchoResponse) {}
// ServerStreamingEcho is server side streaming.
rpc ServerStreamingEcho(EchoRequest) returns (stream EchoResponse) {}
// ClientStreamingEcho is client side streaming.
rpc ClientStreamingEcho(stream EchoRequest) returns (EchoResponse) {}
// BidirectionalStreamingEcho is bidi streaming.
rpc BidirectionalStreamingEcho(stream EchoRequest)
returns (stream EchoResponse) {}
}
文件并不复杂,只有两个 message 一个请求一个返回,之所以选择这个示例是因为该示例包含了rpc中的流式处理,包扩了server 流、client 流以及双向流的操作。 编辑build.rs 文件
use std::{env, path::PathBuf};
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("proto/echo.proto")?;
Ok(())
}
该文件用来通过 tonic-build 生成 grpc 的 rust 基础代码
完成上述工作后就可以构建 server 和 client 代码了
stream_server.rs
pub mod pb {
tonic::include_proto!("stream");
}
use anyhow::Result;
use futures_util::FutureExt;
use pb::{EchoRequest, EchoResponse};
use std::{
error::Error,
io::ErrorKind,
net::{SocketAddr, ToSocketAddrs},
pin::Pin,
thread,
time::Duration,
};
use tokio::{
net::TcpListener,
sync::{
mpsc,
oneshot::{self, Receiver, Sender},
Mutex,
},
task::{self, JoinHandle},
};
use tokio_stream::{
wrappers::{ReceiverStream, TcpListenerStream},
Stream, StreamExt,
};
use tonic::{transport::Server, Request, Response, Status, Streaming};
type EchoResult<T> = Result<Response<T>, Status>;
type ResponseStream = Pin<Box<dyn Stream<Item = Result<EchoResponse, Status>> + Send>>;
fn match_for_io_error(err_status: &Status) -> Option<&std::io::Error> {
let mut err: &(dyn Error + 'static) = err_status;
loop {
if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
return Some(io_err);
}
// h2::Error do not expose std::io::Error with `source()`
// https://github.com/hyperium/h2/pull/462
if let Some(h2_err) = err.downcast_ref::<h2::Error>() {
if let Some(io_err) = h2_err.get_io() {
return Some(io_err);
}
}
err = match err.source() {
Some(err) => err,
None => return None,
};
}
}
#[derive(Debug)]
pub struct EchoServer {}
#[tonic::async_trait]
impl pb::echo_server::Echo for EchoServer {
async fn unary_echo(&self, req: Request<EchoRequest>) -> EchoResult<EchoResponse> {
let req_str = req.into_inner().message;
let response = EchoResponse { message: req_str };
Ok(Response::new(response))
}
type ServerStreamingEchoStream = ResponseStream;
async fn server_streaming_echo(
&self,
req: Request<EchoRequest>,
) -> EchoResult<Self::ServerStreamingEchoStream> {
println!("EchoServer::server_streaming_echo");
println!("\tclient connected from: {:?}", req.remote_addr());
// creating infinite stream with requested message
let repeat = std::iter::repeat(EchoResponse {
message: req.into_inner().message,
});
let mut stream = Box::pin(tokio_stream::iter(repeat).throttle(Duration::from_millis(200)));
let (tx, rx) = mpsc::channel(128);
tokio::spawn(async move {
while let Some(item) = stream.next().await {
match tx.send(Result::<_, Status>::Ok(item)).await {
Ok(_) => {
// item (server response) was queued to be send to client
}
Err(_item) => {
// output_stream was build from rx and both are dropped
break;
}
}
}
println!("\tclient disconnected");
});
let output_stream = ReceiverStream::new(rx);
Ok(Response::new(
Box::pin(output_stream) as Self::ServerStreamingEchoStream
))
}
async fn client_streaming_echo(
&self,
_: Request<Streaming<EchoRequest>>,
) -> EchoResult<EchoResponse> {
Err(Status::unimplemented("not implemented"))
}
type BidirectionalStreamingEchoStream = ResponseStream;
async fn bidirectional_streaming_echo(
&self,
req: Request<Streaming<EchoRequest>>,
) -> EchoResult<Self::BidirectionalStreamingEchoStream> {
println!("EchoServer::bidirectional_streaming_echo");
let mut in_stream = req.into_inner();
let (tx, rx) = mpsc::channel(128);
tokio::spawn(async move {
while let Some(result) = in_stream.next().await {
match result {
Ok(v) => tx
.send(Ok(EchoResponse { message: v.message }))
.await
.expect("working rx"),
Err(err) => {
if let Some(io_err) = match_for_io_error(&err) {
if io_err.kind() == ErrorKind::BrokenPipe {
eprintln!("\tclient disconnected: broken pipe");
break;
}
}
match tx.send(Err(err)).await {
Ok(_) => (),
Err(_err) => break, // response was droped
}
}
}
}
println!("\tstream ended");
});
// echo just write the same data that was received
let out_stream = ReceiverStream::new(rx);
Ok(Response::new(
Box::pin(out_stream) as Self::BidirectionalStreamingEchoStream
))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 基础server
let server = EchoServer {};
Server::builder()
.add_service(pb::echo_server::EchoServer::new(server))
.serve("0.0.0.0:50051".to_socket_addrs().unwrap().next().unwrap())
.await
.unwrap();
Ok(())
}
server 端的代码还是比较清晰的,首先通过 tonic::include_proto! 宏引入grpc定义,参数是 proto 文件中定义的 package 。我们重点说说 server_streaming_echo function 。这个function 的处理流程明白了,其他的流式处理大同小异。首先 通过std::iter::repeat function 定义一个迭代器;然后构建 tokio_stream 在本示例中 每 200毫秒产生一个 repeat;最后构建一个 channel ,tx 用来发送从stream中获取的内容太,rx 封装到response 中返回。 最后 main 函数 拉起服务。
client 代码如下
pub mod pb {
tonic::include_proto!("stream");
}
use std::time::Duration;
use tokio_stream::{Stream, StreamExt};
use tonic::transport::Channel;
use pb::{echo_client::EchoClient, EchoRequest};
fn echo_requests_iter() -> impl Stream<Item = EchoRequest> {
tokio_stream::iter(1..usize::MAX).map(|i| EchoRequest {
message: format!("msg {:02}", i),
})
}
async fn unary_echo(client: &mut EchoClient<Channel>, num: usize) {
for i in 0..num {
let req = tonic::Request::new(EchoRequest {
message: "msg".to_string() + &i.to_string(),
});
let resp = client.unary_echo(req).await.unwrap();
println!("resp:{}", resp.into_inner().message);
}
}
async fn streaming_echo(client: &mut EchoClient<Channel>, num: usize) {
let stream = client
.server_streaming_echo(EchoRequest {
message: "foo".into(),
})
.await
.unwrap()
.into_inner();
// stream is infinite - take just 5 elements and then disconnect
let mut stream = stream.take(num);
while let Some(item) = stream.next().await {
println!("\treceived: {}", item.unwrap().message);
}
// stream is droped here and the disconnect info is send to server
}
async fn bidirectional_streaming_echo(client: &mut EchoClient<Channel>, num: usize) {
let in_stream = echo_requests_iter().take(num);
let response = client
.bidirectional_streaming_echo(in_stream)
.await
.unwrap();
let mut resp_stream = response.into_inner();
while let Some(received) = resp_stream.next().await {
let received = received.unwrap();
println!("\treceived message: `{}`", received.message);
}
}
async fn bidirectional_streaming_echo_throttle(client: &mut EchoClient<Channel>, dur: Duration) {
let in_stream = echo_requests_iter().throttle(dur);
let response = client
.bidirectional_streaming_echo(in_stream)
.await
.unwrap();
let mut resp_stream = response.into_inner();
while let Some(received) = resp_stream.next().await {
let received = received.unwrap();
println!("\treceived message: `{}`", received.message);
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut client = EchoClient::connect("http://127.0.0.1:50051").await.unwrap();
println!("Unary echo:");
unary_echo(&mut client, 10).await;
tokio::time::sleep(Duration::from_secs(1)).await;
println!("Streaming echo:");
streaming_echo(&mut client, 5).await;
tokio::time::sleep(Duration::from_secs(1)).await; //do not mess server println functions
// Echo stream that sends 17 requests then graceful end that connection
println!("\r\nBidirectional stream echo:");
bidirectional_streaming_echo(&mut client, 17).await;
// Echo stream that sends up to `usize::MAX` requests. One request each 2s.
// Exiting client with CTRL+C demonstrate how to distinguish broken pipe from
// graceful client disconnection (above example) on the server side.
println!("\r\nBidirectional stream echo (kill client with CTLR+C):");
bidirectional_streaming_echo_throttle(&mut client, Duration::from_secs(2)).await;
Ok(())
}
测试一下,分别运行 server 和 client
cargo run --bin stream-server
cargo run --bin stream-client
在开发中,我们通常不会再 client 和 server都开发好的情况下才开始测试。通常在开发server 端的时候采用 grpcurl 工具进行测试工作
grpcurl -import-path ./proto -proto echo.proto list
grpcurl -import-path ./proto -proto echo.proto describe stream.Echo
grpcurl -plaintext -import-path ./proto -proto echo.proto -d '{"message":"1234"}' 127.0.0.1:50051 stream.Echo/UnaryEcho
此时,如果我们不指定 -import-path 参数,执行如下命令
grpcurl -plaintext 127.0.0.1:50051 list
会出现如下报错信息
Failed to list services: server does not support the reflection API
让服务端程序支持 reflection API
use std::{env, path::PathBuf};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
tonic_build::configure()
.file_descriptor_set_path(out_dir.join("stream_descriptor.bin"))
.compile(&["proto/echo.proto"], &["proto"])
.unwrap();
Ok(())
}
file_descriptor_set_path 生成一个文件,其中包含为协议缓冲模块编码的 prost_types::FileDescriptorSet
文件。这是实现 gRPC 服务器反射所必需的。
接下来改造一下 stream-server.rs,涉及两处更改。
新增 STREAM_DESCRIPTOR_SET 常量
pub mod pb {
tonic::include_proto!("stream");
pub const STREAM_DESCRIPTOR_SET: &[u8] =
tonic::include_file_descriptor_set!("stream_descriptor");
}
修改main函数
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 基础server
// let server = EchoServer {};
// Server::builder()
// .add_service(pb::echo_server::EchoServer::new(server))
// .serve("0.0.0.0:50051".to_socket_addrs().unwrap().next().unwrap())
// .await
// .unwrap();
// tonic_reflection
let service = tonic_reflection::server::Builder::configure()
.register_encoded_file_descriptor_set(pb::STREAM_DESCRIPTOR_SET)
.with_service_name("stream.Echo")
.build()
.unwrap();
let addr = "0.0.0.0:50051".parse().unwrap();
let server = EchoServer {};
Server::builder()
.add_service(service)
.add_service(pb::echo_server::EchoServer::new(server))
.serve(addr)
.await?;
Ok(())
}
register_encoded_file_descriptor_set 将包含编码的 prost_types::FileDescriptorSet
的 byte slice 注册到 gRPC Reflection 服务生成器注册。
再次测试
grpcurl -plaintext 127.0.0.1:50051 list
grpcurl -plaintext 127.0.0.1:50051 describe stream.Echo
返回正确结果。
作者:京东科技 贾世闻
来源:京东云开发者社区 转载请注明来源
文盘Rust -- tonic-Rust grpc初体验的更多相关文章
- Doxygen生成美丽注释文档(1):初体验
Chapter 1 - 准备工作 (Windows环境) 1.1 程序包下载 1. Doxygen * 源码: git clone https://github.com/doxygen/doxygen ...
- .NET Core 微服务之grpc 初体验(干货)
Grpc介绍 GitHub: https://github.com/grpc/grpc gRPC是一个高性能.通用的开源RPC框架,其由Google主要面向移动应用开发并基于HTTP/2协议标准而设计 ...
- ASP.NET Core 3.0 上的gRPC服务模板初体验(多图)
早就听说ASP.NET Core 3.0中引入了gRPC的服务模板,正好趁着家里电脑刚做了新系统,然后装了VS2019的功夫来体验一把.同时记录体验的过程.如果你也想按照本文的步骤体验的话,那你得先安 ...
- node.js 初体验
node.js 初体验 2011-10-31 22:56 by 聂微东, 174545 阅读, 118 评论, 收藏, 编辑 PS: ~ 此篇文章的进阶内容在为<Nodejs初阶之express ...
- Node.js 网页瘸腿爬虫初体验
延续上一篇,想把自己博客的文档标题利用Node.js的request全提取出来,于是有了下面的初哥爬虫,水平有限,这只爬虫目前还有点瘸腿,请看官你指正了. // 内置http模块,提供了http服务器 ...
- .NET平台开源项目速览(15)文档数据库RavenDB-介绍与初体验
不知不觉,“.NET平台开源项目速览“系列文章已经15篇了,每一篇都非常受欢迎,可能技术水平不高,但足够入门了.虽然工作很忙,但还是会抽空把自己知道的,已经平时遇到的好的开源项目分享出来.今天就给大家 ...
- 【腾讯Bugly干货分享】基于 Webpack & Vue & Vue-Router 的 SPA 初体验
本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57d13a57132ff21c38110186 导语 最近这几年的前端圈子,由于 ...
- 在同一个硬盘上安装多个 Linux 发行版及 Fedora 21 、Fedora 22 初体验
在同一个硬盘上安装多个 Linux 发行版 以前对多个 Linux 发行版的折腾主要是在虚拟机上完成.我的桌面电脑性能比较强大,玩玩虚拟机没啥问题,但是笔记本电脑就不行了.要在我的笔记本电脑上折腾多个 ...
- Microsoft IoT Starter Kit 开发初体验
1. 引子 今年6月底,在上海举办的中国国际物联网大会上,微软中国面向中国物联网社区推出了Microsoft IoT Starter Kit ,并且免费开放1000套的申请.申请地址为:http:// ...
- win7升win10,初体验
跟宿舍哥们聊着聊着,聊到最近发布正式版的win10,听网上各种评论,吐槽,撒花的,想想,倒不如自己升级一下看看,反正不喜欢还可以还原.于是就开始了win10的初体验了,像之前装黑苹果双系统一样的兴奋, ...
随机推荐
- filler 抓取手机app的数据,手机wifi设置
1.处于同一局域网下, 2.手机的代服务器修改为手动 3.代理服务器,名称为本机ip地址端口为8888,可以自己设置 4.fillder上面选择允许远程链接
- ORM核心功能之导航属性- EFCore和 SqlSugar
导航属性 导航属性是作为ORM核心功能中的核心,在SqlSugar没有支持导航属性前,都说只是一个高级DbHelper, 经过3年的SqlSugar重构已经拥有了一套 非常成熟的导航属性体系,本文不是 ...
- RALB负载均衡算法的应用
一.背景 搜索推荐算法架构为京东集团所有的搜索推荐业务提供服务,实时返回处理结果给上游.部门各子系统已经实现了基于CPU的自适应限流,但是Client端对Server端的调用依然是RR轮询的方式,没有 ...
- PostgreSQL 性能优化: EXPLAIN 使用教程
使用 EXPLAIN EXPLAIN基础 代价估计 启动开销 总开销 计划结点输出行数 计划结点输出行宽 执行统计 实际启动开销 实际总开销 实际输出行数 实际执行次数 I/O统计 共享块命中数 共享 ...
- nacos连接不上配置的坑
问题: 今天在使用nacos时,发现怎么样都连接不上配置 思路: 毋庸置疑这个肯定是配置问题,下面是我现在的配置 nacos: username: nacos password: nacos serv ...
- 4.3 x64dbg 搜索内存可利用指令
发现漏洞的第一步则是需要寻找到可利用的反汇编指令片段,在某些时候远程缓冲区溢出需要通过类似于jmp esp等特定的反汇编指令实现跳转功能,并以此来执行布置好的ShellCode恶意代码片段,LyScr ...
- ENVI实现QUAC、简化黑暗像元、FLAASH方法的遥感影像大气校正
本文介绍基于ENVI软件,实现对Landsat 7遥感影像加以预处理与多种不同大气校正方法的操作. 目录 1 数据导入与辐射定标 2 波段合成 3 编辑头文件 4 转换文件格式 5 QUAC快速大气校 ...
- Failed to connect to 127.0.0.1 port 1080: Connection refused拒绝连接错误
一.git拒绝连接原因分析 使用git从远程仓库下载代码出现上述的错误是因为使用了proxy代理,所以要解决该问题,核心操作就是要取消代理 二.解决方式 1.查看Linux当前有没有使用代理 通过gi ...
- UE构建基础和实践:四、使用脚本打包
序言 使用UE版本为UE5.20 在实际项目中,我们常常使用自动化脚本来进行构建,它可以增加我们的生产效率,并降低人为操作带来的失误风险. BuildGraph BuildGraph UE官方提供的构 ...
- Ubutnu 20.04 安装和使用单机版hadoop 3.2 [转载]
按照此文档操作,可以一次部署成功:Ubutnu 20.04 安装和使用单机版hadoop 3.2 部署之后,提交测试任务报资源问题.原因是yarn还需要配置,如下: $ cat yarn-site.x ...