Skip to content

RPC over TCP

c80k edited this page May 3, 2020 · 4 revisions

TCP is currently the only transport layer which Capnp.Net.Runtime supports out of the box. If you ever need it, you should be easy to add more transport layers by yourself. It is unlikely that you need to modify Capnp.Net.Runtime code itself for doing that, since the basic building blocks are public. E.g. the test assembly Capnp.Net.Runtime.Tests implements an EnginePair, which establishes a more or less direct bridge between two RpcEngine instances. The rest of this page focuses on the two main TCP actors: TcpRpcClient and TcpRpcServer.

Both classes do exactly what you expect them to do: While TcpRpcServer listens on a TCP port, accepts incoming connections and provides them with a RPC protocol engine, TcpRpcClient establishes a connection to a Cap'n Proto server. Both classes act by default on an unsecured, uncompressed TCP connection, like their C++ counterparts capnp::EzRpcClient and capnp::EzRpcServer do. This means that they are out-of-the-box interoperable with other Cap'n Proto implementations. If you need special features, like compression or TLS, you may do so by injecting an appropriate midlayer (will be described later on).

TcpRpcServer

It is highly recommended to construct a TcpRpcServer instance with the parameterless constructor. This way, you may apply configuration options before starting client acceptance:

using var server = new TcpRpcServer();
// configure
server.AddBuffering();
server.OnConnectionChanged += (s, c) => { ... };
server.Main = new MyInterface();
server.StartAccepting(...);

The snippet above shows TcpRpcServer's extension points in action:

  • Midlayers: AddBuffering() does indeed nothing but injecting a "buffering midlayer". Midlayers will be described later on. Buffering is recommended in almost every scenario (see also the performance considerations).
  • The OnConnectionChanged event, which fires anytime a new connection is accepted or an existing connection is closed. The main purpose of that event is providing user feedback on connection state(s). But it might also be used to implement some basic access control: An event handle might decide to call Close() on a new connection, which will close it immediately, before any RPC message being sent or received over it.

Most importantly, define the server's bootstrap capability by assigning server.Main. The most common pattern is that you derive a class from a generated capability interface and assign 'Mainto an instance of that class. The instance will be kept alive untilTcpRpcServergetsDispose()`'d.

TcpRpcClient

Again, it is highly recommended to construct a TcpRpcClient instance with the parameterless constructor, then configure, then start connecting:

using var client = new TcpRpcClient();
// configure
client.AddBuffering();
client.Connect(...);
var main = client.GetMain<IMyInterface>();

Like with TcpRpcServer, the TcpRpcClient supports midlayers, and buffering is highly recommended. Note that the Connect method has asynchronous behavior (a better name would have been BeginConnect). The Task returned by WhenConnected returns when the connection was established or failed.

Calling GetMain<T>() queries the sever's bootstrap capability and returns a capability for it (whereby T is the bootstrap capability's interface type). Strictly speaking, it returns a promise on the bootstrap capability (see promise pipelining for details). Therefore it is not required to wait for the connection to establish or the query to return. Just go ahead.

Midlayers

Technically, a midlayer is a .NET Stream which wraps around another Stream. Conceptually, it is a layer which is stacked somewhere between the underlying Socket's NetworkStream (at the "bottom") and the FramePump (at the "top"), hence the name. Midlayers provide a very generic and powerful means of customizing an RPC conversation's transport layer behavior. Uses include, but are not limited to the following (disclaimer: The author did not try all of these ideas):

  • Buffering: Indeed, AddBuffering is nothing but a tiny extension method which calls InjectMidlayer with a specific stream adapter.
  • Flow control: You might introduce a custom flow control strategy as discussed in the performance considerations.
  • Compression: What about injecting a GZipStream? Could also be a custom implementation of Cap'n Proto's packing algorithm.
  • Encryption: Go ahead with injecting an SslStream.
  • Robustness testing: Some tests from Capnp.Net.Runtime.Tests inject a ScatteringStream which simulates the behavior of physical networks by limiting the amount of transmitted bytes per Write operation to a maximum transmission unit (MTU). The author hopes not to shock you by confessing that that early Capnp.Net.Runtime versions were not robust when dealing with segmented network packets.
  • Monitoring and statistics

The API for installing a midlayer is the same for both TcpRpcClient and TcpRpcServer:

public void InjectMidlayer(Func<Stream, Stream> createFunc);

It expects a functor that constructs a Stream instance from a given Stream instance. The given stream is conceptually "below" the returned stream, and the returned stream internally wraps around the given stream. Streams are stacked from bottom-up. Thus, the functor passed to the very first InjectMidlayer call receives the underlying Sockets NetworkStream. The functor passed to the second InjectMidlayer call receives the Stream which was returned from the previous functor, and so on.

When a connection gets closed, both TcpRpcClient and TcpRpcServer only close the topmost stream, expecting it to close its wrapped stream, and so on. Many Stream specializations from the .NET library come with a boolean constructor flag leaveOpen, letting the caller decide whether the wrapped stream should be closed automically (false) or not (true). Baring the previous explanation in mind, you should specify such flag with false.

Usage example:

client.InjectMidlayer(s => new GZipStream(s, CompressionLevel.Fastest, false));