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

Apache-2.0

180KB
3K SLoC

dcl-rpc

Build License Cargo Documentation

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 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. *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