15 releases (stable)
2.3.5 | May 24, 2023 |
---|---|
2.3.4 | May 16, 2023 |
2.0.1 | Apr 28, 2023 |
1.0.0 | Mar 21, 2023 |
0.1.3 | Feb 28, 2023 |
#46 in WebSocket
180KB
3K
SLoC
dcl-rpc
The Rust implementation of Decentraland RPC. At Decentraland, we have our own implementation of RPC for communications between the different services.
Currently, there are other implementations:
Requirements
- Install Just
Install Just for commands
cargo install just
Examples
Run the integration example
RPC Client in Rust and RPC Server in Rust running Websocket transport example, Memory Transport example and example using different types of transports
just run-integration
Run the integration example with an specific transport
RPC Client in Rust and RPC Server in Rust running the example passed to the command
just run-integration {ws|memory|dyn}
Run the multi language integration example
RPC Client in Typescript and RPC Server in Rust using WebSockets
just run-multilang
You can find the code for these examples in the examples/
directory.
Usage
Import
[dependencies]
dcl-rpc = "*"
[build-dependencies]
prost-build = "*"
dcl-rpc = "*" # As a build depency as well because we need the codegen module for the code-generation of the defined RPC Service in the .proto
Protobuf
Create a file app.proto
to define the messages that will be used, for example:
syntax = "proto3";
package decentraland.echo;
message Text {
string say_something = 1;
}
service EchoService {
rpc Hello(Text) returns (Text) {}
}
Then, define a build.rs
file to build the types of the message:
use std::io::Result;
fn main() -> Result<()> {
// Tell Cargo that if the given file changes, to rerun this build script.
println!("cargo:rerun-if-changed=src/echo.proto");
let mut conf = prost_build::Config::new();
conf.service_generator(Box::new(dcl_rpc::codegen::RPCServiceGenerator::new()));
conf.compile_protos(&["src/echo.proto"], &["src"])?;
Ok(())
}
The build.rs
script runs every time that your .proto
changes. The script will generate a file in the OUT_DIR
, named as the package
field in the .proto
file (if it's not declared, the name will be '_.rs'). This file will include:
- All your declared messages in the
.proto
as Rust structs. *1 - (
#[cfg(feature = "server")]
) A trait, named{YOUR_RPC_SERVICE_NAME}Server: Send + Sync + 'static
, with the methods defined in your service for the server side. So you should use this trait to build an implementation with the business logic. *2 - (
#[cfg(feature = "client")]
) A trait, named{YOUR_RPC_SERVICE_NAME}ClientDefinition<T: Transport + 'static>: ServiceClient<T> + Send + Sync + 'static
, and an implementation of it for the client side, named{YOUR_RPC_SERVICE_NAME}Client
. You could use this auto-generated implementation when using theRpcClient
passing the implementation (struct with the trait implemented) as a generic in theload_module
function, which it'll be in charge of requesting the procedures of your service. But you could also have your own implementation of the{YOUR_RPC_SERVICE_NAME}ClientDefinition
trait, as long as the implementations meets with trait's andRpcClient
requirements . *3 - (
#[cfg(feature = "server")]
) A struct in charge of registering your declared service when aRpcServerPort
is created. You should use this struct and its registering function inside theRpcServer
port creation handler. *4
To import them you must add:
include!(concat!(env!("OUT_DIR"), "/decentraland.echo.rs"));
This statement should be added to the src/lib.rs
in order to make the auto-generated code part of your crate, otherwise it will treat every include as different types.
Server Side
use dcl_rpc::{
transports::web_socket::{WebSocketServer, WebSocketTransport},
server::{RpcServer, RpcServerPort},
service_module_definition::{Definition, ServiceModuleDefinition, CommonPayload}
};
use crate::{
EchoServiceRegistration, // (*4)
};
// Define the IP and Port where the WebSocket Server will run
let ws_server = WebSocketServer::new("localhost:8080");
// Start listening on that IP:PORT
let mut connection_listener = ws_server.listen().await.unwrap();
// Add here any data that the server needs to solve the messages, for example db.
let ctx = MyExampleContext {
hardcoded_database: create_db(),
};
let mut server = RpcServer::create(ctx);
server.set_handler(|port: &mut RpcServerPort<MyExampleContext>| {
// The EchoServiceRegistration will be autogenerated, so you'll need to define the echo_service, which will have all the behaviors of your service. Following the example, it'll have the logic for the `hello` message.
EchoServiceRegistration::register_service(port, echo_service::MyEchoService {})
});
// The WebSocket Server listens for incoming connections, when a connection is established, it creates a new WebSocketTransport with that connection and attaches it to the server event sender. The loop continues to listen for incoming connections and attach transports until it is stopped.
// and keep waiting for new ones
let server_events_sender = server.get_server_events_sender();
tokio::spawn(async move {
while let Some(Ok(connection)) = connection_listener.recv().await {
let transport = WebSocketTransport::new(connection);
match server_events_sender.send_attach_transport(transport) {
Ok(_) => {
println!("> RpcServer > transport attached successfully");
}
Err(_) => {
println!("> RpcServer > unable to attach transport");
panic!()
}
}
}
});
server.run().await;
Implement the trait for your service
use crate::{
MyExampleContext,
EchoServiceServer, // (*2)
Text // (*1) message
};
pub struct MyEchoService;
#[async_trait::async_trait]
impl EchoServiceServer<MyExampleContext> for MyEchoService {
async fn hello(&self, request: Text, _ctx: Arc<MyExampleContext>) -> Text {
request
}
}
Client Side
Initiate a WebSocket Client Connection and send a Hello World message to the echo server.
use crate::{EchoServiceClient, RPCServiceClient} // (*3)
use dcl_rpc::{transports::web_socket::{WebSocketClient, WebSocketTransport}, client::RpcClient};
use ws_rust::Text;
let client_connection = WebSocketClient::connect("ws://localhost:8080")
.await
.unwrap();
let client_transport = WebSocketTransport::new(client_connection);
let mut client = RpcClient::new(client_transport).await.unwrap();
let port = client.create_port("echo").await.unwrap();
let module = port.load_module::<EchoServiceClient>("EchoService").await.unwrap();
let response = module.hello(Text { say_something: "Hello World!".to_string()}).await;
Dependencies
~11–24MB
~356K SLoC