Fast and efficient remote procedure call (RPC) framework for C#
- Features
- Installation
- Contract
- Data Contract
- Code generation
- Client setup
- Server setup
- Callbacks
- Basic authentication setup
- SSL Setup
- Prebuilt messages
- High throughput capability (especialy while dealing with lot of small messages)
- Low latency
- Direct and backward calls (callbacks) in one contract
- Optimization for message multicasting
- Support of grpc-like streams (not implemented yet)
- Configurable back-pressure (partially implemented)
This library is distributed via NuGet. We target .NET 4.71 and .NET 5.0. The library code is pure C#.
In order to get things working, you need you need to install the following NuGet packages.
Core library:
Install-Package SharpRpc.Core
Builder library with a code generator to generate messages and stubs for your RPC contract:
Install-Package SharpRpc.Builder
And a serializer library:
Install-Package MessagePack
Note: Only MessagePack is supported by now. More serializers will be added in near future...
To define a contract you need to create a public interface with RpcContract and RpcSerializer attributes.
[RpcContract]
[RpcSerializer(SerializerChoice.MessagePack)]
public interface MyContract
{
[Rpc(RpcType.Call)]
int MyRemoteCall(FooEntity entity, int p1);
[Rpc(RpcType.Message)]
void MyRemoteMessage(int p1, string p2);
}
Each remote call must be marked with Rpc attribute with one of four call types:
Rpc Type | Call Initiator | Description |
---|---|---|
Call | Client | Request from cleint to server. |
Message | Client | One-way message from client to server. |
Callback | Server | Request from server to client. |
CallbackMessage | Server | One-way message from server to client. |
Note: The contract interface is not for direct use and should not be implemented. Its only purpose is to supply metadata for code generation.
If you have objects as call parameters or results, they must be attributed according to specification of serializer you use. For example (MessagePack):
[MessagePackObject]
public class FooEntity
{
[Key(0)]
public string Name { get; set; }
[Key(1)]
public int Age { get; set; }
[Key(2)]
public double Height { get; set; }
}
For each contract class the builder generates corresponding wrapper class which contains several subclasses to facilitate RPC communication. The builder composes this class name from contract name by adding '_Gen' suffix to it.
Class name | Description |
---|---|
<contract_name>_Gen.Client | Instantiate this class to call the service methods. |
<contract_name>_Gen.ServiceBase | Override this class to implements service methods. |
<contract_name>_Gen.CallbackClient | Instance of this class is avaialble as Client property on service side to allow server to callback the client. |
<contract_name>_Gen.CallbackServiceBase | Override this class to implement callback methods. |
Note: CallbackClient and CallbackServiceBase classes are generated only if there is at least one callback or callback message in the contract.
To setup communication from client side, create an endpoint first, than pass the endpoint instance to CreateClient method to create a client stub:
var endpoint = new TcpClientEndpoint("localhost", 812, TcpSecurity.None);
var client = MyContract_Gen.CreateClient(endpoint);
Endpoints carry all communication parameters and settings. Each communication transport has its own endpoint type. E.g. TCP transport uses TcpClientEndpoint and TcpServiceEdnpoint classes. Note: SharpRpc supports only TCP transport by now.
Connection may be initiated directly by calling TryConnectAsync method:
var connectResult = await client.Channel.TryConnectAsync();
// or
var connectResult = client.Channel.TryConnectAsync().Result;
Or inderectly by just calling any generated RPC method:
client.MyRemoteMessage(1, "23");
First, implement service methods by overriding generated <contract_name>_Gen.ServiceBase class:
public class MyContractServiceImpl : MyContract_Gen.ServiceBase
{
public override ValueTask<int> MyRemoteCall(FooEntity entity, int p1)
{
// ...
return new ValueTask<int>();
}
public override ValueTask MyRemoteMessage(int p1, string p2)
{
// ...
return new ValueTask();
}
}
Second, create a RpcServer instance and supply it with a binding and an endpoint:
var endpoint = new TcpServerEndpoint(IPAddress.IPv6Any, 812, TcpServerSecurity.None);
var binding = MyContract_Gen.CreateBinding(() => new MyContractServiceImpl());
var server = new RpcServer(binding);
server.AddEndpoint(endpoint);
Note. You may attach multiple endpoint instances to a single server. E.g., you may have both insecure and secure(SSL) TCP endpoints for a single service.
Third, start the server:
server.Start();
You may stop server at any time by calling server.StopAsync();
Currently, SharpRpc supports only basic login/password for authenticating clients and only certificates for authenticating servers. More authentication methods will be added in the future.
On the client-side create BasicCredentials object and pass it to the endpoint:
var endpoint = new TcpClientEndpoint(address, port, security); endpoint.Credentials = new BasicCredentials("Admin", "zzzz");
Note: It's not advised to use basic authentication together with TcpSecuriot.None option. Login and password should not be send via unprocected transport.
On the server-side create a login/password validator class first:
internal class AuthValidator : PasswordValidator
{
public ValueTask<string> Validate(string userName, string password)
{
if (userName == "Admin" && password == "zzzz")
return ValueTask.FromResult<string>(null);
return ValueTask.FromResult("Invalid credentials.");
}
}
Than update Authenticator filed in the server endpoint:
var tcpEndpoint = new TcpServerEndpoint(IPAddress.IPv6Any, 812, security);
tcpEndpoint.Authenticator = new BasicAuthenticator(new AuthValidator());
If you have multiple endpoints authenticator should be configured separately for every one of them.
The prebuilt messages may be useful in cases when one message is multiple times, e.g. multicasting the same update for multiple clients. Prebuilding a message allows for significantly improved performance by excluding multiple serializations of the same message.
To use prebuild message sepcify EnablePrebuilder=true in the contract attribute:
[RpcServiceContract(EnablePrebuilder = true)]
public interface SomeRpcContract
{
}
This will signal the builder to generate a prebuilder class and overloads which accept prebuilt messages. Only operation contracts with RpcType.Message or RpcType.CallbackMessage allows prebuilding.
Then you may use the prebuilder to pre-serialize messages and send them to multiple clients: