Overview - Snet Docs

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.Samples and Snet.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 frame
  • LengthFieldBasedFrameDecoder(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:

  1. Username and password match server configuration
  2. The ISn exists in the server's Infos list
  3. The INs list 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 / IAsyncDisposable implementation

๐Ÿ“š 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 Bootstrap with a single MultithreadEventLoopGroup
  • Sends Authentication on OpenAsync(), awaits server Message response
  • Maintains ConcurrentDictionary<string, object> for proxy caching
  • Maintains Dictionary<string, Type> for interface-to-implementation mapping
  • Dispatches incoming data by message TAG type: request, response, authentication, message
  • Handles incoming request messages via reflection-based method invocation (like a server)

Class RpcService

Inherits CoreUnify<RpcService, Service.Basics>, implements IRpc.

  • Uses DotNetty ServerBootstrap with boss + worker MultithreadEventLoopGroups
  • 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.SerializeObject then DeserializeObject)

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 comparing Basics
  • TryInvokeMember(InvokeMemberBinder, object[], out object) -- Intercepts method calls:
    1. Starts an Await entry keyed by channel ID
    2. Constructs a Request with interface name, method name, and arguments
    3. Serializes to JSON, wraps in IByteBuffer, sends via channel.WriteAndFlushAsync()
    4. Blocks on Await.Wait() for the response
    5. Deserializes Response.data into the method's return type
    6. On timeout or error, raises Exception on the owning RpcClient or RpcService

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:

  • ConcurrentDictionary for proxy caches, authenticated client channels, and await slots
  • Proxy.Instance() uses double-check locking with a static lock object
  • Await uses ConcurrentDictionary with AutoResetEvent for safe cross-thread signaling
  • DotNetty's MultithreadEventLoopGroup handles 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.

๐Ÿ”— Related Resources