0

I'm new with RPC protocols so probably my question may have some conceptual errors.

I'm trying to write a higher level library for cap'n proto for writing both servers/clients, something like tonic but over cap'n proto instead protobuf and runtime agnostic.

I would like have an interface pretty similar to tonic. Something like this for the server side.

use my_lib::server::Server;

use crate::services::{SomeService, SomeService2}; // ...SomeService3, 4, 5 .........

#[async_std::main]
async fn main() ->async_std::io::Result<()> {
    let service_1 = SomeService::new();
    let service_2 = SomeService2::new();

    Server::new()
        .add_service(service_1)
        .add_service(service_2) // add service 3, .. 4 ...  5 ... .... n
        .serve("0.0.0.0:4567").await?;

    Ok(())
}

To can call to whatever be, service_1 or service_2.

But I'm a bit (well actually more than a bit...) stuck.

I reproduced the hello_world example

#[async_std::main]
async fn main() -> async_std::io::Result<()> {
    let hello_world_client: hello_world::Client = capnp_rpc::new_client(HelloWorldImpl);

    let listener = TcpListener::bind("127.0.0.1:50070")
        .await
        .expect("failed to open listener");

    let mut incoming = listener.incoming();

    while let Some(stream) = incoming.next().await {
        let stream = stream.expect("failed to unwrap stream");

        let reader = stream.clone();
        let writer = stream;

        let network = twoparty::VatNetwork::new(
            reader,
            writer,
            rpc_twoparty_capnp::Side::Server,
            Default::default(),
        );

        let rpc_system = RpcSystem::new(Box::new(network), Some(hello_world_client.clone().client));

        async_std::task::spawn_local(rpc_system).await.expect("");
    }

    Ok(())
}

This works well when I run the client. But I'm trying to figure out how to put more than one service in the same server. My intuition tells to me that the Server struct should have a set of Services (but also I have no idea how to represent a service) maybe something like Vec<Service> or maybe HashMap<_?_, Service> so the incoming stream should "be matched" to their own service - But honestly I have no idea.

My first attempt was to simply create a hello_world_2 and have it placed in series next to hello_world, something like this.

And this does not work when you test it, only the client of the service that is placed first works.

I understand that this behavior has to do with the flow of how the stream is processed, when the client of the second service calls the server the only interface that can read is the one of the first service, this brings me back to the beginning of my question, how to handle multiple RPC interfaces on a single server?

I'm sorry if this question is a bit silly, but I don't really understand how to define what a service or route is.

al3x
  • 589
  • 1
  • 4
  • 16

1 Answers1

1

You can declare a separate service which has methods to get all the other services:

interface MainService {
  getFooService @0 () -> (foo :FooService);
  getBarService @1 () -> (bar :BarService);
  # ...
}

Then, make that the main service that you export.

Cap'n Proto allows one service's methods to accept or return other services. You can use Cap'n Proto's Promise Pipelining feature to avoid having to wait for a round trip to the server before you can start making calls on the returned services.

Kenton Varda
  • 41,353
  • 8
  • 121
  • 105
  • Is there a fundamental problem with the approach I am trying to follow? I mean, let's say for one reason or another I don't want to do promise pipelining and I simply want to manage more than one service on my server, is there some reason with the design of cap'n proto that prevents me from doing this? – al3x Jun 26 '23 at 04:16
  • 1
    This is how Cap'n Proto is designed. There is only one main (or "bootstrap") service on the connection. It was designed this way because you can write a main service like above that exports multiple services in any way you want. If you want to have services by string name or by integer index or whatever -- it's up to you, just write a main service that implements a method like `get(string)` or `get(int)`. There's just no reason for Cap'n Proto to have a "built-in" way to do this because doing it with a main service is just as good and much more flexible. – Kenton Varda Jun 26 '23 at 13:32
  • Really Thanks! This is very helpful I'll try it. One question more maybe do you know what are at byte 52 of the input stream? [I'm trying analyze the stream](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=289f3ed4c684ef55c80b282c94c9f14e) – al3x Jun 26 '23 at 16:58
  • I think I found it I guess [here](https://github.com/capnproto/capnproto/blob/master/c++/src/capnp/rpc.capnp) is the specification of how to de-serialize the rpc-message and if I am not wrong I have to analyze the bytes between 52-160 of the input stream and with the result of this analysis I can make the "dispatch" of each service. – al3x Jun 26 '23 at 17:16
  • Yep, rpc.capnp is the specification of the protocol. Note that you can use `capnp compile -ocapnp rpc.capnp` to have the compiler parse the schema and then write it back out again with comments telling you the offsets of every field and other such information. – Kenton Varda Jun 28 '23 at 14:23
  • I realized that analyze the stream will be painful - I just added a extra round trip over the socket with a "id header", but I don't know... I feel I can do it better. [example](https://gitlab.com/is.not-project/is_not_capnp/-/blob/master/examples/hello-world/src/bin/server.rs) – al3x Jun 30 '23 at 04:11