通过tonic支持节点间通信(初步

This commit is contained in:
junxiang Mu
2024-08-27 11:40:00 +08:00
parent d183dd6e91
commit 0a373bc260
21 changed files with 1632 additions and 36 deletions
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "protos"
version.workspace = true
edition.workspace = true
[dependencies]
#async-backtrace = { workspace = true, optional = true }
flatbuffers = { workspace = true }
prost = { workspace = true }
protobuf = { workspace = true }
tokio = { workspace = true }
tonic = { workspace = true, features = ["transport", "tls"] }
tower = { workspace = true }
[build-dependencies]
prost-build = { workspace = true }
tonic-build = { workspace = true }
+258
View File
@@ -0,0 +1,258 @@
use std::{
cmp, env, fs,
io::Write,
path::{Path, PathBuf},
process::Command,
};
type AnyError = Box<dyn std::error::Error>;
const ENV_OUT_DIR: &str = "OUT_DIR";
const VERSION_PROTOBUF: Version = Version(27, 0, 0); // 27.0
const VERSION_FLATBUFFERS: Version = Version(24, 3, 25); // 24.3.25
/// Build protos if the major version of `flatc` or `protoc` is greater
/// or lesser than the expected version.
const ENV_BUILD_PROTOS: &str = "BUILD_PROTOS";
/// Path of `flatc` binary.
const ENV_FLATC_PATH: &str = "FLATC_PATH";
fn main() -> Result<(), AnyError> {
let version = protobuf_compiler_version()?;
let need_compile = match version.compare_ext(&VERSION_PROTOBUF) {
Ok(cmp::Ordering::Equal) => true,
Ok(_) => {
let version_err = Version::build_error_message(&version, &VERSION_PROTOBUF).unwrap();
println!("cargo:warning=Tool `protoc` {version_err}, skip compiling.");
false
}
Err(version_err) => {
// return Err(format!("Tool `protoc` {version_err}, please update it.").into());
println!("cargo:warning=Tool `protoc` {version_err}, please update it.");
false
}
};
if !need_compile {
return Ok(());
}
// path of proto file
let project_root_dir = env::current_dir()?;
let proto_dir = project_root_dir.join("src");
let proto_files = &["node.proto"];
let proto_out_dir = project_root_dir.join("src").join("proto_gen");
let flatbuffer_out_dir = project_root_dir.join("src").join("flatbuffers_generated");
let descriptor_set_path = PathBuf::from(env::var(ENV_OUT_DIR).unwrap()).join("proto-descriptor.bin");
tonic_build::configure()
.out_dir(proto_out_dir)
.file_descriptor_set_path(descriptor_set_path)
.protoc_arg("--experimental_allow_proto3_optional")
.compile_well_known_types(true)
.emit_rerun_if_changed(false)
.compile(proto_files, &[proto_dir.clone()])
.map_err(|e| format!("Failed to generate protobuf file: {e}."))?;
// protos/gen/mod.rs
let generated_mod_rs_path = project_root_dir.join("src").join("proto_gen").join("mod.rs");
let mut generated_mod_rs = fs::File::create(generated_mod_rs_path)?;
writeln!(&mut generated_mod_rs, "pub mod node_service;")?;
generated_mod_rs.flush()?;
let generated_mod_rs_path = project_root_dir.join("src").join("lib.rs");
let mut generated_mod_rs = fs::File::create(generated_mod_rs_path)?;
writeln!(&mut generated_mod_rs, "#![allow(unused_imports)]")?;
writeln!(&mut generated_mod_rs, "#![allow(clippy::all)]")?;
writeln!(&mut generated_mod_rs, "pub mod proto_gen;")?;
generated_mod_rs.flush()?;
let flatc_path = match env::var(ENV_FLATC_PATH) {
Ok(path) => {
println!("cargo:warning=Specified flatc path by environment {ENV_FLATC_PATH}={path}");
path
}
Err(_) => "flatc".to_string(),
};
// build src/protos/*.fbs files to src/protos/gen/
compile_flatbuffers_models(
&mut generated_mod_rs,
&flatc_path,
proto_dir.clone(),
flatbuffer_out_dir.clone(),
vec!["models"],
)?;
Ok(())
}
/// Compile proto/**.fbs files.
fn compile_flatbuffers_models<P: AsRef<Path>, S: AsRef<str>>(
generated_mod_rs: &mut fs::File,
flatc_path: &str,
in_fbs_dir: P,
out_rust_dir: P,
mod_names: Vec<S>,
) -> Result<(), AnyError> {
let version = flatbuffers_compiler_version(flatc_path)?;
let need_compile = match version.compare_ext(&VERSION_FLATBUFFERS) {
Ok(cmp::Ordering::Equal) => true,
Ok(_) => {
let version_err = Version::build_error_message(&version, &VERSION_FLATBUFFERS).unwrap();
println!("cargo:warning=Tool `{flatc_path}` {version_err}, skip compiling.");
false
}
Err(version_err) => {
return Err(format!("Tool `{flatc_path}` {version_err}, please update it.").into());
}
};
let fbs_dir = in_fbs_dir.as_ref();
let rust_dir = out_rust_dir.as_ref();
fs::create_dir_all(rust_dir)?;
// $rust_dir/mod.rs
let mut sub_mod_rs = fs::File::create(rust_dir.join("mod.rs"))?;
writeln!(generated_mod_rs)?;
writeln!(generated_mod_rs, "mod flatbuffers_generated;")?;
for mod_name in mod_names.iter() {
let mod_name = mod_name.as_ref();
writeln!(generated_mod_rs, "pub use flatbuffers_generated::{mod_name}::*;")?;
writeln!(&mut sub_mod_rs, "pub mod {mod_name};")?;
if need_compile {
let fbs_file_path = fbs_dir.join(format!("{mod_name}.fbs"));
let output = Command::new(flatc_path)
.arg("-o")
.arg(rust_dir)
.arg("--rust")
.arg("--gen-mutable")
.arg("--gen-onefile")
.arg("--gen-name-strings")
.arg("--filename-suffix")
.arg("")
.arg(&fbs_file_path)
.output()
.map_err(|e| format!("Failed to execute process of flatc: {e}"))?;
if !output.status.success() {
return Err(format!(
"Failed to generate file '{}' by flatc(path: '{flatc_path}'): {}.",
fbs_file_path.display(),
String::from_utf8_lossy(&output.stderr),
)
.into());
}
}
}
generated_mod_rs.flush()?;
sub_mod_rs.flush()?;
Ok(())
}
/// Run command `flatc --version` to get the version of flatc.
///
/// ```ignore
/// $ flatc --version
/// flatc version 24.3.25
/// ```
fn flatbuffers_compiler_version(flatc_path: impl AsRef<Path>) -> Result<Version, String> {
let flatc_path = flatc_path.as_ref();
Version::try_get(format!("{}", flatc_path.display()), |output| {
const PREFIX_OF_VERSION: &str = "flatc version ";
let output = output.trim();
if let Some(version) = output.strip_prefix(PREFIX_OF_VERSION) {
Ok(version.to_string())
} else {
Err(format!("Failed to get flatc version: {output}"))
}
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct Version(u32, u32, u32);
impl Version {
fn try_get<F: FnOnce(&str) -> Result<String, String>>(exe: String, output_to_version_string: F) -> Result<Self, String> {
let cmd = format!("{exe} --version");
let output = std::process::Command::new(exe)
.arg("--version")
.output()
.map_err(|e| format!("Failed to execute `{cmd}`: {e}",))?;
let output_utf8 = String::from_utf8(output.stdout).map_err(|e| {
let output_lossy = String::from_utf8_lossy(e.as_bytes());
format!("Command `{cmd}` returned invalid UTF-8('{output_lossy}'): {e}")
})?;
if output.status.success() {
let version_string = output_to_version_string(&output_utf8)?;
Ok(version_string.parse::<Self>()?)
} else {
Err(format!("Failed to get version by command `{cmd}`: {output_utf8}"))
}
}
fn build_error_message(version: &Self, expected: &Self) -> Option<String> {
match version.compare_major_version(expected) {
cmp::Ordering::Equal => None,
cmp::Ordering::Greater => Some(format!("version({version}) is greater than version({expected})")),
cmp::Ordering::Less => Some(format!("version({version}) is lesser than version({expected})")),
}
}
fn compare_ext(&self, expected_version: &Self) -> Result<cmp::Ordering, String> {
match env::var(ENV_BUILD_PROTOS) {
Ok(build_protos) => {
if build_protos.is_empty() || build_protos == "0" {
Ok(self.compare_major_version(expected_version))
} else {
match self.compare_major_version(expected_version) {
cmp::Ordering::Equal => Ok(cmp::Ordering::Equal),
_ => Err(Self::build_error_message(self, expected_version).unwrap()),
}
}
}
Err(_) => Ok(self.compare_major_version(expected_version)),
}
}
fn compare_major_version(&self, other: &Self) -> cmp::Ordering {
self.0.cmp(&other.0)
}
}
impl std::str::FromStr for Version {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut version = [0_u32; 3];
for (i, v) in s.split('.').take(3).enumerate() {
version[i] = v.parse().map_err(|e| format!("Failed to parse version string '{s}': {e}"))?;
}
Ok(Version(version[0], version[1], version[2]))
}
}
impl std::fmt::Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}.{}", self.0, self.1, self.2)
}
}
/// Run command `protoc --version` to get the version of flatc.
///
/// ```ignore
/// $ protoc --version
/// libprotoc 27.0
/// ```
fn protobuf_compiler_version() -> Result<Version, String> {
Version::try_get("protoc".to_string(), |output| {
const PREFIX_OF_VERSION: &str = "libprotoc ";
let output = output.trim();
if let Some(version) = output.strip_prefix(PREFIX_OF_VERSION) {
Ok(version.to_string())
} else {
Err(format!("Failed to get protoc version: {output}"))
}
})
}
@@ -0,0 +1 @@
pub mod models;
@@ -0,0 +1,124 @@
// automatically generated by the FlatBuffers compiler, do not modify
// @generated
use core::cmp::Ordering;
use core::mem;
extern crate flatbuffers;
use self::flatbuffers::{EndianScalar, Follow};
#[allow(unused_imports, dead_code)]
pub mod models {
use core::cmp::Ordering;
use core::mem;
extern crate flatbuffers;
use self::flatbuffers::{EndianScalar, Follow};
pub enum PingBodyOffset {}
#[derive(Copy, Clone, PartialEq)]
pub struct PingBody<'a> {
pub _tab: flatbuffers::Table<'a>,
}
impl<'a> flatbuffers::Follow<'a> for PingBody<'a> {
type Inner = PingBody<'a>;
#[inline]
unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner {
Self {
_tab: flatbuffers::Table::new(buf, loc),
}
}
}
impl<'a> PingBody<'a> {
pub const VT_PAYLOAD: flatbuffers::VOffsetT = 4;
pub const fn get_fully_qualified_name() -> &'static str {
"models.PingBody"
}
#[inline]
pub unsafe fn init_from_table(table: flatbuffers::Table<'a>) -> Self {
PingBody { _tab: table }
}
#[allow(unused_mut)]
pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr, A: flatbuffers::Allocator + 'bldr>(
_fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr, A>,
args: &'args PingBodyArgs<'args>,
) -> flatbuffers::WIPOffset<PingBody<'bldr>> {
let mut builder = PingBodyBuilder::new(_fbb);
if let Some(x) = args.payload {
builder.add_payload(x);
}
builder.finish()
}
#[inline]
pub fn payload(&self) -> Option<flatbuffers::Vector<'a, u8>> {
// Safety:
// Created from valid Table for this object
// which contains a valid value in this slot
unsafe {
self._tab
.get::<flatbuffers::ForwardsUOffset<flatbuffers::Vector<'a, u8>>>(PingBody::VT_PAYLOAD, None)
}
}
}
impl flatbuffers::Verifiable for PingBody<'_> {
#[inline]
fn run_verifier(v: &mut flatbuffers::Verifier, pos: usize) -> Result<(), flatbuffers::InvalidFlatbuffer> {
use self::flatbuffers::Verifiable;
v.visit_table(pos)?
.visit_field::<flatbuffers::ForwardsUOffset<flatbuffers::Vector<'_, u8>>>("payload", Self::VT_PAYLOAD, false)?
.finish();
Ok(())
}
}
pub struct PingBodyArgs<'a> {
pub payload: Option<flatbuffers::WIPOffset<flatbuffers::Vector<'a, u8>>>,
}
impl<'a> Default for PingBodyArgs<'a> {
#[inline]
fn default() -> Self {
PingBodyArgs { payload: None }
}
}
pub struct PingBodyBuilder<'a: 'b, 'b, A: flatbuffers::Allocator + 'a> {
fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a, A>,
start_: flatbuffers::WIPOffset<flatbuffers::TableUnfinishedWIPOffset>,
}
impl<'a: 'b, 'b, A: flatbuffers::Allocator + 'a> PingBodyBuilder<'a, 'b, A> {
#[inline]
pub fn add_payload(&mut self, payload: flatbuffers::WIPOffset<flatbuffers::Vector<'b, u8>>) {
self.fbb_
.push_slot_always::<flatbuffers::WIPOffset<_>>(PingBody::VT_PAYLOAD, payload);
}
#[inline]
pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a, A>) -> PingBodyBuilder<'a, 'b, A> {
let start = _fbb.start_table();
PingBodyBuilder {
fbb_: _fbb,
start_: start,
}
}
#[inline]
pub fn finish(self) -> flatbuffers::WIPOffset<PingBody<'a>> {
let o = self.fbb_.end_table(self.start_);
flatbuffers::WIPOffset::new(o.value())
}
}
impl core::fmt::Debug for PingBody<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let mut ds = f.debug_struct("PingBody");
ds.field("payload", &self.payload());
ds.finish()
}
}
} // pub mod models
+6
View File
@@ -0,0 +1,6 @@
#![allow(unused_imports)]
#![allow(clippy::all)]
pub mod proto_gen;
mod flatbuffers_generated;
pub use flatbuffers_generated::models::*;
+5
View File
@@ -0,0 +1,5 @@
namespace models;
table PingBody {
payload: [ubyte];
}
+19
View File
@@ -0,0 +1,19 @@
syntax = "proto3";
package node_service;
/* -------------------------------------------------------------------- */
message PingRequest {
uint64 version = 1;
bytes body = 2;
}
message PingResponse {
uint64 version = 1;
bytes body = 2;
}
/* -------------------------------------------------------------------- */
service NodeService {
rpc Ping(PingRequest) returns (PingResponse) {};
}
+1
View File
@@ -0,0 +1 @@
pub mod node_service;
+250
View File
@@ -0,0 +1,250 @@
// This file is @generated by prost-build.
/// --------------------------------------------------------------------
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PingRequest {
#[prost(uint64, tag = "1")]
pub version: u64,
#[prost(bytes = "vec", tag = "2")]
pub body: ::prost::alloc::vec::Vec<u8>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PingResponse {
#[prost(uint64, tag = "1")]
pub version: u64,
#[prost(bytes = "vec", tag = "2")]
pub body: ::prost::alloc::vec::Vec<u8>,
}
/// Generated client implementations.
pub mod node_service_client {
#![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)]
use tonic::codegen::http::Uri;
use tonic::codegen::*;
#[derive(Debug, Clone)]
pub struct NodeServiceClient<T> {
inner: tonic::client::Grpc<T>,
}
impl NodeServiceClient<tonic::transport::Channel> {
/// Attempt to create a new client by connecting to a given endpoint.
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
where
D: TryInto<tonic::transport::Endpoint>,
D::Error: Into<StdError>,
{
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
Ok(Self::new(conn))
}
}
impl<T> NodeServiceClient<T>
where
T: tonic::client::GrpcService<tonic::body::BoxBody>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + Send,
{
pub fn new(inner: T) -> Self {
let inner = tonic::client::Grpc::new(inner);
Self { inner }
}
pub fn with_origin(inner: T, origin: Uri) -> Self {
let inner = tonic::client::Grpc::with_origin(inner, origin);
Self { inner }
}
pub fn with_interceptor<F>(inner: T, interceptor: F) -> NodeServiceClient<InterceptedService<T, F>>
where
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
Response = http::Response<<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody>,
>,
<T as tonic::codegen::Service<http::Request<tonic::body::BoxBody>>>::Error: Into<StdError> + Send + Sync,
{
NodeServiceClient::new(InterceptedService::new(inner, interceptor))
}
/// Compress requests with the given encoding.
///
/// This requires the server to support it otherwise it might respond with an
/// error.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.send_compressed(encoding);
self
}
/// Enable decompressing responses.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.accept_compressed(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_decoding_message_size(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_encoding_message_size(limit);
self
}
pub async fn ping(
&mut self,
request: impl tonic::IntoRequest<super::PingRequest>,
) -> std::result::Result<tonic::Response<super::PingResponse>, tonic::Status> {
self.inner
.ready()
.await
.map_err(|e| tonic::Status::new(tonic::Code::Unknown, format!("Service was not ready: {}", e.into())))?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static("/node_service.NodeService/Ping");
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("node_service.NodeService", "Ping"));
self.inner.unary(req, path, codec).await
}
}
}
/// Generated server implementations.
pub mod node_service_server {
#![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)]
use tonic::codegen::*;
/// Generated trait containing gRPC methods that should be implemented for use with NodeServiceServer.
#[async_trait]
pub trait NodeService: Send + Sync + 'static {
async fn ping(
&self,
request: tonic::Request<super::PingRequest>,
) -> std::result::Result<tonic::Response<super::PingResponse>, tonic::Status>;
}
#[derive(Debug)]
pub struct NodeServiceServer<T: NodeService> {
inner: Arc<T>,
accept_compression_encodings: EnabledCompressionEncodings,
send_compression_encodings: EnabledCompressionEncodings,
max_decoding_message_size: Option<usize>,
max_encoding_message_size: Option<usize>,
}
impl<T: NodeService> NodeServiceServer<T> {
pub fn new(inner: T) -> Self {
Self::from_arc(Arc::new(inner))
}
pub fn from_arc(inner: Arc<T>) -> Self {
Self {
inner,
accept_compression_encodings: Default::default(),
send_compression_encodings: Default::default(),
max_decoding_message_size: None,
max_encoding_message_size: None,
}
}
pub fn with_interceptor<F>(inner: T, interceptor: F) -> InterceptedService<Self, F>
where
F: tonic::service::Interceptor,
{
InterceptedService::new(Self::new(inner), interceptor)
}
/// Enable decompressing requests with the given encoding.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.accept_compression_encodings.enable(encoding);
self
}
/// Compress responses with the given encoding, if the client supports it.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.send_compression_encodings.enable(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.max_decoding_message_size = Some(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.max_encoding_message_size = Some(limit);
self
}
}
impl<T, B> tonic::codegen::Service<http::Request<B>> for NodeServiceServer<T>
where
T: NodeService,
B: Body + Send + 'static,
B::Error: Into<StdError> + Send + 'static,
{
type Response = http::Response<tonic::body::BoxBody>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/node_service.NodeService/Ping" => {
#[allow(non_camel_case_types)]
struct PingSvc<T: NodeService>(pub Arc<T>);
impl<T: NodeService> tonic::server::UnaryService<super::PingRequest> for PingSvc<T> {
type Response = super::PingResponse;
type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>;
fn call(&mut self, request: tonic::Request<super::PingRequest>) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move { <T as NodeService>::ping(&inner, request).await };
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = PingSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(accept_compression_encodings, send_compression_encodings)
.apply_max_message_size_config(max_decoding_message_size, max_encoding_message_size);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => Box::pin(async move {
Ok(http::Response::builder()
.status(200)
.header("grpc-status", tonic::Code::Unimplemented as i32)
.header(http::header::CONTENT_TYPE, tonic::metadata::GRPC_CONTENT_TYPE)
.body(empty_body())
.unwrap())
}),
}
}
}
impl<T: NodeService> Clone for NodeServiceServer<T> {
fn clone(&self) -> Self {
let inner = self.inner.clone();
Self {
inner,
accept_compression_encodings: self.accept_compression_encodings,
send_compression_encodings: self.send_compression_encodings,
max_decoding_message_size: self.max_decoding_message_size,
max_encoding_message_size: self.max_encoding_message_size,
}
}
}
impl<T: NodeService> tonic::server::NamedService for NodeServiceServer<T> {
const NAME: &'static str = "node_service.NodeService";
}
}