概览 - Snet Docs

Snet.Rpc 文档


📘 概述

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

核心特性:

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

安装版本基线为 26.301.1(2026-10-29 已核对 NuGet)。本文行为及第三方依赖按当前源码核对;源码 <Version>1.0.0.0</Version> 与内部 ProjectReference 是构建配置,不是安装版本;源码行为可能晚于已发布包。


▶️ 快速开始

仓库中已内置示例项目:Snet.RPC.Service.Samples 和 Snet.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>();

// 先启动客户端,等客户端完成OpenAsync和RegisterAsync后再按Enter。
Console.WriteLine("客户端准备好后按Enter");
Console.ReadLine();
// 双向调用:服务端调用客户端方法
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.Instance() 对创建过程加锁,但在锁外读取静态列表,不能据此保证工厂并发安全。

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 与该首个条目的接口列表匹配

当前实现通过 Select(...).First() 执行这两项检查。应配置非空且匹配的首个 Info;空列表会抛异常,后续条目无法独立通过这些前置检查。

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

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

5. 双向 RPC

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

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

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

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

RpcClient 和 RpcService 均继承自 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判别模型(非消息继承基类);先读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. 响应为空、失败或发生异常时,调用所属 RpcClient 或 RpcService 的 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>() 添加本地接口与实现的映射,可以在连接打开前调用。应在启动阶段、请求到达前完成注册;重复注册同一接口返回失败。

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

客户端调用 OpenAsync() 时发送 UserName、Password、ISn 和 INs。服务端检查凭据和首个 Info 的标识、接口列表,并回复 Message。显式拒绝分支会关闭通道。配置必须包含匹配的首个 Info;当前代码未遍历全部条目进行授权。

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

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

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

发生异常或收到拒绝认证的响应时,OpenAsync() 返回失败的 OperateResult。异常和空认证响应会调用 CloseAsync(true);非空且 State = false 的响应直接返回。若认证或 RPC 响应始终未到达,当前无参等待可能无限阻塞。通道异常调用 RpcClient.Exception(),通过 OnInfoEvent 报告并调用 Dispose()。

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

当前实现不能保证同一通道上的并发调用安全。代理使用通道 ID 作为等待键,消息没有逐请求关联 ID,并发调用会共享等待槽。应对每个通道串行调用,并在启动阶段完成注册。注册字典及锁外代理列表读取也使整体线程安全承诺不成立。TimeOut 未应用到等待过程,Await.Wait() 没有超时或取消支持。


🔗 相关资源