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.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>();
// 双向调用:服务端调用客户端方法
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 |
客户端可提供的接口列表 |
服务端按以下顺序验证:
- 用户名和密码与服务器配置匹配
ISn存在于服务端的Infos列表中INs列表与服务端注册的接口匹配
认证成功时,服务端回复 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 的消息区分基类 |
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)-- 拦截方法调用:- 以通道 ID 为键启动
Await条目 - 使用接口名称、方法名称和参数构造
Request - 序列化为 JSON,包装为
IByteBuffer,通过channel.WriteAndFlushAsync()发送 - 通过
Await.Wait()阻塞等待响应 - 将
Response.data反序列化为方法的返回类型 - 超时或错误时,向所属的
RpcClient或RpcService触发Exception
- 以通道 ID 为键启动
类 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() 时发送包含 UserName、Password、ISn 和 INs 的 Authentication 对象。服务端进行三重验证:(1) 凭据是否匹配,(2) ISn 是否存在于服务端配置的 Infos 中,(3) 接口列表是否匹配。服务端回复 Message 指示成功或失败。认证失败时通道将被关闭。
3. 可以使用自定义序列化格式吗?
框架使用 Newtonsoft.Json 将所有消息序列化为 JSON。Request 和 Response 数据模型分别携带 List<object> 参数和 object? 数据,均通过 JSON 进行序列化/反序列化。若要使用其他格式,需修改 Proxy.TryInvokeMember 和 Response 方法中的序列化调用。
4. 连接失败时会发生什么?
如果初始 OpenAsync() 失败(例如服务端不可达、认证被拒),方法返回 Status = false 的 OperateResult 并包含错误详情。客户端会自动调用 CloseAsync(true) 清理资源。如果连接在运行过程中断开,RpcClientHandler 的 ExceptionCaught 处理器会调用 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 与客户端通道管理。 |
