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:
bash
cargo install just
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
RPC Client in Rust and RPC Server in Rust running the example passed to the command
just run-integration {ws|memory|dyn}
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.
```toml [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 ```
Create a file app.proto
to define the messages that will be used, for example:
```proto 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:
```rust 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:
.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 the RpcClient
passing the implementation (struct with the trait implemented) as a generic in the load_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 and RpcClient
requirements . *3#[cfg(feature = "server")]
) A struct in charge of registering your declared service when a RpcServerPort
is created. You should use this struct and its registering function inside the RpcServer
port creation handler. *4To import them you must add:
rust
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.
```rust use dclrpc::{ transports::websocket::{WebSocketServer, WebSocketTransport}, server::{RpcServer, RpcServerPort}, servicemoduledefinition::{Definition, ServiceModuleDefinition, CommonPayload} };
use crate::{ EchoServiceRegistration, // (*4) };
// Define the IP and Port where the WebSocket Server will run let wsserver = WebSocketServer::new("localhost:8080"); // Start listening on that IP:PORT let mut connectionlistener = ws_server.listen().await.unwrap();
// Add here any data that the server needs to solve the messages, for example db. let ctx = MyExampleContext { hardcodeddatabase: createdb(), };
let mut server = RpcServer::create(ctx);
server.sethandler(|port: &mut RpcServerPorthello
message.
EchoServiceRegistration::registerservice(port, echoservice::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 servereventssender = server.getservereventssender(); tokio::spawn(async move { while let Some(Ok(connection)) = connectionlistener.recv().await { let transport = WebSocketTransport::new(connection); match servereventssender.sendattachtransport(transport) { Ok() => { println!("> RpcServer > transport attached successfully"); } Err() => { println!("> RpcServer > unable to attach transport"); panic!() } } } });
server.run().await; ```
Implement the trait for your service
```rust use crate::{ MyExampleContext, EchoServiceServer, // (2) Text // (1) message };
pub struct MyEchoService;
impl EchoServiceServer
Initiate a WebSocket Client Connection and send a Hello World message to the echo server.
```rust use crate::{EchoServiceClient, RPCServiceClient} // (*3) use dclrpc::{transports::websocket::{WebSocketClient, WebSocketTransport}, client::RpcClient}; use ws_rust::Text;
let client_connection = WebSocketClient::connect("ws://localhost:8080") .await .unwrap();
let clienttransport = WebSocketTransport::new(clientconnection); let mut client = RpcClient::new(clienttransport).await.unwrap(); let port = client.createport("echo").await.unwrap();
let module = port.loadmodule::