Snet.Rpc Documentation
๐ Overview
Snet.Rpc is a high-performance RPC (Remote Procedure Call) framework built on DotNetty for .NET 8 and .NET 10. It supports both inter-process communication and distributed remote calls, making it ideal for microservice architectures, distributed computing, and internal service communication.
Key Features:
- Async I/O via DotNetty -- High-concurrency, non-blocking TCP transport with boss/worker event loop groups
- Service Registration & Exposure -- Bind interfaces to implementations with a single method call
- Client Dynamic Proxy -- Transparent remote method invocation using
DynamicObject-based proxies (powered by ImpromptuInterface) - Pluggable Serialization -- JSON-based serialization by default with Newtonsoft.Json and System.Text.Json support
- Built-in Authentication -- Username/password, unique identifier (ISn), and interface list verification on connect
- Bidirectional RPC -- Both client and server can invoke methods on each other through registered channels
| Property | Value |
|---|---|
| Package | Snet.Rpc |
| Namespace | Snet.Rpc |
| Target Frameworks | .NET 8.0 / .NET 10.0 |
| License | MIT |
| Dependencies | DotNetty.Codecs 0.7.6, ImpromptuInterface 8.0.6, Snet.Core |
โถ๏ธ Quick Start
Sample projects are included in the repository:
Snet.RPC.Service.SamplesandSnet.RPC.Client.Samples.
Server:
using Snet.Rpc.service;
var rpcService = RpcService.Instance(new Snet.Rpc.data.Service.Basics
{
Port = 6688,
TimeOut = 1000,
UserName = "snet",
Password = "snet",
Infos = new List<Snet.Rpc.data.Service.Info>
{
new()
{
ISn = "RPC1",
INs = new() { new() { INames = "IHello" } }
}
}
});
rpcService.Open();
rpcService.Register<IHello, Hello>();
// Bidirectional call: server invokes client methods
IHello proxy = rpcService.Create<IHello>();
proxy.Kitty(new Test { aaaa = "Server -> Client", bbbb = DateTime.Now.ToString() });
Console.WriteLine(proxy.Get());
Client:
using Snet.Rpc.client;
var rpcClient = RpcClient.Instance(new Snet.Rpc.data.Client.Basics
{
IpAddress = "127.0.0.1",
Port = 6688,
TimeOut = 1000,
UserName = "snet",
Password = "snet",
ISn = "RPC1",
INs = new() { new() { INames = "IHello" } }
});
rpcClient.Open();
rpcClient.Register<IHello, Hello>();
// Remote method invocation via dynamic proxy
IHello proxy = rpcClient.Create<IHello>();
proxy.Kitty(new Test { aaaa = "Client -> Server", bbbb = DateTime.Now.ToString() });
Console.WriteLine(proxy.Get());
Shared Interface & Implementation:
public class Test
{
public string aaaa { get; set; }
public string bbbb { get; set; }
}
public interface IHello
{
void Kitty(Test test);
string Get();
}
public class Hello : IHello
{
public string Get() => "Hello from: " + DateTime.Now.ToString();
public void Kitty(Test test) => Console.WriteLine(test.aaaa);
}
โ๏ธ Installation & Configuration
Install via NuGet:
dotnet add package Snet.Rpc
Client Configuration -- Client.Basics:
| Property | Type | Default | Description |
|---|---|---|---|
IpAddress |
string |
"127.0.0.1" |
Remote server IP address |
Port |
int |
6688 |
Remote server port |
TimeOut |
int |
1000 |
Timeout in milliseconds |
UserName |
string |
"ysai" |
Authentication username |
Password |
string |
"ysai" |
Authentication password |
ISn |
string |
"888888" |
Unique client identifier |
INs |
List<Details> |
new List<Details>() |
Registered interface names |
Server Configuration -- Service.Basics:
| Property | Type | Default | Description |
|---|---|---|---|
Port |
int |
6688 |
Listening port |
TimeOut |
int |
1000 |
Timeout in milliseconds |
UserName |
string |
"snet" |
Authentication username |
Password |
string |
"snet" |
Authentication password |
Infos |
List<Info> |
new List<Info>() |
Authorized client information |
Server Configuration -- Service.Info:
| Property | Type | Default | Description |
|---|---|---|---|
ISn |
string |
"888888" |
Unique client identifier |
INs |
List<Details> |
new List<Details>() |
Permitted interface names |
Interface Details -- Details:
| Property | Type | Default | Description |
|---|---|---|---|
INames |
string |
"Interface Name" |
Interface name for registration/authentication |
๐ง Core Concepts
1. Dynamic Proxy Pattern
The Proxy class extends System.Dynamic.DynamicObject and uses ImpromptuInterface to create transparent RPC proxies. When a method is called on the proxy, TryInvokeMember intercepts the call, serializes the method name and arguments into a Request, sends it over the DotNetty channel, blocks until the Response arrives, and deserializes the return value.
Proxies are cached in a ConcurrentDictionary<string, object> keyed by interface name for reuse. The Proxy class itself uses a thread-safe double-check locking singleton pattern.
2. Request-Response Model
Request Response
+------------------+ +------------------+
| TAG = request | | TAG = response |
| IName = "IHello" | -------> | info = "success" |
| MName = "Get" | <------- | status = true |
| Params = [...] | | data = "result" |
+------------------+ | time = "2026..." |
+------------------+
Request: Carries the interface name (IName), method name (MName), and parameter list (Params)Response: Returns result information (info), success status (status), return data (data), and server timestamp (time)
3. TCP Transport with Length-Field Framing
DotNetty provides the TCP transport layer with a custom pipeline:
LengthFieldPrepender(8)-- Prepends an 8-byte length header before each outgoing frameLengthFieldBasedFrameDecoder(int.MaxValue, 0, 8, 0, 8)-- Decodes incoming frames by reading the 8-byte length header, then consuming the payload
The pipeline composition:
[LengthFieldPrepender] -> [LengthFieldBasedFrameDecoder] -> [Handler]
Client: RpcClientHandler delegates to RpcClient.Response() and RpcClient.Exception()
Server: RpcServiceHandler delegates to RpcService.Response() and RpcService.Exception()
4. Authentication Flow
On OpenAsync(), the client sends an Authentication message containing:
| Field | Description |
|---|---|
UserName |
Username string |
Password |
Password string |
ISn |
Unique client identifier |
INs |
List of interfaces the client can provide |
The server validates in this order:
- Username and password match server configuration
- The
ISnexists in the server'sInfoslist - The
INslist matches the server's registered interfaces
On success, the server responds with a Message { State = true, Info = "Authentication successful" }. On failure, the server sends an error message and closes the channel.
After authentication, the server adds the client's channel to its ConcurrentDictionary<List<Details>, IChannel> for future bidirectional calls.
5. Bidirectional RPC
Both RpcClient and RpcService implement IRpc, meaning either side can:
- Register interfaces and their implementations
- Create proxies for remote interfaces
- Handle incoming requests via reflection-based method invocation
The server identifies the target client channel by matching the interface name against its authenticated clients dictionary, enabling server-to-client RPC calls.
6. Singleton Pattern via CoreUnify<T,B>
Both RpcClient and RpcService inherit from CoreUnify<T, B> (from Snet.Core), which provides:
- Thread-safe singleton instance management via
Instance(Basics)factory method - Synchronous convenience wrappers:
Open(),Register<I,O>(),Close() - Async operation lifecycle:
BegOperateAsync()/EndOperateAsync() - Event-based logging and information notifications
IDisposable/IAsyncDisposableimplementation
๐ API Reference
Interface IRpc
| Method | Signature | Description |
|---|---|---|
RegisterAsync |
Task<OperateResult> RegisterAsync<I, O>(CancellationToken token = default) |
Register an interface-implementation mapping for remote invocation |
OpenAsync |
Task<OperateResult> OpenAsync(CancellationToken token = default) |
Start the connection (client connects / server binds) |
CloseAsync |
Task<OperateResult> CloseAsync(bool hardClose = false, CancellationToken token = default) |
Stop the connection gracefully or forcefully |
Create |
T Create<T>() where T : class |
Create a dynamic proxy for transparent remote method calls |
Response |
void Response(IByteBuffer data, IChannel channel) |
Handle incoming data from a DotNetty channel |
Exception |
void Exception(Exception ex) |
Handle exceptions from the transport layer |
Class RpcClient
Inherits CoreUnify<RpcClient, Client.Basics>, implements IRpc.
- Uses DotNetty
Bootstrapwith a singleMultithreadEventLoopGroup - Sends
AuthenticationonOpenAsync(), awaits serverMessageresponse - Maintains
ConcurrentDictionary<string, object>for proxy caching - Maintains
Dictionary<string, Type>for interface-to-implementation mapping - Dispatches incoming data by message
TAGtype: request, response, authentication, message - Handles incoming
requestmessages via reflection-based method invocation (like a server)
Class RpcService
Inherits CoreUnify<RpcService, Service.Basics>, implements IRpc.
- Uses DotNetty
ServerBootstrapwith boss + workerMultithreadEventLoopGroups - Maintains
ConcurrentDictionary<List<Details>, IChannel>of authenticated client channels - Validates authentication in three stages: credentials, ISn existence, interface list comparison
- Reflection-based method invocation:
Activator.CreateInstance()+MethodInfo.Invoke() - Automatic parameter type conversion via JSON round-trip (
JsonConvert.SerializeObjectthenDeserializeObject)
Data Models
| Class | Namespace | Properties | Description |
|---|---|---|---|
Request |
Snet.Rpc.data |
TAG (Types), IName (string), MName (string), Params (List<object>) |
Remote method call request |
Response |
Snet.Rpc.data |
TAG (Types), info (string), status (bool), data (object?), time (string) |
Remote method call result |
Authentication |
Snet.Rpc.data |
TAG (Types), UserName (string), Password (string), ISn (string), INs (List<Details>) |
Client authentication payload |
Message |
Snet.Rpc.data |
TAG (Types), Info (string), State (bool), Time (string) |
Notification/status message |
Type |
Snet.Rpc.data |
TAG (Types) |
Base class for TAG-based message discrimination |
Types (enum) |
Snet.Rpc.data |
request, response, authentication, message |
Message type enumeration |
Details |
Snet.Rpc.data |
INames (string) |
Interface name descriptor |
ProxyData.Basics |
Snet.Rpc.data |
Main (object), channel (IChannel), iName (string), type (System.Type), Await (Await) |
Proxy initialization data |
AwaitData |
Snet.Rpc.data |
WaitHandler (AutoResetEvent), resultData (string) |
Synchronous wait container |
Client.Basics |
Snet.Rpc.data |
IpAddress, Port, TimeOut, UserName, Password, ISn, INs |
Client configuration |
Service.Basics |
Snet.Rpc.data |
Port, TimeOut, UserName, Password, Infos |
Server configuration |
Service.Info |
Snet.Rpc.data |
ISn (string), INs (List<Details>) |
Authorized client entry |
Class Proxy
Extends System.Dynamic.DynamicObject. Thread-safe singleton via double-check locking.
Instance(ProxyData.Basics)-- Factory method that reuses existing proxies by comparingBasicsTryInvokeMember(InvokeMemberBinder, object[], out object)-- Intercepts method calls:- Starts an
Awaitentry keyed by channel ID - Constructs a
Requestwith interface name, method name, and arguments - Serializes to JSON, wraps in
IByteBuffer, sends viachannel.WriteAndFlushAsync() - Blocks on
Await.Wait()for the response - Deserializes
Response.datainto the method's return type - On timeout or error, raises
Exceptionon the owningRpcClientorRpcService
- Starts an
Class Await
Request-response synchronization engine using ConcurrentDictionary<string, AwaitData> + AutoResetEvent.
| Method | Description |
|---|---|
Start(string tag) |
Register a wait slot by tag (channel ID), creates AwaitData if not exists |
Set(string tag, string rData) |
Write result data and signal AutoResetEvent.Set() to wake the waiting thread |
Wait(string tag) |
Block on AutoResetEvent.WaitOne(), then remove the slot and return AwaitData |
Flow: Start(tag) -> send request -> Wait(tag) blocks -> response arrives -> Set(tag, data) wakes -> Wait returns result
๐ป Code Examples
Complete Server Example (Program.cs)
using Snet.Log;
using Snet.Rpc.service;
using Snet.Utility;
using System.Text;
// Initialize server
RpcService rpcService = RpcService.Instance(new Snet.Rpc.data.Service.Basics
{
Port = 6688,
TimeOut = 1000,
UserName = "snet",
Password = "snet",
Infos = new List<Snet.Rpc.data.Service.Info>
{
new Snet.Rpc.data.Service.Info
{
INs = new List<Snet.Rpc.data.Details>
{
new Snet.Rpc.data.Details { INames = "IHello" }
},
ISn = "RPC1"
}
}
});
LogHelper.Info((await rpcService.OpenAsync()).ToJson(true));
// Register interface-implementation mappings
await rpcService.RegisterAsync<IHello, Hello>();
// Server can also invoke client methods (bidirectional RPC)
while (true)
{
Console.ReadLine();
IHello hello = rpcService.Create<IHello>();
hello.Kitty(new Test
{
aaaa = "Server -> Client",
bbbb = DateTime.Now.ToDateTimeString()
});
LogHelper.Info(hello.Get());
}
// Interface and implementation
public class Test
{
public string aaaa { get; set; }
public string bbbb { get; set; }
}
public interface IHello
{
void Kitty(Test test);
string Get();
}
public class Hello : IHello
{
public string Get() => "Server GET method: " + DateTime.Now.ToDateTimeString();
public void Kitty(Test test) => LogHelper.Info(test.ToJson(true));
}
Complete Client Example (Program.cs)
using Snet.Log;
using Snet.Rpc.client;
using Snet.Utility;
// Initialize client
RpcClient rpcClient = RpcClient.Instance(new Snet.Rpc.data.Client.Basics
{
IpAddress = "127.0.0.1",
Port = 6688,
TimeOut = 1000,
Password = "ysai",
UserName = "ysai",
ISn = "RPC1",
INs = new List<Snet.Rpc.data.Details>
{
new Snet.Rpc.data.Details { INames = "IHello" }
},
});
LogHelper.Info((await rpcClient.OpenAsync()).ToJson(true));
// Register interface-implementation mappings
await rpcClient.RegisterAsync<IHello, Hello>();
// Invoke remote methods via dynamic proxy
while (true)
{
Console.ReadLine();
IHello hello = rpcClient.Create<IHello>();
hello.Kitty(new Test
{
aaaa = "Client -> Server",
bbbb = DateTime.Now.ToDateTimeString()
});
LogHelper.Info(hello.Get());
}
// Interface and implementation (mirrors server-side definitions)
public class Test
{
public string aaaa { get; set; }
public string bbbb { get; set; }
}
public interface IHello
{
void Kitty(Test test);
string Get();
}
public class Hello : IHello
{
public string Get() => "Client GET method: " + DateTime.Now.ToDateTimeString();
public void Kitty(Test test) => LogHelper.Info(test.ToJson(true));
}
โ FAQ
1. What is the difference between Register and RegisterAsync?
Register<I, O>() is a synchronous convenience wrapper provided by CoreUnify<T, B>. It internally calls RegisterAsync<I, O>() and blocks until completion. Use RegisterAsync for non-blocking scenarios; use Register when synchronous initialization is preferred.
2. How does authentication work?
When the client calls OpenAsync(), it sends an Authentication object containing UserName, Password, ISn, and INs. The server validates: (1) credentials match, (2) the ISn exists in the server's configured Infos, (3) the interface list matches. The server responds with a Message indicating success or failure. Failed authentications result in the channel being closed.
3. Can I use custom serialization formats?
The framework serializes all messages as JSON using Newtonsoft.Json. Both Request and Response data models carry List<object> parameters and object? data respectively, which are serialized/deserialized via JSON. To use a different format, you would need to modify the serialization calls in Proxy.TryInvokeMember and the Response methods.
4. What happens on connection failure?
If the initial OpenAsync() fails (e.g., server unreachable, authentication rejected), the method returns OperateResult with Status = false and the error details. The client automatically calls CloseAsync(true) to clean up resources. If a connection drops mid-operation, the ExceptionCaught handler in RpcClientHandler invokes RpcClient.Exception(), which fires an OnInfoEventHandler event and calls Dispose().
5. Is the RPC framework thread-safe?
Yes. Key thread-safety mechanisms include:
ConcurrentDictionaryfor proxy caches, authenticated client channels, and await slotsProxy.Instance()uses double-check locking with a staticlockobjectAwaitusesConcurrentDictionarywithAutoResetEventfor safe cross-thread signaling- DotNetty's
MultithreadEventLoopGrouphandles concurrent I/O operations CoreUnify<T, B>provides thread-safe singleton instance management
๐ Version History
| Date | Version | Changes |
|---|---|---|
| 2026-07-23 | โ | Current release. .NET 8.0 / .NET 10.0 support. DotNetty Codecs 0.7.6, ImpromptuInterface 8.0.6, Snet.Core. JSON serialization with Newtonsoft.Json and System.Text.Json dual support. Bidirectional RPC with client channel management. |
