概览 - Snet Docs

Snet.Rpc 文档


📘 概述

Snet.Rpc 是一个基于 DotNetty 构建的高性能 RPC(远程过程调用)框架,面向 .NET 8 和 .NET 10 平台。支持进程间通信与分布式远程调用,适用于微服务架构、分布式计算以及内部服务通信场景。

核心特性:

  • 基于 DotNetty 的异步 IO -- 采用 boss/worker 事件循环组实现高并发、非阻塞 TCP 传输
  • 服务注册与暴露机制 -- 通过单个方法调用即可绑定接口与实现
  • 客户端动态代理 -- 基于 DynamicObject 和 ImpromptuInterface 实现透明的远程方法调用
  • 可插拔序列化 -- 默认使用 JSON 序列化,同时支持 Newtonsoft.Json 和 System.Text.Json
  • 内置身份认证 -- 连接时验证用户名/密码、唯一标识 (ISn) 和接口列表
  • 双向 RPC -- 客户端和服务端均可通过已注册的通道互相调用对方的方法
属性
包名 Snet.Rpc
命名空间 Snet.Rpc
目标框架 .NET 8.0 / .NET 10.0
许可证 MIT
依赖项 DotNetty.Codecs 0.7.6, ImpromptuInterface 8.0.6, Snet.Core

▶️ 快速开始

仓库中已内置示例项目:Snet.RPC.Service.SamplesSnet.RPC.Client.Samples

服务端:

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" } }
        }
    }
});

await rpcService.OpenAsync();
await rpcService.RegisterAsync<IHello, Hello>();

// 双向调用:服务端调用客户端方法
IHello proxy = rpcService.Create<IHello>();
proxy.Kitty(new Test { aaaa = "服务端 => 客户端", bbbb = DateTime.Now.ToString() });
Console.WriteLine(proxy.Get());

客户端:

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" } }
});

await rpcClient.OpenAsync();
await rpcClient.RegisterAsync<IHello, Hello>();

// 通过动态代理调用远程方法
IHello proxy = rpcClient.Create<IHello>();
proxy.Kitty(new Test { aaaa = "客户端 => 服务端", bbbb = DateTime.Now.ToString() });
Console.WriteLine(proxy.Get());

共享接口与实现:

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() => "来自 " + DateTime.Now.ToString() + " 的问候";
    public void Kitty(Test test) => Console.WriteLine(test.aaaa);
}

⚙️ 安装与配置

通过 NuGet 安装:

dotnet add package Snet.Rpc

客户端配置 -- Client.Basics

属性 类型 默认值 说明
IpAddress string "127.0.0.1" 远程服务端 IP 地址
Port int 6688 远程服务端端口
TimeOut int 1000 超时时间(毫秒)
UserName string "ysai" 认证用户名
Password string "ysai" 认证密码
ISn string "888888" 客户端唯一标识
INs List<Details> new List<Details>() 注册的接口名称集合

服务端配置 -- Service.Basics

属性 类型 默认值 说明
Port int 6688 监听端口
TimeOut int 1000 超时时间(毫秒)
UserName string "snet" 认证用户名
Password string "snet" 认证密码
Infos List<Info> new List<Info>() 已授权的客户端信息

服务端配置 -- Service.Info

属性 类型 默认值 说明
ISn string "888888" 客户端唯一标识
INs List<Details> new List<Details>() 允许的接口名称集合

接口详情 -- Details

属性 类型 默认值 说明
INames string "Interface Name" 用于注册/认证的接口名称

🧠 核心概念

1. 动态代理模式

Proxy 类继承自 System.Dynamic.DynamicObject,借助 ImpromptuInterface 创建透明的 RPC 代理。当代理上的方法被调用时,TryInvokeMember 拦截该调用,将方法名和参数序列化为 Request,通过 DotNetty 通道发送,阻塞等待 Response 返回后反序列化出返回值。

代理实例通过 ConcurrentDictionary<string, object> 缓存,以接口名称为键以提高复用性。Proxy 类本身采用线程安全的双重检查锁定单例模式。

2. 请求-响应模型

Request(请求)                   Response(响应)
+------------------+             +------------------+
| TAG = request    |             | TAG = response   |
| IName = "IHello" |  ------->  | info = "请求成功" |
| MName = "Get"    |  <-------  | status = true     |
| Params = [...]   |             | data = "结果"     |
+------------------+             | time = "2026..."  |
                                 +------------------+
  • Request:携带接口名称 (IName)、方法名称 (MName) 和参数列表 (Params)
  • Response:返回结果信息 (info)、成功状态 (status)、返回数据 (data) 和服务器时间戳 (time)

3. 基于长度字段帧的 TCP 传输

DotNetty 提供 TCP 传输层,采用自定义管道:

  • LengthFieldPrepender(8) -- 在每个输出帧前添加 8 字节长度头
  • LengthFieldBasedFrameDecoder(int.MaxValue, 0, 8, 0, 8) -- 读取 8 字节长度头后解码输入帧有效载荷

管道组成:

[LengthFieldPrepender] -> [LengthFieldBasedFrameDecoder] -> [Handler]

客户端RpcClientHandler 将事件委托给 RpcClient.Response()RpcClient.Exception() 服务端RpcServiceHandler 将事件委托给 RpcService.Response()RpcService.Exception()

4. 认证流程

调用 OpenAsync() 时,客户端发送包含以下字段的 Authentication 消息:

字段 说明
UserName 用户名字符串
Password 密码字符串
ISn 客户端唯一标识
INs 客户端可提供的接口列表

服务端按以下顺序验证:

  1. 用户名和密码与服务器配置匹配
  2. ISn 存在于服务端的 Infos 列表中
  3. INs 列表与服务端注册的接口匹配

认证成功时,服务端回复 Message { State = true, Info = "认证成功" }。认证失败时,服务端发送错误消息并关闭通道。

认证通过后,服务端将客户端通道添加到 ConcurrentDictionary<List<Details>, IChannel> 中以备后续双向调用。

5. 双向 RPC

RpcClientRpcService 均实现 IRpc 接口,这意味着任意一方均可:

  • 注册接口及其实现
  • 创建远程接口的代理
  • 处理传入的请求(通过基于反射的方法调用)

服务端通过接口名称在已认证客户端字典中匹配目标客户端通道,从而实现服务端到客户端的 RPC 调用。

6. 基于 CoreUnify<T,B> 的单例模式

RpcClientRpcService 均继承自 CoreUnify<T, B>(来自 Snet.Core),该基类提供:

  • 通过 Instance(Basics) 工厂方法实现线程安全的单例管理
  • 异步生命周期方法:OpenAsync()RegisterAsync<I,O>()CloseAsync()
  • 操作计时管理:BegOperateAsync() / EndOperateAsync()
  • 基于事件的日志与信息通知
  • IDisposable / IAsyncDisposable 实现

📚 API 参考

接口 IRpc

方法 签名 说明
RegisterAsync Task<OperateResult> RegisterAsync<I, O>(CancellationToken token = default) 注册接口与实现的映射关系
OpenAsync Task<OperateResult> OpenAsync(CancellationToken token = default) 启动连接(客户端连接 / 服务端绑定)
CloseAsync Task<OperateResult> CloseAsync(bool hardClose = false, CancellationToken token = default) 正常关闭或强制关闭连接
Create T Create<T>() where T : class 创建动态代理实现透明远程方法调用
Response void Response(IByteBuffer data, IChannel channel) 处理来自 DotNetty 通道的传入数据
Exception void Exception(Exception ex) 处理传输层异常

RpcClient

继承自 CoreUnify<RpcClient, Client.Basics>,实现 IRpc

  • 使用 DotNetty Bootstrap 和单个 MultithreadEventLoopGroup
  • OpenAsync() 时发送 Authentication,等待服务端 Message 响应
  • 维护 ConcurrentDictionary<string, object> 用于代理缓存
  • 维护 Dictionary<string, Type> 用于接口到实现的映射
  • 根据消息 TAG 类型分发传入数据:request、response、authentication、message
  • 通过基于反射的方法调用处理传入的 request 消息(充当服务端角色)

RpcService

继承自 CoreUnify<RpcService, Service.Basics>,实现 IRpc

  • 使用 DotNetty ServerBootstrap 和 boss + worker 两个 MultithreadEventLoopGroup
  • 维护 ConcurrentDictionary<List<Details>, IChannel> 存储已认证的客户端通道
  • 三阶段认证验证:凭据验证、ISn 存在性验证、接口列表比对
  • 基于反射的方法调用:Activator.CreateInstance() + MethodInfo.Invoke()
  • 通过 JSON 往返转换实现自动参数类型转换(JsonConvert.SerializeObject 后再 DeserializeObject

数据模型

命名空间 属性 说明
Request Snet.Rpc.data TAG (Types), IName (string), MName (string), Params (List<object>) 远程方法调用请求
Response Snet.Rpc.data TAG (Types), info (string), status (bool), data (object?), time (string) 远程方法调用结果
Authentication Snet.Rpc.data TAG (Types), UserName (string), Password (string), ISn (string), INs (List<Details>) 客户端认证载荷
Message Snet.Rpc.data TAG (Types), Info (string), State (bool), Time (string) 通知/状态消息
Type Snet.Rpc.data TAG (Types) 基于 TAG 的消息区分基类
Types (枚举) Snet.Rpc.data request, response, authentication, message 消息类型枚举
Details Snet.Rpc.data INames (string) 接口名称描述符
ProxyData.Basics Snet.Rpc.data Main (object), channel (IChannel), iName (string), type (System.Type), Await (Await) 代理初始化数据
AwaitData Snet.Rpc.data WaitHandler (AutoResetEvent), resultData (string) 同步等待容器
Client.Basics Snet.Rpc.data IpAddress, Port, TimeOut, UserName, Password, ISn, INs 客户端配置
Service.Basics Snet.Rpc.data Port, TimeOut, UserName, Password, Infos 服务端配置
Service.Info Snet.Rpc.data ISn (string), INs (List<Details>) 已授权客户端条目

Proxy

继承自 System.Dynamic.DynamicObject。通过双重检查锁定实现线程安全单例。

  • Instance(ProxyData.Basics) -- 工厂方法,通过比较 Basics 复用已有代理实例
  • TryInvokeMember(InvokeMemberBinder, object[], out object) -- 拦截方法调用:
    1. 以通道 ID 为键启动 Await 条目
    2. 使用接口名称、方法名称和参数构造 Request
    3. 序列化为 JSON,包装为 IByteBuffer,通过 channel.WriteAndFlushAsync() 发送
    4. 通过 Await.Wait() 阻塞等待响应
    5. Response.data 反序列化为方法的返回类型
    6. 超时或错误时,向所属的 RpcClientRpcService 触发 Exception

Await

基于 ConcurrentDictionary<string, AwaitData> + AutoResetEvent 的请求-响应同步引擎。

方法 说明
Start(string tag) 按标签(通道 ID)注册等待槽,不存在则创建 AwaitData
Set(string tag, string rData) 写入结果数据并触发 AutoResetEvent.Set() 唤醒等待线程
Wait(string tag) 阻塞在 AutoResetEvent.WaitOne() 上,随后移除槽并返回 AwaitData

流程:Start(tag) -> 发送请求 -> Wait(tag) 阻塞 -> 响应到达 -> Set(tag, data) 唤醒 -> Wait 返回结果


💻 代码示例

完整服务端示例 (Program.cs)

using Snet.Log;
using Snet.Rpc.service;
using Snet.Utility;
using System.Text;

// 初始化服务端
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));

// 注册接口与实现
await rpcService.RegisterAsync<IHello, Hello>();

// 服务端同样可以调用客户端方法(双向 RPC)
while (true)
{
    Console.ReadLine();
    IHello hello = rpcService.Create<IHello>();
    hello.Kitty(new Test
    {
        aaaa = "服务端 => 客户端",
        bbbb = DateTime.Now.ToDateTimeString()
    });
    LogHelper.Info(hello.Get());
}

// 接口与实现
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() => "我是服务端的 GET 方法: " + DateTime.Now.ToDateTimeString();
    public void Kitty(Test test) => LogHelper.Info(test.ToJson(true));
}

完整客户端示例 (Program.cs)

using Snet.Log;
using Snet.Rpc.client;
using Snet.Utility;

// 初始化客户端
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));

// 注册接口与实现
await rpcClient.RegisterAsync<IHello, Hello>();

// 通过动态代理调用远程方法
while (true)
{
    Console.ReadLine();
    IHello hello = rpcClient.Create<IHello>();
    hello.Kitty(new Test
    {
        aaaa = "客户端 => 服务端",
        bbbb = DateTime.Now.ToDateTimeString()
    });
    LogHelper.Info(hello.Get());
}

// 接口与实现(与服务端定义保持一致)
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() => "我是客户端的 GET 方法: " + DateTime.Now.ToDateTimeString();
    public void Kitty(Test test) => LogHelper.Info(test.ToJson(true));
}

❓ 常见问题

1. RegisterAsync 与同步 API 有什么区别?

RpcClient / RpcService 仅暴露异步方法:OpenAsync()RegisterAsync<I, O>()CloseAsync()。不存在同步的 Open() / Register<I, O>() / Close() 包装。使用 RegisterAsync<I, O>() 注册接口与实现的映射关系;注册前需先调用 OpenAsync()

2. 认证机制是如何工作的?

客户端调用 OpenAsync() 时发送包含 UserNamePasswordISnINsAuthentication 对象。服务端进行三重验证:(1) 凭据是否匹配,(2) ISn 是否存在于服务端配置的 Infos 中,(3) 接口列表是否匹配。服务端回复 Message 指示成功或失败。认证失败时通道将被关闭。

3. 可以使用自定义序列化格式吗?

框架使用 Newtonsoft.Json 将所有消息序列化为 JSON。RequestResponse 数据模型分别携带 List<object> 参数和 object? 数据,均通过 JSON 进行序列化/反序列化。若要使用其他格式,需修改 Proxy.TryInvokeMemberResponse 方法中的序列化调用。

4. 连接失败时会发生什么?

如果初始 OpenAsync() 失败(例如服务端不可达、认证被拒),方法返回 Status = falseOperateResult 并包含错误详情。客户端会自动调用 CloseAsync(true) 清理资源。如果连接在运行过程中断开,RpcClientHandlerExceptionCaught 处理器会调用 RpcClient.Exception(),触发 OnInfoEventHandler 事件并调用 Dispose()

5. RPC 框架是线程安全的吗?

是的。关键的线程安全机制包括:

  • ConcurrentDictionary 用于代理缓存、已认证客户端通道和等待槽
  • Proxy.Instance() 使用带静态 lock 对象的双重检查锁定
  • Await 使用 ConcurrentDictionary 配合 AutoResetEvent 实现安全的跨线程信号通知
  • DotNetty 的 MultithreadEventLoopGroup 处理并发 I/O 操作
  • CoreUnify<T, B> 提供线程安全的单例实例管理

📅 版本历史

日期 版本 变更说明
2026-07-23 当前版本。支持 .NET 8.0 / .NET 10.0。依赖 DotNetty Codecs 0.7.6、ImpromptuInterface 8.0.6、Snet.Core。Newtonsoft.Json 和 System.Text.Json 双序列化支持。双向 RPC 与客户端通道管理。

🔗 相关资源