├── .gitignore
├── Directory.Build.props
├── NuGet.config
├── README.md
├── SatelliteRpc.sln
├── SatelliteRpc.sln.DotSettings.user
├── assets
├── 240225164316071.png
└── 240225164318938.png
├── samples
├── Client
│ ├── Client.csproj
│ ├── DemoHostedService.cs
│ ├── Program.cs
│ └── Rpc
│ │ ├── DemoLoginClient.cs
│ │ └── ILoginClient.cs
└── Server
│ ├── Program.cs
│ ├── Server.csproj
│ └── Services
│ ├── ILoginService.cs
│ └── LoginService.cs
├── src
├── SatelliteRpc.Client.SourceGenerator
│ ├── GeneratorExecutionContextExtensions.cs
│ ├── SatelliteRpc.Client.SourceGenerator.csproj
│ └── SatelliteRpcClientGenerator.cs
├── SatelliteRpc.Client
│ ├── Configuration
│ │ └── SatelliteRpcClientOptions.cs
│ ├── Extensions
│ │ ├── HostBuilderExtensions.cs
│ │ └── ServiceCollectionExtensions.cs
│ ├── Middleware
│ │ ├── IRpcClientMiddleware.cs
│ │ ├── IRpcClientMiddlewareBuilder.cs
│ │ └── RpcClientMiddlewareBuilder.cs
│ ├── SatelliteRpc.Client.csproj
│ ├── SatelliteRpcAttribute.cs
│ └── Transport
│ │ ├── CallContext.cs
│ │ ├── DefaultSatelliteRpcClient.cs
│ │ ├── IRpcConnection.cs
│ │ ├── ISatelliteRpcClient.cs
│ │ └── RpcConnection.cs
├── SatelliteRpc.Protocol
│ ├── BasicProtocolParser.cs
│ ├── PayloadConverters
│ │ ├── IPayloadConverter.cs
│ │ ├── PayloadConverterSource.cs
│ │ └── ProtocolBufferPayloadConverter.cs
│ ├── Protocol
│ │ ├── AppRequest.cs
│ │ ├── AppResponse.cs
│ │ ├── Empty.cs
│ │ ├── Login.proto
│ │ ├── PayloadType.cs
│ │ ├── PayloadWriter.cs
│ │ └── ResponseStatus.cs
│ └── SatelliteRpc.Protocol.csproj
├── SatelliteRpc.Server
│ ├── Configuration
│ │ ├── IRpcServerBuilder.cs
│ │ ├── RpcServerBuilder.cs
│ │ └── SatelliteRpcServerOptions.cs
│ ├── Exceptions
│ │ ├── NotFoundException.cs
│ │ └── ParametersBindException.cs
│ ├── Extensions
│ │ ├── HostBuilderExtensions.cs
│ │ ├── RpcServerBuilderExtensions.cs
│ │ ├── ServiceCollectionExtensions.cs
│ │ └── WebHostBuilderExtensions.cs
│ ├── Observability
│ │ └── ObservabilityMiddleware.cs
│ ├── RpcService
│ │ ├── DataExchange
│ │ │ ├── DefaultRpcDataExchange.cs
│ │ │ └── IRpcDataExchange.cs
│ │ ├── Endpoint
│ │ │ ├── DefaultRpcEndPointResolver.cs
│ │ │ ├── EndpointInvokeMiddleware.cs
│ │ │ ├── IEndpointResolver.cs
│ │ │ ├── RpcServiceEndpoint.cs
│ │ │ └── RpcServiceEndpointDataSource.cs
│ │ ├── IRpcService.cs
│ │ ├── Middleware
│ │ │ ├── IRpcServiceMiddleware.cs
│ │ │ ├── IRpcServiceMiddlewareBuilder.cs
│ │ │ └── RpcServiceMiddlewareBuilder.cs
│ │ ├── RpcServiceHandler.cs
│ │ └── ServiceContext.cs
│ ├── SatelliteRpc.Server.csproj
│ └── Transport
│ │ ├── IRpcConnectionApplicationHandlerBuilder.cs
│ │ ├── RpcConnectionApplicationHandlerBuilder.cs
│ │ ├── RpcConnectionHandler.cs
│ │ └── RpcRawContext.cs
└── SatelliteRpc.Shared
│ ├── Application
│ ├── ApplicationBuilder.cs
│ ├── ApplicationDelegate.cs
│ └── IApplicationMiddleware.cs
│ ├── Collections
│ ├── PooledArray.cs
│ ├── PooledArrayExtensions.cs
│ └── PooledList.cs
│ ├── DisposeManager.cs
│ ├── MethodInvoker.cs
│ └── SatelliteRpc.Shared.csproj
└── tests
├── SatelliteRpc.Protocol.Tests
├── AppRequestTests.cs
├── AppResponseTests.cs
├── GlobalUsings.cs
├── PayloadConverterTests
│ └── ProtocolBufferPayloadConverterTests.cs
└── SatelliteRpc.Protocol.Tests.csproj
├── SatelliteRpc.Server.Tests
├── GlobalUsings.cs
├── SatelliteRpc.Server.Tests.csproj
└── Transport
│ ├── RpcConnectionHandlerTest.RunResponseHandler.cs
│ └── RpcConnectionHandlerTests.OnConnectedAsync.cs
└── SatelliteRpc.Shared.Tests
├── GlobalUsings.cs
├── MethodInvokerTests.cs
├── PooledArrayTests.cs
├── PooledListTests.cs
└── SatelliteRpc.Shared.Tests.csproj
/.gitignore:
--------------------------------------------------------------------------------
1 | bin/
2 | obj/
3 | /packages/
4 | riderModule.iml
5 | /_ReSharper.Caches/
6 | /.vs
7 | /.idea
8 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | NU1803;CS1591;CS0108;
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/NuGet.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## 背景
2 | 之前在.NET 性能优化群内交流时,我们发现很多朋友对于高性能网络框架有需求,需要创建自己的消息服务器、游戏服务器或者物联网网关。但是大多数小伙伴只知道 DotNetty,虽然 DotNetty 是一个非常优秀的网络框架,广泛应用于各种网络服务器中,不过因为各种原因它已经不再有新的特性支持和更新,很多小伙伴都在寻找替代品。
3 |
4 | 这一切都不用担心,在.NET Core 以后的时代,我们有了更快、更强、更好的 Kestrel 网络框架,正如其名,Kestrel 中文翻译为**红隼(hóng sǔn)**封面就是红隼的样子,是一种飞行速度极快的猛禽。Kestrel 是 ASPNET Core 成为.NET 平台性能最强 Web 服务框架的原因之一,但是很多人还觉得 Kestrel 只是用于 ASPNET Core 的网络框架,但是其实它是一个高性能的通用网络框架。
5 |
6 | 我和拥有多个.NET 千星开源项目作者九哥一拍即合,为了让更多的人了解 Kestrel,计划写一系列的文章来介绍它,九哥已经写了一系列的文章来介绍如何使用Kestrel来创建网络服务,我觉得他写的已经很深入和详细了,于是没有编写的计划。
7 |
8 | 不过最近发现还是有很多朋友在群里面问这样的问题,还有群友提到如何使用Kestrel来实现一个RPC框架,刚好笔者在前面一段时间研究了一下这个,所以这一篇文章也作为Kestrel的应用篇写给大家,目前来说想分为几篇文章来发布,大体的脉络如下所示,后续看自己的时间和读者们感兴趣的点再调整内容。
9 |
10 | * 整体设计
11 | * Kestrel服务端实现
12 | * 请求、响应序列化及反序列化
13 | * 单链接多路复用实现
14 | * 性能优化
15 | * Client实现
16 | * 代码生成技术
17 | * 待定……
18 |
19 | ## 项目
20 |
21 | 本文对应的项目源码已经开源在Github上,由于时间仓促,笔者只花了几天时间设计和实现这个RPC框架,所以里面肯定有一些设计不合理或者存在BUG的地方,还需要大家帮忙查缺补漏。
22 |
23 | SatelliteRpc: https://github.com/InCerryGit/SatelliteRpc
24 |
25 | **如果对您有帮助,欢迎点个star~**
26 | **再次提醒注意:该项目只作为学习、演示使用,没有经过生产环境的检验。**
27 |
28 | ## 项目信息
29 |
30 | ### 编译环境
31 |
32 | 要求 .NET 7.0 SDK 版本,Visual Studio 和 Rider 对应版本都可以。
33 |
34 | ### 目录结构
35 | ```
36 | ├─samples // 示例项目
37 | │ ├─Client // 客户端示例
38 | │ │ └─Rpc // RPC客户端服务
39 | │ └─Server // 服务端示例
40 | │ └─Services // RPC服务端服务
41 | ├─src // 源代码
42 | │ ├─SatelliteRpc.Client // 客户端
43 | │ │ ├─Configuration // 客户端配置信息
44 | │ │ ├─Extensions // 针对HostBuilder和ServiceCollection的扩展
45 | │ │ ├─Middleware // 客户端中间件,包含客户端中间件的构造器
46 | │ │ └─Transport // 客户端传输层,包含请求上下文,默认的客户端和Rpc链接的实现
47 | │ ├─SatelliteRpc.Client.SourceGenerator // 客户端代码生成器,用于生成客户端的调用代码
48 | │ ├─SatelliteRpc.Protocol // 协议层,包含协议的定义,协议的序列化和反序列化,协议的转换器
49 | │ │ ├─PayloadConverters // 承载数据的序列化和反序列化,包含ProtoBuf
50 | │ │ └─Protocol // 协议定义,请求、响应、状态和给出的Login.proto
51 | │ ├─SatelliteRpc.Server // 服务端
52 | │ │ ├─Configuration // 服务端配置信息,还有RpcServer的构造器
53 | │ │ ├─Exceptions // 服务端一些异常
54 | │ │ ├─Extensions // 针对HostBuilder、ServiceCollection、WebHostBuilder的扩展
55 | │ │ ├─Observability // 服务端的可观测性支持,目前实现了中间件
56 | │ │ ├─RpcService // 服务端的具体Rpc服务的实现
57 | │ │ │ ├─DataExchange // 数据交换,包含Rpc服务的数据序列化
58 | │ │ │ ├─Endpoint // Rpc服务的端点,包含Rpc服务的端点,寻址,管理
59 | │ │ │ └─Middleware // 包含Rpc服务的中间件的构造器
60 | │ │ └─Transport // 服务端传输层,包含请求上下文,服务端的默认实现,Rpc链接的实现,链接层中间件构建器
61 | │ └─SatelliteRpc.Shared // 共享层,包含一些共享的类
62 | │ ├─Application // 应用层中间件构建基类,客户端和服务端中间件构建都依赖它
63 | │ └─Collections // 一些集合类
64 | └─tests // 测试项目
65 | ├─SatelliteRpc.Protocol.Tests
66 | ├─SatelliteRpc.Server.Tests
67 | └─SatelliteRpc.Shared.Tests
68 | ```
69 | ## 演示
70 |
71 | 安装好SDK和下载项目以后,`samples`目录是对应的演示项目,这个项目就是通过我们的RPC框架调用Server端创建的一些服务,先启动Server然后再启动Client就可以得到如下的运行结果:
72 |
73 |
74 |
75 |
76 |
77 | ## 设计方案
78 | 下面简单的介绍一下总体的设计方案:
79 |
80 | ### 传输协议设计
81 | 传输协议的主要代码在`SatelliteRpc.Protocol`项目中,协议的定义在`Protocol`目录下。针对RPC的请求和响应创建了两个类,一个是`AppRequest`另一个是`AppResponse`。
82 |
83 | 在代码注释中,描述了协议的具体内容,这里简单的介绍一下,请求协议定义如下:
84 | ```text
85 | [请求总长度][请求Id][请求的路径(字符串)]['\0'分隔符][请求数据序列化类型][请求体]
86 | ```
87 | 响应协议定义如下:
88 | ```text
89 | [响应总长度][请求Id][响应状态][响应数据序列化类型][响应体]
90 | ```
91 | 其中主要的参数和数据在各自请求响应体中,请求体和响应体的序列化类型是通过`PayloadConverters`中的序列化器进行序列化和反序列化的。
92 |
93 | 在响应时使用了**请求Id**,这个请求Id是ulong类型,是一个**链接唯一的**自增的值,每次请求都会自增,这样就可以保证每次请求的Id都是唯一的,这样就可以在客户端和服务端进行匹配,从而找到对应的请求,从而实现多路复用的请求和响应匹配功能。
94 |
95 | 当ulong类型的值超过最大值时,会从0开始重新计数,由于ulong类型的值是64位的,值域非常大,所以在正常的情况下,同一连接下不可能出现请求Id重复的情况。
96 |
97 | ### 客户端设计
98 |
99 | 客户端的层次结构如下所示,最底层是传输层的中间件,它由`RpcConnection`生成,它用于TCP网络连接和最终的发送接受请求,中间件构建器保证了它是整个中间件序列的最后的中间件,然后上层就是用户自定义的中间件。
100 |
101 | 默认的客户端实现`DefaultSatelliteRpcClient`,目前只提供了几个Invoke方法,用于不同传参和返参的服务,在这里会执行中间件序列,最后就是具体的`LoginClient`实现,这里方法定义和`ILoginClient`一致,也和服务端定义一致。
102 |
103 | 最后就是调用的代码,现在有一个`DemoHostedService`的后台服务,会调用一下方法,输出日志信息。
104 |
105 | 下面是一个层次结构图:
106 | ```text
107 | [用户层代码]
108 | |
109 | [LoginClient]
110 | |
111 | [DefaultSatelliteRpcClient]
112 | |
113 | [用户自定义中间件]
114 | |
115 | [RpcConnection]
116 | |
117 | [TCP Client]
118 | ```
119 | 所以整个RCP Client的关键实体的转换如下图所示:
120 | ```text
121 | 请求:[用户PRC 请求响应契约][CallContext - AppRequest&AppResponse][字节流]
122 | 响应:[字节流][CallContext - AppRequest&AppResponse][用户PRC 请求响应契约]
123 | ```
124 | #### 多路复用
125 |
126 | 上文提到,多路复用主要是使用ulong类型的Id来匹配Request和Response,主要代码在`RpcConnection`,它不仅提供了一个最终用于发送请求的方法,
127 | 在里面声明了一个`TaskCompletionSource`的字典,用于存储请求Id和`TaskCompletionSource`的对应关系,这样就可以在收到响应时,通过请求Id找到对应的`TaskCompletionSource`,从而完成请求和响应的匹配。
128 |
129 | 由于请求可能是并发的,所以在`RpcConnection`中声明了`Channel`,将并发的请求放入到Channel中,然后在`RpcConnection`中有一个后台线程,用于从Channel单线程的中取出请求,然后发送请求,避免并发调用远程接口时,底层字节流的混乱。
130 |
131 | #### 扩展性
132 |
133 | 客户端不仅仅支持`ILoginClient`这一个契约,用户可以自行添加其他契约,只要保障服务端有相同的接口实现即可。也支持增加其它proto文件,Protobuf.Tools会自动生成对应的实体类。
134 |
135 | ##### 中间件
136 | 该项目的扩展性类似ASP.NET Core的中间件,可以自行加入中间件处理请求和响应,中间件支持Delegate形式,也支持自定义中间件类的形式,如下代码所示:
137 | ```csharp
138 | public class MyMiddleware : IRpcClientMiddleware
139 | {
140 | public async Task InvokeAsync(ApplicationDelegate next, CallContext next)
141 | {
142 | // do something
143 | await next(context);
144 | // do something
145 | }
146 | }
147 | ```
148 | 在客户端中间件中,可以通过`CallContext`获取到请求和响应的数据,然后可以对数据进行处理,然后调用`next`方法,这样就可以实现中间件的链式调用。
149 |
150 | 同样也可以进行阻断操作,比如在某个中间件中,直接返回响应,这样就不会继续调用后面的中间件;或者记录请求响应日志,或者进行一些其他的操作,类似于ASP.NET Core中间件都可以实现。
151 |
152 | ##### 序列化
153 | 序列化的扩展性主要是通过`PayloadConverters`来实现的,内部实现了抽象了一个接口`IPayloadConverter`,只要实现对应PayloadType的序列化和反序列化方法即可,然后注册到DI容器中,便可以使用。
154 |
155 | 由于时间关系,只列出了Protobuf和Json两种序列化器,实际上可以支持用户自定义序列化器,只需要在请求响应协议中添加标识,然后由用户注入到DI容器即可。
156 |
157 | ##### 其它
158 |
159 | 其它一些类的实现基本都是通过接口和依赖注入的方式实现,用户可以很方便的进行扩展,在DI容器中替换默认实现即可。如:`IRpcClientMiddlewareBuilder`、
160 | `IRpcConnection`、`ISatelliteRpcClient`等。
161 |
162 | 另外也可以自行添加其他的服务,因为代码生成器会自动扫描接口,然后生成对应的调用代码,所以只需要在接口上添加`SatelliteRpcAttribute`,声明好方法契约,就能实现。
163 |
164 | ### 服务端设计
165 |
166 | 服务端的设计总体和客户端设计差不多,中间唯一有一点区别的地方就是服务端的中间件有两种:
167 | - 一种是针对连接层的`RpcConnectionApplicationHandler`中间件,设计它的目的主要是为了灵活处理链接请求,由于可以直接访问原始数据,还没有做路由和参数绑定,后续可观测性指标和一些性能优化在这里做会比较方便。
168 | - 比如为了应对RPC调用,定义了一个名为`RpcServiceHandler`的`RpcConnectionApplicationHandler`中间件,放在整个连接层中间件的最后,这样可以保证最后执行的是RPC Service层的逻辑。
169 | - 另外一种是针对业务逻辑层的`RpcServiceMiddleware`,这里就是类似ASP.NET Core的中间件,此时上下文中已经有了路由信息和参数绑定,可以在这做一些AOP编程,也能直接调用对应的服务方法。
170 | - 在RPC层,我们需要完成路由,参数绑定,执行目标方法等功能,这里就是定义了一个名为`EndpointInvokeMiddleware`的中间件,放在整个RPC Service层中间件的最后,这样可以保证最后执行的是RPC Service层的逻辑。
171 |
172 | 下面是一个层次结构图:
173 | ```text
174 | [用户层代码]
175 | |
176 | [LoginService]
177 | |
178 | [用户自定义的RpcServiceMiddleware]
179 | |
180 | [RpcServiceHandler]
181 | |
182 | [用户自定义的RpcConnectionApplicationHandler]
183 | |
184 | [RpcConnectionHandler]
185 | |
186 | [Kestrel]
187 | ```
188 | 整个RPC Server的关键实体的转换如下图所示:
189 | ```text
190 | 请求:[字节流][RpcRawContext - AppRequest&AppResponse][ServiceContext][用户PRC Service 请求契约]
191 | 响应:[用户PRC Service 响应契约][ServiceContext][AppRequest&AppResponse][字节流]
192 | ```
193 | #### 多路复用
194 |
195 | 服务端对于多路复用的支持就简单的很多,这里是在读取到一个完整的请求以后,直接使用Task.Run执行后续的逻辑,所以能做到同一链接多个请求并发执行,
196 | 对于响应为了避免混乱,使用了`Channel`,将响应放入到Channel中,然后在后台线程中单线程的从Channel中取出响应,然后返回响应。
197 |
198 | #### 终结点
199 |
200 | 在服务端中有一个终结点的概念,这个概念和ASP.NET Core中的概念类似,它具体的实现类是`RpcServiceEndpoint`;在程序开始启动以后;
201 | 便会扫描入口程序集(当然这块可以优化),然后找到所有的`RpcServiceEndpoint`,然后注册到DI容器中,然后由`RpcServiceEndpointDataSource`统一管理,
202 | 最后在进行路由时有`IEndpointResolver`根据路径进行路由,这只提供了默认实现,用户也可以自定义实现,只需要实现`IEndpointResolver`接口,然后替换DI容器中的默认实现即可。
203 |
204 | #### 扩展性
205 |
206 | 服务端的扩展性也是在**中间件**、**序列化**、**其它接口**上,可以通过DI容器很方便的替换默认实现,增加AOP切面等功能,也可以直接添加新的Service服务,因为会默认去扫描入口程序集中的`RpcServiceEndpoint`,然后注册到DI容器中。
207 |
208 | ## 优化
209 |
210 | 现阶段做的性能优化主要是以下几个方面:
211 | - Pipelines
212 | - 在客户端的请求和服务端处理(Kestrel底层使用)中都使用了Pipelines,这样不仅可以降低编程的复杂性,而且由于直接读写Buffer,可以减少内存拷贝,提高性能。
213 | - 表达式树
214 | - 在动态调用目标服务的方法时,使用了表达式树,这样可以减少反射的性能损耗,在实际场景中可以设置一个快慢阈值,当方法调用次数超过阈值时,就可以使用表达式树来调用方法,这样可以提高性能。
215 | - 代码生成
216 | - 在客户端中,使用了代码生成技术,这个可以让用户使用起来更加简单,无需理解RPC的底层实现,只需要定义好接口,然后使用代码生成器生成对应的调用代码即可;另外实现了客户端自动注入,避免运行时反射注入的性能损耗。
217 | - 内存复用
218 | - 对于RPC框架来说,最大的内存开销基本就在请求和响应体上,创建了PooledArray和PooledList,两个池化的底层都是使用的ArrayPool,请求和响应的Payload都是使用的池化的空间。
219 | - 减少内存拷贝
220 | - RPC框架消耗CPU的地方是内存拷贝,上文提到了客户端和服务端均使用Pipelines,在读取响应和请求的时候直接使用`ReadOnlySequence`读取网络层数据,避免拷贝。
221 | - 客户端请求和服务端响应创建了PayloadWriter类,通过`IBufferWriter`直接将序列化的结果写入网络Buffer中,减少内存拷贝,虽然会引入闭包开销,但是相对于内存拷贝来说,几乎可以忽略。
222 | - 对于这个优化实际应该设置一个阈值,当序列化的数据超过阈值时,才使用PayloadWriter,否则使用内存拷贝的方式,需要Benchmark测试支撑阈值设置。
223 |
224 | 其它更多的性能优化需要Benchmark的数据支持,由于时间比较紧,没有做更多的优化。
225 |
226 | ## 待办
227 |
228 | 计划做,但是没有时间去实现的:
229 |
230 | - 服务端代码生成
231 | - 现阶段服务端的路由是通过字典匹配实现,方法调用使用的表达式树,实际上这一块可以使用代码生成来实现,这样可以提高性能。
232 | - 另外一个地方就是Endpoint注册是通过反射扫描入口程序集实现的,实际上这一步可以放在编译阶段处理,在编译时就可以读取到所有的服务,然后生成代码,这样可以减少运行时的反射。
233 | - 客户端取消请求
234 | - 目前客户端的请求取消只是在客户端本身,取消并不会传递到服务端,这一块可以通过协议来实现,在请求协议中添加一个标识,传递Cancel请求,然后在服务端进行判断,如果是取消请求,则服务端也根据ID取消对应的请求。
235 | - Context 和 AppRequest\AppResponse 池化
236 | - 目前的Context和AppRequest\AppResponse都是每次请求都会创建,对于这些小对象可以使用池化的方式来实现复用,其中AppRequest、AppResponse已经实现了复用的功能,但是没有时间去实现池化,Context也可以实现池化,但是目前没有实现。
237 | - 堆外内存、FOH管理
238 | - 目前的内存管理都是使用的堆内存,对于那些有明显作用域的对象和缓存空间可以使用堆外内存或FOH来实现,这样可以减少GC在扫描时的压力。
239 | - AsyncTask的内存优化
240 | - 目前是有一些地方使用的ValueTask,对于这些地方也是内存分配的优化方向,可以使用`PoolingAsyncValueTaskMethodBuilder`来池化ValueTask,这样可以减少内存分配。
241 | - TaskCompletionSource也是可以优化的,后续可以使用`AwaitableCompletionSource`来降低分配。
242 | - 客户端连接池化
243 | - 目前客户端的连接还是单链接,实际上可以使用连接池来实现,这样可以减少TCP链接的创建和销毁,提高性能。
244 | - 异常场景处理
245 | - 目前对于服务端和客户端来说,没有详细的测试,针对TCP链接断开,数据包错误,服务器异常等场景的重试,熔断等策略都没有实现。
--------------------------------------------------------------------------------
/SatelliteRpc.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SatelliteRpc.Server", "src\SatelliteRpc.Server\SatelliteRpc.Server.csproj", "{146A605B-09A6-4E9B-9A1E-C030F7BC0627}"
4 | EndProject
5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{B891FBEA-1F02-49C1-A570-1BB76E24DD1C}"
6 | EndProject
7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server", "samples\Server\Server.csproj", "{50235CF8-82BA-411A-8E08-6358AF60FEFF}"
8 | EndProject
9 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client", "samples\Client\Client.csproj", "{7ABE96C5-C451-4260-936D-818C348512CB}"
10 | EndProject
11 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{594072AE-D250-4FE5-BAC2-36E09A8F0EF0}"
12 | EndProject
13 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1019D4BE-C564-41A2-9A54-64A8E166BFA2}"
14 | EndProject
15 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SatelliteRpc.Server.Tests", "tests\SatelliteRpc.Server.Tests\SatelliteRpc.Server.Tests.csproj", "{0D87520E-912F-40B7-A2D3-6C53EF19C718}"
16 | EndProject
17 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SatelliteRpc.Protocol", "src\SatelliteRpc.Protocol\SatelliteRpc.Protocol.csproj", "{B8021DD8-8871-41C4-9494-BDF28CAACA94}"
18 | EndProject
19 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SatelliteRpc.Client", "src\SatelliteRpc.Client\SatelliteRpc.Client.csproj", "{76C4CA40-3FAC-44A0-808F-9C01AC42B4C3}"
20 | EndProject
21 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SatelliteRpc.Client.SourceGenerator", "src\SatelliteRpc.Client.SourceGenerator\SatelliteRpc.Client.SourceGenerator.csproj", "{1DE03048-151C-4F15-ACA5-B075820C632C}"
22 | EndProject
23 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SatelliteRpc.Shared", "src\SatelliteRpc.Shared\SatelliteRpc.Shared.csproj", "{A740BF50-4F01-434F-B4CD-BAA81B2E3005}"
24 | EndProject
25 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SatelliteRpc.Shared.Tests", "tests\SatelliteRpc.Shared.Tests\SatelliteRpc.Shared.Tests.csproj", "{D6295CC4-A592-46DC-B1CD-B785BFACBACF}"
26 | EndProject
27 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SatelliteRpc.Protocol.Tests", "tests\SatelliteRpc.Protocol.Tests\SatelliteRpc.Protocol.Tests.csproj", "{19040F1D-C753-4FD5-9D78-D9AB2EFE6891}"
28 | EndProject
29 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "items", "items", "{B05A28E4-0DCC-448A-A401-533146FA5BB8}"
30 | ProjectSection(SolutionItems) = preProject
31 | README.md = README.md
32 | EndProjectSection
33 | EndProject
34 | Global
35 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
36 | Debug|Any CPU = Debug|Any CPU
37 | Release|Any CPU = Release|Any CPU
38 | EndGlobalSection
39 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
40 | {146A605B-09A6-4E9B-9A1E-C030F7BC0627}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
41 | {146A605B-09A6-4E9B-9A1E-C030F7BC0627}.Debug|Any CPU.Build.0 = Debug|Any CPU
42 | {146A605B-09A6-4E9B-9A1E-C030F7BC0627}.Release|Any CPU.ActiveCfg = Release|Any CPU
43 | {146A605B-09A6-4E9B-9A1E-C030F7BC0627}.Release|Any CPU.Build.0 = Release|Any CPU
44 | {50235CF8-82BA-411A-8E08-6358AF60FEFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
45 | {50235CF8-82BA-411A-8E08-6358AF60FEFF}.Debug|Any CPU.Build.0 = Debug|Any CPU
46 | {50235CF8-82BA-411A-8E08-6358AF60FEFF}.Release|Any CPU.ActiveCfg = Release|Any CPU
47 | {50235CF8-82BA-411A-8E08-6358AF60FEFF}.Release|Any CPU.Build.0 = Release|Any CPU
48 | {7ABE96C5-C451-4260-936D-818C348512CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
49 | {7ABE96C5-C451-4260-936D-818C348512CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
50 | {7ABE96C5-C451-4260-936D-818C348512CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
51 | {7ABE96C5-C451-4260-936D-818C348512CB}.Release|Any CPU.Build.0 = Release|Any CPU
52 | {0D87520E-912F-40B7-A2D3-6C53EF19C718}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
53 | {0D87520E-912F-40B7-A2D3-6C53EF19C718}.Debug|Any CPU.Build.0 = Debug|Any CPU
54 | {0D87520E-912F-40B7-A2D3-6C53EF19C718}.Release|Any CPU.ActiveCfg = Release|Any CPU
55 | {0D87520E-912F-40B7-A2D3-6C53EF19C718}.Release|Any CPU.Build.0 = Release|Any CPU
56 | {B8021DD8-8871-41C4-9494-BDF28CAACA94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
57 | {B8021DD8-8871-41C4-9494-BDF28CAACA94}.Debug|Any CPU.Build.0 = Debug|Any CPU
58 | {B8021DD8-8871-41C4-9494-BDF28CAACA94}.Release|Any CPU.ActiveCfg = Release|Any CPU
59 | {B8021DD8-8871-41C4-9494-BDF28CAACA94}.Release|Any CPU.Build.0 = Release|Any CPU
60 | {76C4CA40-3FAC-44A0-808F-9C01AC42B4C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
61 | {76C4CA40-3FAC-44A0-808F-9C01AC42B4C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
62 | {76C4CA40-3FAC-44A0-808F-9C01AC42B4C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
63 | {76C4CA40-3FAC-44A0-808F-9C01AC42B4C3}.Release|Any CPU.Build.0 = Release|Any CPU
64 | {1DE03048-151C-4F15-ACA5-B075820C632C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
65 | {1DE03048-151C-4F15-ACA5-B075820C632C}.Debug|Any CPU.Build.0 = Debug|Any CPU
66 | {1DE03048-151C-4F15-ACA5-B075820C632C}.Release|Any CPU.ActiveCfg = Release|Any CPU
67 | {1DE03048-151C-4F15-ACA5-B075820C632C}.Release|Any CPU.Build.0 = Release|Any CPU
68 | {A740BF50-4F01-434F-B4CD-BAA81B2E3005}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
69 | {A740BF50-4F01-434F-B4CD-BAA81B2E3005}.Debug|Any CPU.Build.0 = Debug|Any CPU
70 | {A740BF50-4F01-434F-B4CD-BAA81B2E3005}.Release|Any CPU.ActiveCfg = Release|Any CPU
71 | {A740BF50-4F01-434F-B4CD-BAA81B2E3005}.Release|Any CPU.Build.0 = Release|Any CPU
72 | {D6295CC4-A592-46DC-B1CD-B785BFACBACF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
73 | {D6295CC4-A592-46DC-B1CD-B785BFACBACF}.Debug|Any CPU.Build.0 = Debug|Any CPU
74 | {D6295CC4-A592-46DC-B1CD-B785BFACBACF}.Release|Any CPU.ActiveCfg = Release|Any CPU
75 | {D6295CC4-A592-46DC-B1CD-B785BFACBACF}.Release|Any CPU.Build.0 = Release|Any CPU
76 | {19040F1D-C753-4FD5-9D78-D9AB2EFE6891}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
77 | {19040F1D-C753-4FD5-9D78-D9AB2EFE6891}.Debug|Any CPU.Build.0 = Debug|Any CPU
78 | {19040F1D-C753-4FD5-9D78-D9AB2EFE6891}.Release|Any CPU.ActiveCfg = Release|Any CPU
79 | {19040F1D-C753-4FD5-9D78-D9AB2EFE6891}.Release|Any CPU.Build.0 = Release|Any CPU
80 | EndGlobalSection
81 | GlobalSection(NestedProjects) = preSolution
82 | {50235CF8-82BA-411A-8E08-6358AF60FEFF} = {B891FBEA-1F02-49C1-A570-1BB76E24DD1C}
83 | {7ABE96C5-C451-4260-936D-818C348512CB} = {B891FBEA-1F02-49C1-A570-1BB76E24DD1C}
84 | {146A605B-09A6-4E9B-9A1E-C030F7BC0627} = {1019D4BE-C564-41A2-9A54-64A8E166BFA2}
85 | {0D87520E-912F-40B7-A2D3-6C53EF19C718} = {594072AE-D250-4FE5-BAC2-36E09A8F0EF0}
86 | {B8021DD8-8871-41C4-9494-BDF28CAACA94} = {1019D4BE-C564-41A2-9A54-64A8E166BFA2}
87 | {76C4CA40-3FAC-44A0-808F-9C01AC42B4C3} = {1019D4BE-C564-41A2-9A54-64A8E166BFA2}
88 | {1DE03048-151C-4F15-ACA5-B075820C632C} = {1019D4BE-C564-41A2-9A54-64A8E166BFA2}
89 | {A740BF50-4F01-434F-B4CD-BAA81B2E3005} = {1019D4BE-C564-41A2-9A54-64A8E166BFA2}
90 | {D6295CC4-A592-46DC-B1CD-B785BFACBACF} = {594072AE-D250-4FE5-BAC2-36E09A8F0EF0}
91 | {19040F1D-C753-4FD5-9D78-D9AB2EFE6891} = {594072AE-D250-4FE5-BAC2-36E09A8F0EF0}
92 | EndGlobalSection
93 | EndGlobal
94 |
--------------------------------------------------------------------------------
/SatelliteRpc.sln.DotSettings.user:
--------------------------------------------------------------------------------
1 |
2 | <SessionState ContinuousTestingMode="0" IsActive="True" Name="Test1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
3 | <Solution />
4 | </SessionState>
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/assets/240225164316071.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/InCerryGit/SatelliteRpc/7b4a56d76f3418cb3d8fab00c8412082bbba7161/assets/240225164316071.png
--------------------------------------------------------------------------------
/assets/240225164318938.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/InCerryGit/SatelliteRpc/7b4a56d76f3418cb3d8fab00c8412082bbba7161/assets/240225164318938.png
--------------------------------------------------------------------------------
/samples/Client/Client.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net7.0
6 | enable
7 | enable
8 | true
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/samples/Client/DemoHostedService.cs:
--------------------------------------------------------------------------------
1 | using Client.Rpc;
2 | using Microsoft.Extensions.Hosting;
3 | using Microsoft.Extensions.Logging;
4 | using ServerProto;
5 |
6 | namespace Client;
7 |
8 | public class DemoHostedService : BackgroundService
9 | {
10 | private readonly ILogger _logger;
11 | private readonly ILoginClient _client;
12 |
13 | public DemoHostedService(ILogger logger, ILoginClient client)
14 | {
15 | _logger = logger;
16 | _client = client;
17 | }
18 |
19 | protected override async Task ExecuteAsync(CancellationToken stoppingToken)
20 | {
21 | _logger.LogInformation("DemoHostedService is starting");
22 |
23 | var rp = await _client.Login(new LoginReqProto
24 | {
25 | User = "admin",
26 | Password = "123456",
27 | Sn = 1024
28 | }, stoppingToken);
29 | _logger.LogInformation("Received1: {IsOk},{ErrMsg},{Sn}", rp!.IsOk, rp.ErrMsg, rp.Sn);
30 |
31 | rp = await _client.Login(new LoginReqProto
32 | {
33 | User = "guest",
34 | Password = "654321",
35 | Sn = 2048
36 | }, stoppingToken);
37 | _logger.LogInformation("Received2: {IsOk},{ErrMsg},{Sn}", rp!.IsOk, rp.ErrMsg, rp.Sn);
38 |
39 | await _client.LogOut(stoppingToken);
40 | _logger.LogInformation("DemoHostedService is stopping");
41 | }
42 | }
--------------------------------------------------------------------------------
/samples/Client/Program.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using Client;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using Microsoft.Extensions.Hosting;
5 | using SatelliteRpc.Client;
6 | using SatelliteRpc.Client.Extensions;
7 |
8 | await Host
9 | .CreateDefaultBuilder()
10 | .UseRpcClient(options =>
11 | {
12 | options.ServerAddress = IPAddress.Parse("127.0.0.1");
13 | options.ServerPort = 58886;
14 | }, middlewareBuilder =>
15 | {
16 | middlewareBuilder.Use(async (next, context) =>
17 | {
18 | Console.WriteLine($"Before {context.Request.Id}");
19 | await next(context);
20 | Console.WriteLine($"After {context.Request.Id}");
21 | });
22 | })
23 | .ConfigureServices(services =>
24 | {
25 | services.AddAutoGeneratedClients();
26 | services.AddHostedService();
27 | })
28 | .Build()
29 | .RunAsync();
--------------------------------------------------------------------------------
/samples/Client/Rpc/DemoLoginClient.cs:
--------------------------------------------------------------------------------
1 | // 自动化生成的客户端实例如下类似
2 | //using SatelliteRpc.Client;
3 | //using ServerProto;
4 |
5 | //namespace Client.Rpc;
6 |
7 | //public class DemoLoginClient : ILoginClient
8 | //{
9 | // private readonly ISatelliteRpcClient _client;
10 | //
11 | // public DemoLoginClient(ISatelliteRpcClient client)
12 | // {
13 | // _client = client;
14 | // }
15 | //
16 | // public async Task Login(LoginReqProto req, CancellationToken? cancellationToken = null)
17 | // {
18 | // return await _client.InvokeAsync("LoginService/Login", req, cancellationToken);
19 | // }
20 | //
21 | // public Task LogOut(CancellationToken? cancellationToken = null)
22 | // {
23 | // return _client.InvokeAsync("LoginService/LogOut", cancellationToken);
24 | // }
25 | //}
--------------------------------------------------------------------------------
/samples/Client/Rpc/ILoginClient.cs:
--------------------------------------------------------------------------------
1 | using SatelliteRpc.Client;
2 | using ServerProto;
3 |
4 | namespace Client.Rpc;
5 |
6 | [SatelliteRpc("LoginService")]
7 | public interface ILoginClient
8 | {
9 | Task Login(LoginReqProto req, CancellationToken? cancellationToken = null);
10 |
11 | Task LogOut(CancellationToken? cancellationToken = null);
12 | }
--------------------------------------------------------------------------------
/samples/Server/Program.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using Microsoft.Extensions.Hosting;
3 | using SatelliteRpc.Server.Extensions;
4 |
5 | await Host
6 | .CreateDefaultBuilder(args)
7 | .UseSatelliteRpcServer(builder =>
8 | {
9 | // define your rpc server
10 | builder.ConfigureSatelliteRpcServer(options =>
11 | {
12 | // define your options here
13 | options.Host = IPAddress.Parse("127.0.0.1");
14 | options.Port = 58886;
15 | });
16 |
17 | builder.AddRpcConnectionHandler(connMidBuilder =>
18 | {
19 | connMidBuilder.Use(async (next, context) =>
20 | {
21 | Console.WriteLine($"Before Connection Middleware {context.Request.Id}");
22 | await next(context);
23 | Console.WriteLine($"After Connection Middleware {context.Request.Id}");
24 | });
25 | });
26 |
27 | builder.AddRpcServiceMiddleware(serviceMidBuilder =>
28 | {
29 | serviceMidBuilder.Use(async (next, context) =>
30 | {
31 | Console.WriteLine($"Before Service Middleware {context.RawContext.Request.Id}");
32 | await next(context);
33 | Console.WriteLine($"After Service Middleware {context.RawContext.Request.Id}");
34 | });
35 | });
36 | })
37 | .Build()
38 | .RunAsync();
--------------------------------------------------------------------------------
/samples/Server/Server.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net7.0
6 | enable
7 | enable
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/samples/Server/Services/ILoginService.cs:
--------------------------------------------------------------------------------
1 | using SatelliteRpc.Server.RpcService;
2 | using ServerProto;
3 |
4 | namespace Server.Services;
5 |
6 | public interface ILoginService : IRpcService
7 | {
8 | Task Login(LoginReqProto req, CancellationToken? cancellationToken);
9 |
10 | Task LogOut(CancellationToken? cancellationToken = null);
11 | }
--------------------------------------------------------------------------------
/samples/Server/Services/LoginService.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Logging;
2 | using ServerProto;
3 |
4 | namespace Server.Services;
5 |
6 | public class LoginService : ILoginService
7 | {
8 | private readonly ILogger _logger;
9 |
10 | public LoginService(ILogger logger)
11 | {
12 | _logger = logger;
13 | }
14 |
15 | public Task Login(LoginReqProto req, CancellationToken? cancellationToken)
16 | {
17 | _logger.LogInformation("Login: {UserName} {Password}", req.User, req.Password);
18 | return Task.FromResult(new LoginRespProto
19 | {
20 | IsOk = true,
21 | ErrMsg = "Not Implemented",
22 | Sn = req.Sn
23 | });
24 | }
25 |
26 | public Task LogOut(CancellationToken? cancellationToken = null)
27 | {
28 | _logger.LogInformation("LogOut");
29 | return Task.CompletedTask;
30 | }
31 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Client.SourceGenerator/GeneratorExecutionContextExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 |
3 | namespace SatelliteRpc.Client.SourceGenerator;
4 |
5 | ///
6 | /// The extension of GeneratorExecutionContext
7 | /// Use this class to log message, warning and error
8 | ///
9 | public static class GeneratorExecutionContextExtensions
10 | {
11 | public static void LogMessage(this GeneratorExecutionContext context, string message)
12 | {
13 | context.ReportDiagnostic(Diagnostic.Create(
14 | new DiagnosticDescriptor(
15 | id: "GEN001",
16 | title: "Generator Message",
17 | messageFormat: message,
18 | category: "SatelliteRpc.Generator",
19 | DiagnosticSeverity.Info,
20 | isEnabledByDefault: true),
21 | location: null));
22 | }
23 |
24 | public static void LogWarning(this GeneratorExecutionContext context, string message)
25 | {
26 | context.ReportDiagnostic(Diagnostic.Create(
27 | new DiagnosticDescriptor(
28 | id: "GEN002",
29 | title: "Generator Warning",
30 | messageFormat: message,
31 | category: "SatelliteRpc.Generator",
32 | DiagnosticSeverity.Warning,
33 | isEnabledByDefault: true),
34 | location: null));
35 | }
36 |
37 | public static void LogError(this GeneratorExecutionContext context, string message)
38 | {
39 | context.ReportDiagnostic(Diagnostic.Create(
40 | new DiagnosticDescriptor(
41 | id: "GEN003",
42 | title: "Generator Error",
43 | messageFormat: message,
44 | category: "SatelliteRpc.Generator",
45 | DiagnosticSeverity.Error,
46 | isEnabledByDefault: true),
47 | location: null));
48 | }
49 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Client.SourceGenerator/SatelliteRpc.Client.SourceGenerator.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | enable
6 | enable
7 | true
8 | 11.0
9 |
10 |
11 |
12 |
13 | runtime; build; native; contentfiles; analyzers; buildtransitive
14 | all
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/SatelliteRpc.Client.SourceGenerator/SatelliteRpcClientGenerator.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using Microsoft.CodeAnalysis;
3 | using Microsoft.CodeAnalysis.CSharp.Syntax;
4 | using Microsoft.CodeAnalysis.Text;
5 |
6 | namespace SatelliteRpc.Client.SourceGenerator;
7 |
8 | ///
9 | /// The generator of SatelliteRpc client
10 | ///
11 | [Generator(LanguageNames.CSharp)]
12 | public class SatelliteRpcClientGenerator : ISourceGenerator
13 | {
14 | public void Initialize(GeneratorInitializationContext context)
15 | {
16 | context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
17 | }
18 |
19 | public void Execute(GeneratorExecutionContext context)
20 | {
21 |
22 | context.LogMessage("Client Generator started");
23 |
24 | if (context.SyntaxReceiver is not SyntaxReceiver receiver)
25 | return;
26 |
27 | var namespaceNameSet = new HashSet();
28 | var clientClasses = new List();
29 | const string namespaceName = "SatelliteRpc.Client";
30 |
31 | foreach (var @interface in receiver.CandidateInterfaces)
32 | {
33 | // first get the semantic model for the interface, and make sure it's annotated
34 | var model = context.Compilation.GetSemanticModel(@interface.SyntaxTree);
35 | if (model.GetDeclaredSymbol(@interface) is not INamedTypeSymbol symbol) continue;
36 | if (!symbol.GetAttributes().Any(ad =>
37 | ad.AttributeClass?.ToDisplayString() == "SatelliteRpc.Client.SatelliteRpcAttribute")) continue;
38 |
39 | var rpcAttribute = symbol.GetAttributes().FirstOrDefault(ad =>
40 | ad.AttributeClass?.ToDisplayString() == "SatelliteRpc.Client.SatelliteRpcAttribute");
41 |
42 | if (rpcAttribute == null) continue;
43 |
44 | var generateClient = (bool?)rpcAttribute
45 | .NamedArguments
46 | .FirstOrDefault(kvp => kvp.Key == "GenerateClient").Value.Value ?? true;
47 | var generateDependencyInjection = (bool?)rpcAttribute
48 | .NamedArguments
49 | .FirstOrDefault(kvp => kvp.Key == "GenerateDependencyInjection").Value.Value ?? true;
50 |
51 | // Skip if both are false
52 | if (!generateClient && !generateDependencyInjection) continue;
53 |
54 | // below is the code to generate the client
55 | var serviceName = (string?)rpcAttribute.ConstructorArguments.FirstOrDefault().Value;
56 |
57 | context.LogMessage(serviceName ?? "Null");
58 |
59 | var className = symbol.Name.Substring(1) + "Client"; // Changed the class name
60 | var interfaceName = symbol.Name;
61 |
62 | if (generateDependencyInjection)
63 | {
64 | clientClasses.Add($"{interfaceName},{className}");
65 | }
66 |
67 | var symbolNamespace = symbol.ContainingNamespace.ToDisplayString();
68 |
69 | namespaceNameSet.Add(symbolNamespace);
70 |
71 | var stringBuilder = new StringBuilder();
72 |
73 | stringBuilder.AppendLine("// ");
74 | stringBuilder.AppendLine("using SatelliteRpc.Client;");
75 | stringBuilder.AppendLine("using SatelliteRpc.Client.Transport;");
76 | stringBuilder.AppendLine("using ServerProto;");
77 | stringBuilder.AppendLine("using System.Threading.Tasks;");
78 | stringBuilder.AppendLine("using System.Threading;");
79 | stringBuilder.AppendLine($"using {symbolNamespace};");
80 | stringBuilder.AppendLine();
81 | stringBuilder.AppendLine($"namespace {symbolNamespace}");
82 | stringBuilder.AppendLine("{");
83 | stringBuilder.AppendLine($" public class {className} : {interfaceName}");
84 | stringBuilder.AppendLine(" {");
85 | stringBuilder.AppendLine(" private readonly ISatelliteRpcClient _client;");
86 | stringBuilder.AppendLine();
87 | stringBuilder.AppendLine($" public {className}(ISatelliteRpcClient client)");
88 | stringBuilder.AppendLine(" {");
89 | stringBuilder.AppendLine(" _client = client;");
90 | stringBuilder.AppendLine(" }");
91 |
92 | // Assume all methods return Task or Task
93 | foreach (var member in symbol.GetMembers().OfType())
94 | {
95 |
96 | var parameters = string.Join(", ", member.Parameters.Select(p => $"{p.Type} {p.Name}"));
97 | var callParameters = string.Join(",", member.Parameters.Select(p => p.Name));
98 | var methodName = member.Name;
99 |
100 | var returnType = member.ReturnType.ToString() == "System.Threading.Tasks.Task"
101 | ? "Task"
102 | : $"Task<{((INamedTypeSymbol)member.ReturnType).TypeArguments[0].Name}>"; // Changed the return type
103 |
104 | var invokeAsync = member.Parameters.Length > 1
105 | ? $"await _client.InvokeAsync<{member.Parameters[0].Type.Name}, {((INamedTypeSymbol)member.ReturnType).TypeArguments[0].Name}>"
106 | : "await _client.InvokeAsync";
107 |
108 |
109 | stringBuilder.AppendLine();
110 | stringBuilder.AppendLine($" public async {returnType} {methodName}({parameters})");
111 | stringBuilder.AppendLine(" {");
112 | if (returnType.StartsWith("Task<"))
113 | stringBuilder.AppendLine($" return {invokeAsync}(\"{serviceName}/{methodName}\", {callParameters});");
114 | else
115 | stringBuilder.AppendLine($" {invokeAsync}(\"{serviceName}/{methodName}\", {callParameters});");
116 | stringBuilder.AppendLine(" }");
117 |
118 | context.LogMessage($"Generator method: {methodName} success");
119 | }
120 |
121 | stringBuilder.AppendLine(" }");
122 | stringBuilder.AppendLine("}");
123 |
124 | context.AddSource($"{className}.g.cs", SourceText.From(stringBuilder.ToString(), Encoding.UTF8));
125 |
126 | context.LogMessage("Client Generator finished");
127 |
128 | context.LogMessage("DI Generator started");
129 |
130 | // Generate a new class to register clients to DI container
131 | var registrationClassBuilder = new StringBuilder();
132 | registrationClassBuilder.AppendLine("// ");
133 | registrationClassBuilder.AppendLine("using Microsoft.Extensions.DependencyInjection;");
134 | foreach (var name in namespaceNameSet)
135 | {
136 | registrationClassBuilder.AppendLine($"using {name};");
137 | }
138 | registrationClassBuilder.AppendLine();
139 | registrationClassBuilder.AppendLine($"namespace {namespaceName}");
140 | registrationClassBuilder.AppendLine("{");
141 | registrationClassBuilder.AppendLine(" public static class RpcClientServiceCollectionExtensions");
142 | registrationClassBuilder.AppendLine(" {");
143 | registrationClassBuilder.AppendLine(" public static IServiceCollection AddAutoGeneratedClients(this IServiceCollection services)");
144 | registrationClassBuilder.AppendLine(" {");
145 |
146 | for (int i = 0; i < clientClasses.Count; i++)
147 | {
148 | registrationClassBuilder.AppendLine($" services.AddSingleton<{clientClasses[i]}>();");
149 | }
150 |
151 | registrationClassBuilder.AppendLine(" return services;");
152 | registrationClassBuilder.AppendLine(" }");
153 | registrationClassBuilder.AppendLine(" }");
154 | registrationClassBuilder.AppendLine("}");
155 |
156 | context.AddSource("RpcClientServiceCollectionExtensions.g.cs", SourceText.From(registrationClassBuilder.ToString(), Encoding.UTF8));
157 |
158 | context.LogMessage("DI Generator finished");
159 | }
160 | }
161 |
162 | private class SyntaxReceiver : ISyntaxReceiver
163 | {
164 | public List CandidateInterfaces { get; } = new();
165 |
166 | public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
167 | {
168 | // We only care about interface declarations
169 | if (syntaxNode is InterfaceDeclarationSyntax { AttributeLists.Count: > 0 } @interface)
170 | {
171 | CandidateInterfaces.Add(@interface);
172 | }
173 | }
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/SatelliteRpc.Client/Configuration/SatelliteRpcClientOptions.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using SatelliteRpc.Protocol.Protocol;
3 |
4 | namespace SatelliteRpc.Client.Configuration;
5 |
6 | public class SatelliteRpcClientOptions
7 | {
8 | ///
9 | /// Rpc remote server address
10 | ///
11 | public IPAddress ServerAddress { get; set; } = IPAddress.Parse("127.0.0.1");
12 |
13 | ///
14 | /// Rpc remote server port
15 | ///
16 | public int ServerPort { get; set; } = 58888;
17 |
18 | ///
19 | /// Request channel max count
20 | ///
21 | public int RequestChannelMaxCount { get; set; } = 1024;
22 |
23 | ///
24 | /// The request default serializer type
25 | ///
26 | public PayloadType PayloadType { get; set; } = PayloadType.Protobuf;
27 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Client/Extensions/HostBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using Microsoft.Extensions.Hosting;
3 | using SatelliteRpc.Client.Configuration;
4 | using SatelliteRpc.Client.Transport;
5 | using SatelliteRpc.Shared.Application;
6 |
7 | namespace SatelliteRpc.Client.Extensions;
8 |
9 | public static class HostBuilderExtensions
10 | {
11 | ///
12 | /// Use Rpc Client
13 | ///
14 | ///
15 | ///
16 | ///
17 | ///
18 | public static IHostBuilder UseRpcClient(
19 | this IHostBuilder builder,
20 | Action? configure = null,
21 | Action>? configureMiddleware = null)
22 | {
23 | builder.ConfigureServices(services => { services.Configure(configure ?? (_ => { })); });
24 | return builder.ConfigureServices(services => { services.AddRpcClient(configureMiddleware); });
25 | }
26 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Client/Extensions/ServiceCollectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using Microsoft.Extensions.DependencyInjection.Extensions;
3 | using SatelliteRpc.Client.Middleware;
4 | using SatelliteRpc.Client.Transport;
5 | using SatelliteRpc.Protocol.PayloadConverters;
6 | using SatelliteRpc.Shared.Application;
7 |
8 | namespace SatelliteRpc.Client.Extensions;
9 |
10 | public static class ServiceCollectionExtensions
11 | {
12 | ///
13 | /// Add Rpc Client
14 | /// Register and
15 | ///
16 | ///
17 | ///
18 | ///
19 | public static IServiceCollection AddRpcClient(this IServiceCollection services,
20 | Action>? configureMiddleware)
21 | {
22 | services.TryAddTransient();
23 | services.TryAddTransient();
24 |
25 | services.TryAddSingleton();
26 | services.TryAddSingleton();
27 | services.TryAddSingleton(provider =>
28 | new RpcClientMiddlewareBuilder(provider, configureMiddleware));
29 | return services;
30 | }
31 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Client/Middleware/IRpcClientMiddleware.cs:
--------------------------------------------------------------------------------
1 | using SatelliteRpc.Client.Transport;
2 | using SatelliteRpc.Shared.Application;
3 |
4 | namespace SatelliteRpc.Client.Middleware;
5 |
6 | ///
7 | /// The middleware interface of Rpc Client
8 | ///
9 | public interface IRpcClientMiddleware : IApplicationMiddleware
10 | {
11 |
12 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Client/Middleware/IRpcClientMiddlewareBuilder.cs:
--------------------------------------------------------------------------------
1 | using SatelliteRpc.Client.Transport;
2 | using SatelliteRpc.Shared.Application;
3 |
4 | namespace SatelliteRpc.Client.Middleware;
5 |
6 | ///
7 | /// The middleware builder interface of Rpc Client
8 | ///
9 | public interface IRpcClientMiddlewareBuilder
10 | {
11 | ///
12 | /// Build the middleware
13 | ///
14 | ///
15 | ApplicationDelegate Build();
16 |
17 | ///
18 | /// The application middleware builder
19 | ///
20 | ApplicationBuilder Builder { get; }
21 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Client/Middleware/RpcClientMiddlewareBuilder.cs:
--------------------------------------------------------------------------------
1 | using SatelliteRpc.Client.Transport;
2 | using SatelliteRpc.Shared.Application;
3 |
4 | namespace SatelliteRpc.Client.Middleware;
5 |
6 | public class RpcClientMiddlewareBuilder : IRpcClientMiddlewareBuilder
7 | {
8 | ///
9 | /// The application middleware builder
10 | ///
11 | public ApplicationBuilder Builder { get; }
12 |
13 | ///
14 | /// Constructor
15 | ///
16 | /// service provider
17 | /// configure middleware action
18 | public RpcClientMiddlewareBuilder(
19 | IServiceProvider services,
20 | Action>? configure = null)
21 | {
22 | Builder = new ApplicationBuilder(services);
23 | configure?.Invoke(Builder);
24 | }
25 |
26 | ///
27 | /// Build the middleware
28 | ///
29 | ///
30 | public ApplicationDelegate Build()
31 | {
32 | return Builder.Build();
33 | }
34 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Client/SatelliteRpc.Client.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net7.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/SatelliteRpc.Client/SatelliteRpcAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace SatelliteRpc.Client;
2 |
3 | ///
4 | /// Rpc service attribute, used to mark the interface as a rpc service
5 | /// Generate the client and dependency injection by default
6 | ///
7 | [AttributeUsage(AttributeTargets.Interface)]
8 | public class SatelliteRpcAttribute : Attribute
9 | {
10 | public SatelliteRpcAttribute(string serviceName)
11 | {
12 | ServiceName = serviceName;
13 | }
14 |
15 | public string ServiceName { get; set; }
16 |
17 | public bool GenerateClient { get; set; } = true;
18 |
19 | public bool GenerateDependencyInjection { get; set; } = true;
20 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Client/Transport/CallContext.cs:
--------------------------------------------------------------------------------
1 | using SatelliteRpc.Protocol.Protocol;
2 |
3 | namespace SatelliteRpc.Client.Transport;
4 |
5 | ///
6 | /// The call context of Rpc Client
7 | /// Use this class to pass data between middleware
8 | ///
9 | public class CallContext : IDisposable
10 | {
11 | ///
12 | /// Request cancellation token
13 | /// Pass client call cancellation token to middleware
14 | ///
15 | public CancellationToken Cancel { get; set; }
16 |
17 | ///
18 | /// Rpc request
19 | ///
20 | public AppRequest Request { get; set; }
21 |
22 | ///
23 | /// Rpc response
24 | ///
25 | public AppResponse? Response { get; set; }
26 |
27 | ///
28 | /// Constructor
29 | ///
30 | ///
31 | ///
32 | public CallContext(AppRequest request, CancellationToken cancel)
33 | {
34 | Request = request;
35 | Cancel = cancel;
36 | }
37 |
38 | ///
39 | /// When the context is disposed, the request and response will be disposed
40 | ///
41 | public void Dispose()
42 | {
43 | Request.Dispose();
44 | Response?.Dispose();
45 | }
46 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Client/Transport/DefaultSatelliteRpcClient.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using Microsoft.Extensions.Options;
3 | using SatelliteRpc.Client.Configuration;
4 | using SatelliteRpc.Client.Middleware;
5 | using SatelliteRpc.Protocol.PayloadConverters;
6 | using SatelliteRpc.Protocol.Protocol;
7 | using SatelliteRpc.Shared.Application;
8 |
9 | namespace SatelliteRpc.Client.Transport;
10 |
11 | ///
12 | /// Default implementation of the ISatelliteRpcClient interface.
13 | /// This class is responsible for invoking remote methods.
14 | ///
15 | public class DefaultSatelliteRpcClient : ISatelliteRpcClient
16 | {
17 | private ulong _id;
18 |
19 | private readonly IPayloadConverter _payloadConverter;
20 | private readonly ApplicationDelegate _clientMiddleware;
21 |
22 | ///
23 | /// Initializes a new instance of the class.
24 | ///
25 | /// Provides access to the SatelliteRpcClientOptions.
26 | /// The RPC connection to use.
27 | /// The source for payload converters.
28 | /// The middleware builder.
29 | public DefaultSatelliteRpcClient(
30 | IOptions optionsAccessor,
31 | IRpcConnection connection,
32 | PayloadConverterSource converterSource,
33 | IRpcClientMiddlewareBuilder builder)
34 | {
35 | var options = optionsAccessor.Value;
36 |
37 | _payloadConverter = converterSource.GetConverter(options.PayloadType);
38 |
39 | // The last middleware, used to send network requests
40 | builder.Builder.Use(async (_, context) => { await connection.RequestMiddlewareAsync(context); });
41 | _clientMiddleware = builder.Build();
42 | }
43 |
44 | ///
45 | /// Invoke a remote method without a payload.
46 | ///
47 | /// The remote method path.
48 | /// A cancellation token that can be used to cancel the operation.
49 | public async Task InvokeAsync(string path, CancellationToken? cancellationToken = null)
50 | {
51 | // because the request payload is empty, so we use the Empty type
52 | await InvokeAsync(path, PayloadWriter.Empty, cancellationToken);
53 | }
54 |
55 | ///
56 | /// Invoke a remote method with a specified request and response type.
57 | ///
58 | /// The remote method path.
59 | /// The request payload.
60 | /// A cancellation token that can be used to cancel the operation.
61 | /// The type of the request payload.
62 | /// The type of the response payload.
63 | /// A task that represents the asynchronous operation. The task result contains the response payload.
64 | public async Task InvokeAsync(
65 | string path,
66 | TRequest rawRequest,
67 | CancellationToken? cancellationToken = null)
68 | {
69 | var payload = _payloadConverter.CreatePayloadWriter(rawRequest);
70 | return await InvokeAsync(path, payload, cancellationToken);
71 | }
72 |
73 | ///
74 | /// Private helper method to invoke a remote method with a specified response type.
75 | ///
76 | /// The remote method path.
77 | /// The request payload writer.
78 | /// A cancellation token that can be used to cancel the operation.
79 | /// The type of the response payload.
80 | /// A task that represents the asynchronous operation. The task result contains the response payload.
81 | private async ValueTask InvokeAsync(
82 | string path,
83 | PayloadWriter payload,
84 | CancellationToken? cancellationToken = null)
85 | {
86 | var request = new AppRequest
87 | {
88 | Id = Interlocked.Increment(ref _id),
89 | Path = path,
90 | PayloadWriter = payload
91 | };
92 |
93 | // Create a new call context
94 | // The call context will be disposed after the request is completed
95 | using var callContext = new CallContext(request, cancellationToken ?? CancellationToken.None);
96 | await _clientMiddleware(callContext);
97 |
98 | // if the response status is not success, the payload is the error message
99 | // this is a convention
100 | if (callContext.Response!.Status != ResponseStatus.Success)
101 | {
102 | throw new Exception(Encoding.UTF8.GetString(callContext.Response.Payload.Span));
103 | }
104 |
105 | return typeof(TResponse) == typeof(Empty)
106 | ? default
107 | : (TResponse?)_payloadConverter.Convert(callContext.Response.Payload, typeof(TResponse));
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/SatelliteRpc.Client/Transport/IRpcConnection.cs:
--------------------------------------------------------------------------------
1 | namespace SatelliteRpc.Client.Transport;
2 |
3 | public interface IRpcConnection
4 | {
5 | ///
6 | /// Send request to server middleware
7 | ///
8 | ///
9 | ///
10 | ValueTask RequestMiddlewareAsync(CallContext callContext);
11 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Client/Transport/ISatelliteRpcClient.cs:
--------------------------------------------------------------------------------
1 | namespace SatelliteRpc.Client.Transport;
2 |
3 | public interface ISatelliteRpcClient
4 | {
5 | ///
6 | /// Invoke the remote method
7 | ///
8 | ///
9 | ///
10 | ///
11 | Task InvokeAsync(string path, CancellationToken? cancellationToken = null);
12 |
13 | ///
14 | /// Invoke the remote method
15 | ///
16 | ///
17 | ///
18 | ///
19 | ///
20 | ///
21 | ///
22 | Task InvokeAsync(
23 | string path,
24 | TRequest rawRequest,
25 | CancellationToken? cancellationToken = null);
26 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Client/Transport/RpcConnection.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Concurrent;
2 | using System.IO.Pipelines;
3 | using System.Net;
4 | using System.Net.Sockets;
5 | using System.Threading.Channels;
6 | using Microsoft.Extensions.Hosting;
7 | using Microsoft.Extensions.Logging;
8 | using Microsoft.Extensions.Options;
9 | using SatelliteRpc.Client.Configuration;
10 | using SatelliteRpc.Protocol.Protocol;
11 |
12 | namespace SatelliteRpc.Client.Transport;
13 |
14 | ///
15 | /// Represents a connection for executing remote procedure calls (RPCs).
16 | /// This class implements both and .
17 | ///
18 | public class RpcConnection : IRpcConnection, IDisposable
19 | {
20 | private readonly ILogger _logger;
21 | private readonly PipeReader _pipeReader;
22 | private readonly PipeWriter _pipeWriter;
23 |
24 | // Channel for sending requests.
25 | private readonly Channel _requestChannel;
26 |
27 | // Table for tracking response tasks.
28 | private readonly ConcurrentDictionary> _responseTable = new();
29 |
30 | ///
31 | /// Initializes a new instance of the class.
32 | ///
33 | public RpcConnection(
34 | ILogger logger,
35 | IOptions optionsAccessor,
36 | IHostApplicationLifetime lifetime)
37 | {
38 | _logger = logger;
39 | var options = optionsAccessor.Value;
40 |
41 | _requestChannel = Channel.CreateBounded(
42 | new BoundedChannelOptions(options.RequestChannelMaxCount)
43 | {
44 | SingleReader = true,
45 | SingleWriter = false,
46 | FullMode = BoundedChannelFullMode.Wait,
47 | });
48 |
49 | var tcpClient = new TcpClient();
50 | tcpClient.Connect(new IPEndPoint(options.ServerAddress, options.ServerPort));
51 | var stream = tcpClient.GetStream();
52 | _pipeReader = PipeReader.Create(stream);
53 | _pipeWriter = PipeWriter.Create(stream);
54 |
55 | // Start write and read tasks.
56 | _ = WriteAsync(lifetime.ApplicationStopping);
57 | _ = ReadAsync(lifetime.ApplicationStopping);
58 |
59 | // Register to dispose when application is stopping.
60 | lifetime.ApplicationStopping.Register(Dispose);
61 | }
62 |
63 | ///
64 | /// Middleware for handling RPC requests.
65 | ///
66 | public async ValueTask RequestMiddlewareAsync(CallContext callContext)
67 | {
68 | var request = callContext.Request;
69 | var cancellationToken = callContext.Cancel;
70 | var tcs = new TaskCompletionSource();
71 | _responseTable.TryAdd(request.Id, tcs);
72 | try
73 | {
74 | await _requestChannel.Writer.WriteAsync(request, cancellationToken);
75 |
76 | // When cancel, remove TaskCompletionSource from table, and throw exception.
77 | cancellationToken.Register(() =>
78 | {
79 | if (_responseTable.TryRemove(request.Id, out var resTcs))
80 | {
81 | resTcs.TrySetCanceled(cancellationToken);
82 | }
83 | });
84 |
85 | callContext.Response = await tcs.Task;
86 | }
87 | finally
88 | {
89 | _responseTable.TryRemove(request.Id, out _);
90 | }
91 | }
92 |
93 | ///
94 | /// This class is responsible for writing RPC requests asynchronously to a pipe writer.
95 | ///
96 | private async Task WriteAsync(CancellationToken cancellationToken)
97 | {
98 | try
99 | {
100 | // Continuously write to the pipe writer while there are requests to be read
101 | while (await _requestChannel.Reader.WaitToReadAsync(cancellationToken))
102 | {
103 | while (_requestChannel.Reader.TryRead(out var request))
104 | {
105 | try
106 | {
107 | // Serialize the request and write it to the pipe writer
108 | request.Serialize(_pipeWriter);
109 | await _pipeWriter.FlushAsync(cancellationToken);
110 | }
111 | catch (Exception ex)
112 | {
113 | _logger.LogError(ex, "[{Id}]Write request error", request.Id);
114 |
115 | // If an error occurs, set the response status to InternalError
116 | SetResponse(new AppResponse
117 | {
118 | Id = request.Id,
119 | Status = ResponseStatus.InternalError
120 | });
121 | }
122 | }
123 | }
124 | }
125 | catch (Exception ex)
126 | {
127 | _logger.LogError(ex, "WriteAsync error");
128 | }
129 | }
130 |
131 | ///
132 | /// This class is responsible for reading RPC responses asynchronously from a pipe reader.
133 | ///
134 | private async Task ReadAsync(CancellationToken cancellationToken)
135 | {
136 | try
137 | {
138 | // Continuously read from the pipe reader until a cancellation is requested
139 | while (cancellationToken.IsCancellationRequested == false)
140 | {
141 | var result = await _pipeReader.ReadAsync(cancellationToken);
142 | if (result.IsCanceled)
143 | {
144 | break;
145 | }
146 |
147 | // Attempt to deserialize the read buffer into an AppResponse
148 | var (success, response) = AppResponse.TryDeserialize(result.Buffer, out var consumed);
149 |
150 | // If the deserialization failed, continue to the next iteration
151 | if (success == false)
152 | {
153 | continue;
154 | }
155 |
156 | // If the deserialization succeeded, set the response and advance the pipe reader
157 | SetResponse(response!);
158 | _pipeReader.AdvanceTo(consumed);
159 |
160 | // If the read operation completed, break the loop
161 | if (result.IsCompleted)
162 | {
163 | break;
164 | }
165 | }
166 | }
167 | catch (Exception ex)
168 | {
169 | _logger.LogError(ex, "ReadAsync error");
170 | }
171 | }
172 |
173 |
174 | ///
175 | /// Sets the response for a request.
176 | ///
177 | private void SetResponse(AppResponse response)
178 | {
179 | if (_responseTable.TryGetValue(response.Id, out var tcs))
180 | {
181 | tcs.SetResult(response);
182 | }
183 | }
184 |
185 | ///
186 | /// Disposes resources used by the connection.
187 | ///
188 | public void Dispose()
189 | {
190 | _pipeWriter.CancelPendingFlush();
191 | _pipeWriter.Complete();
192 |
193 | _pipeReader.CancelPendingRead();
194 | _pipeReader.Complete();
195 | }
196 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Protocol/BasicProtocolParser.cs:
--------------------------------------------------------------------------------
1 | // using System.Buffers;
2 | // using System.Buffers.Binary;
3 | // using System.Diagnostics.CodeAnalysis;
4 | // using System.Runtime.CompilerServices;
5 | // using Google.Protobuf;
6 | //
7 | // namespace SatelliteRpc.Protocol;
8 | //
9 | // public static class BasicProtocolParser
10 | // {
11 | // [MethodImpl(MethodImplOptions.AggressiveInlining)]
12 | // public static bool TryParserRequest(
13 | // ReadOnlySequence sequence,
14 | // out SequencePosition consumed,
15 | // [MaybeNullWhen(returnValue: false)] out AppRequest request)
16 | // {
17 | // // Frame format is defined here:
18 | // // 8 bytes for length of payload
19 | // // N bytes of payload
20 | // request = null;
21 | // consumed = default;
22 | //
23 | // if (sequence.Length < 8)
24 | // return false;
25 | //
26 | // var length = GetLength(sequence);
27 | // if (length > sequence.Length)
28 | // return false;
29 | //
30 | // var payload = sequence.Slice(8, length);
31 | // request = AppRequest.Parser.ParseFrom(payload);
32 | //
33 | // consumed = sequence.GetPosition(8 + length);
34 | // return true;
35 | // }
36 | //
37 | // [MethodImpl(MethodImplOptions.AggressiveInlining)]
38 | // private static long GetLength(in ReadOnlySequence buffer)
39 | // {
40 | // if (buffer.First.Length >= 8)
41 | // {
42 | // return BinaryPrimitives.ReadInt64LittleEndian(buffer.First.Span[..8]);
43 | // }
44 | //
45 | // Span lengthBuffer = stackalloc byte[8];
46 | // buffer.Slice(0, 8).CopyTo(lengthBuffer);
47 | // return BinaryPrimitives.ReadInt64LittleEndian(lengthBuffer);
48 | // }
49 | //
50 | // [MethodImpl(MethodImplOptions.AggressiveInlining)]
51 | // public static bool TryParserResponse(
52 | // ReadOnlySequence sequence,
53 | // out SequencePosition consumed,
54 | // [MaybeNullWhen(returnValue: false)] out AppResponse response)
55 | // {
56 | // // Frame format is defined here:
57 | // // 8 bytes for length of payload
58 | // // N bytes of payload
59 | // response = null;
60 | // consumed = default;
61 | //
62 | // if (sequence.Length < 8)
63 | // return false;
64 | //
65 | // var length = GetLength(sequence);
66 | // if (length > sequence.Length)
67 | // return false;
68 | //
69 | // var payload = sequence.Slice(8, length);
70 | // response = AppResponse.Parser.ParseFrom(payload);
71 | //
72 | // consumed = sequence.GetPosition(8 + length);
73 | // return true;
74 | // }
75 | //
76 | // [MethodImpl(MethodImplOptions.AggressiveInlining)]
77 | // public static void WriteResponseHeader(IBufferWriter pipeWriter, AppResponse response)
78 | // {
79 | // var length = response.CalculateSize();
80 | // Span lengthBuffer = stackalloc byte[8];
81 | // BinaryPrimitives.WriteInt64LittleEndian(lengthBuffer, length);
82 | // pipeWriter.Write(lengthBuffer);
83 | // response.WriteTo(pipeWriter);
84 | // }
85 | //
86 | // [MethodImpl(MethodImplOptions.AggressiveInlining)]
87 | // public static void WriteRequestHeader(IBufferWriter pipeWriter, AppRequest request)
88 | // {
89 | // var length = request.CalculateSize();
90 | // Span lengthBuffer = stackalloc byte[8];
91 | // BinaryPrimitives.WriteInt64LittleEndian(lengthBuffer, length);
92 | // pipeWriter.Write(lengthBuffer);
93 | // request.WriteTo(pipeWriter);
94 | // }
95 | // }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Protocol/PayloadConverters/IPayloadConverter.cs:
--------------------------------------------------------------------------------
1 | using SatelliteRpc.Protocol.Protocol;
2 | using SatelliteRpc.Shared.Collections;
3 |
4 | namespace SatelliteRpc.Protocol.PayloadConverters;
5 |
6 | public interface IPayloadConverter
7 | {
8 | ///
9 | /// Payload type
10 | ///
11 | ///
12 | PayloadType PayloadType { get; }
13 |
14 | ///
15 | /// Convert payload to bytes
16 | ///
17 | ///
18 | ///
19 | ///
20 | object? Convert(PooledArray payload, Type type);
21 |
22 | ///
23 | /// Create payload writer
24 | /// for performance reasons, we use the PayloadWriter to write the payload to the network buffer
25 | ///
26 | ///
27 | ///
28 | PayloadWriter CreatePayloadWriter(object? payload);
29 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Protocol/PayloadConverters/PayloadConverterSource.cs:
--------------------------------------------------------------------------------
1 | using SatelliteRpc.Protocol.Protocol;
2 |
3 | namespace SatelliteRpc.Protocol.PayloadConverters;
4 |
5 | ///
6 | /// Manage payload converters
7 | ///
8 | public class PayloadConverterSource
9 | {
10 | private readonly IReadOnlyDictionary _converters;
11 |
12 | public PayloadConverterSource(IEnumerable converters)
13 | {
14 | _converters = converters.ToDictionary(c => c.PayloadType);
15 | }
16 |
17 | ///
18 | /// Get payload converter by payload type
19 | ///
20 | ///
21 | ///
22 | ///
23 | public IPayloadConverter GetConverter(PayloadType payloadType)
24 | {
25 | if (_converters.TryGetValue(payloadType, out var converter))
26 | {
27 | return converter;
28 | }
29 |
30 | throw new Exception($"No converter found for type {payloadType}");
31 | }
32 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Protocol/PayloadConverters/ProtocolBufferPayloadConverter.cs:
--------------------------------------------------------------------------------
1 | using Google.Protobuf;
2 | using SatelliteRpc.Protocol.Protocol;
3 | using SatelliteRpc.Shared.Collections;
4 |
5 | namespace SatelliteRpc.Protocol.PayloadConverters;
6 |
7 | ///
8 | /// Payload converter for protobuf
9 | ///
10 | public class ProtocolBufferPayloadConverter : IPayloadConverter
11 | {
12 | public PayloadType PayloadType => PayloadType.Protobuf;
13 |
14 | ///
15 | /// Create payload writer
16 | /// for performance reasons, we use the PayloadWriter to write the payload to the network buffer
17 | ///
18 | ///
19 | ///
20 | public PayloadWriter CreatePayloadWriter(object? payload)
21 | {
22 | return payload switch
23 | {
24 | null => PayloadWriter.Empty,
25 | IMessage message => new PayloadWriter
26 | {
27 | GetPayloadSize = () => message.CalculateSize(),
28 | PayloadWriteTo = (buffer) => message.WriteTo(buffer)
29 | },
30 | _ => throw new Exception("Invalid response type")
31 | };
32 | }
33 |
34 | ///
35 | /// Convert payload to bytes
36 | ///
37 | ///
38 | ///
39 | ///
40 | public object? Convert(PooledArray payload, Type type)
41 | {
42 | if (!typeof(IMessage).IsAssignableFrom(type))
43 | {
44 | throw new ArgumentException("Type must be protobuf message", nameof(type));
45 | }
46 |
47 | if (payload.Length == 0)
48 | {
49 | return null;
50 | }
51 |
52 | var message = Activator.CreateInstance(type) as IMessage;
53 | message!.MergeFrom(payload.Span);
54 | return message!;
55 | }
56 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Protocol/Protocol/AppRequest.cs:
--------------------------------------------------------------------------------
1 | using System.Buffers;
2 | using System.Buffers.Binary;
3 | using System.Text;
4 | using SatelliteRpc.Shared;
5 | using SatelliteRpc.Shared.Collections;
6 |
7 | namespace SatelliteRpc.Protocol.Protocol;
8 |
9 | ///
10 | /// AppRequest
11 | ///
12 | public class AppRequest : DisposeManager
13 | {
14 | ///
15 | /// Total length of the request
16 | ///
17 | public int TotalLength { get; set; }
18 |
19 | ///
20 | /// Request Id, used to match the response
21 | /// for multiplexing
22 | ///
23 | public ulong Id { get; set; }
24 |
25 | ///
26 | /// Request path
27 | ///
28 | public string Path { get; set; }
29 |
30 | ///
31 | /// Payload type
32 | ///
33 | public PayloadType PayloadType { get; set; }
34 |
35 | ///
36 | /// Payload
37 | /// if the payload is empty, then the PayloadWriter is used
38 | /// note: because performance reasons, we use the PayloadWriter to write the payload to the network buffer.
39 | ///
40 | public PooledArray Payload { get; set; }
41 |
42 | ///
43 | /// Payload writer
44 | ///
45 | public PayloadWriter PayloadWriter { get; set; } = default;
46 |
47 | ///
48 | /// Get this request size
49 | ///
50 | ///
51 | public (int HeadLength, int PayloadLength) GetSize()
52 | {
53 | // sum every field length
54 | return PayloadWriter.HasPayload
55 | ? (sizeof(ulong) + Encoding.UTF8.GetByteCount(Path) + 1 + sizeof(int) + sizeof(int), PayloadWriter.GetPayloadSize())
56 | : (sizeof(ulong) + Encoding.UTF8.GetByteCount(Path) + 1 + sizeof(int) + sizeof(int), Payload.Length);
57 | }
58 |
59 | public void Serialize(IBufferWriter writer)
60 | {
61 | // The data format is as follows:
62 | // 4 bytes total length
63 | // N bytes header
64 | // N bytes payload
65 |
66 | var length = GetSize();
67 | var totalLength = length.HeadLength + length.PayloadLength + 4;
68 | scoped var span = writer.GetSpan(totalLength);
69 |
70 | // write total length
71 | BinaryPrimitives.WriteInt32LittleEndian(span, length.HeadLength + length.PayloadLength);
72 | span = span[sizeof(int)..];
73 |
74 | // write id
75 | BinaryPrimitives.WriteUInt64LittleEndian(span, Id);
76 | span = span[sizeof(ulong)..];
77 |
78 | // write path, and add null terminator
79 | var pathBytes = Encoding.UTF8.GetBytes(Path);
80 | pathBytes.CopyTo(span);
81 | span[pathBytes.Length] = 0; // Null terminator
82 | span = span[(pathBytes.Length + 1)..];
83 |
84 | // write payload type
85 | BinaryPrimitives.WriteInt32LittleEndian(span, (int)PayloadType);
86 | span = span[sizeof(int)..];
87 |
88 | // write payload length
89 | BinaryPrimitives.WriteInt32LittleEndian(span, length.PayloadLength);
90 | span = span[sizeof(int)..];
91 |
92 | // for performance reasons, we use the PayloadWriter to write the payload to the network buffer
93 | // reduced memory copying
94 | if (!PayloadWriter.HasPayload)
95 | {
96 | Payload.Span.CopyTo(span);
97 | writer.Advance(totalLength);
98 | }
99 | else
100 | {
101 | writer.Advance(length.HeadLength + 4);
102 | PayloadWriter.PayloadWriteTo(writer);
103 | }
104 | }
105 |
106 | ///
107 | /// Try deserialize
108 | ///
109 | ///
110 | ///
111 | ///
112 | ///
113 | public static (bool Success, AppRequest? Request) TryDeserialize(
114 | ReadOnlySequence sequence,
115 | out SequencePosition consumed,
116 | AppRequest? reuse = null)
117 | {
118 | // The data format is as follows:
119 | // 4 bytes total length
120 | // N bytes header
121 | // N bytes payload
122 |
123 | consumed = default;
124 |
125 | // if the sequence length is less than 4, it means that the request is not complete
126 | if (sequence.Length < 4)
127 | {
128 | return (false, null);
129 | }
130 |
131 | // read the total length of the request,
132 | // if the sequence length is less than the total length,
133 | // it means that the request is not complete
134 | var reader = new SequenceReader(sequence);
135 | reader.TryReadLittleEndian(out int totalLength);
136 | if (sequence.Length < totalLength + 4)
137 | {
138 | return (false, null);
139 | }
140 |
141 | // Deserialize the request
142 | var request = reuse ?? new AppRequest();
143 | request.TotalLength = totalLength;
144 |
145 | // read id
146 | reader.TryReadLittleEndian(out long id);
147 | request.Id = (ulong)id;
148 |
149 | // read path
150 | if (reader.TryReadTo(out ReadOnlySequence pathSequence, (byte)'\0'))
151 | {
152 | request.Path = Encoding.UTF8.GetString(pathSequence.ToArray());
153 | }
154 |
155 | // read payload type
156 | reader.TryReadLittleEndian(out int payloadType);
157 | request.PayloadType = (PayloadType)payloadType;
158 |
159 | // read payload length
160 | reader.TryReadLittleEndian(out int payloadLength);
161 |
162 | // for performance reasons, we use the PooledArray to store the payload
163 | // PooledArray use memory pool
164 | var payload = new PooledArray(payloadLength);
165 | reader.Sequence.Slice(reader.Position, payloadLength).CopyTo(payload.Span);
166 |
167 | // The payload is stored in the PooledArray,
168 | // so we need to register the PooledArray to the DisposeManager,
169 | // when the AppRequest is disposed, the PooledArray will be automatically returned to the memory pool
170 | request.RegisterForDispose(payload);
171 | request.Payload = payload;
172 | reader.Advance(payloadLength);
173 |
174 | // consumed
175 | consumed = reader.Position;
176 |
177 | return (true, request);
178 | }
179 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Protocol/Protocol/AppResponse.cs:
--------------------------------------------------------------------------------
1 | using System.Buffers;
2 | using System.Buffers.Binary;
3 | using SatelliteRpc.Shared;
4 | using SatelliteRpc.Shared.Collections;
5 |
6 | namespace SatelliteRpc.Protocol.Protocol;
7 |
8 | ///
9 | /// AppResponse
10 | ///
11 | public class AppResponse : DisposeManager
12 | {
13 | ///
14 | /// Total length of the request
15 | ///
16 | public int TotalLength { get; set; }
17 |
18 | ///
19 | /// Request Id, used to match the request
20 | /// for multiplexing
21 | ///
22 | public ulong Id { get; set; }
23 |
24 | ///
25 | /// Response status
26 | ///
27 | ///
28 | public ResponseStatus Status { get; set; }
29 |
30 | ///
31 | /// Payload type
32 | ///
33 | public PayloadType PayloadType { get; set; }
34 |
35 | ///
36 | /// Payload
37 | /// if the payload is empty, then the PayloadWriter is used
38 | /// note: because performance reasons, we use the PayloadWriter to write the payload to the network buffer.
39 | ///
40 | public PooledArray Payload { get; set; }
41 |
42 | ///
43 | /// Payload writer
44 | ///
45 | public PayloadWriter PayloadWriter { get; set; } = default;
46 |
47 | ///
48 | /// Get this request size
49 | /// except for the first 4 bytes
50 | ///
51 | ///
52 | public (int HeadLength, int PayloadLength) GetSize()
53 | {
54 | // sum every field length
55 | return PayloadWriter.HasPayload
56 | ? (sizeof(ulong) + sizeof(int) + sizeof(int) + sizeof(int), PayloadWriter.GetPayloadSize())
57 | : (sizeof(ulong) + sizeof(int) + sizeof(int) + sizeof(int), Payload.Length);
58 | }
59 |
60 | public void Serialize(IBufferWriter writer)
61 | {
62 | // The data format is as follows:
63 | // 4 bytes total length
64 | // N bytes header
65 | // N bytes payload
66 |
67 | var length = GetSize();
68 | var totalLength = length.HeadLength + length.PayloadLength + 4;
69 |
70 | scoped var span = writer.GetSpan(totalLength);
71 |
72 | // write total length
73 | BinaryPrimitives.WriteInt32LittleEndian(span, length.HeadLength + length.PayloadLength);
74 | span = span[sizeof(int)..];
75 |
76 | // write id
77 | BinaryPrimitives.WriteUInt64LittleEndian(span, Id);
78 | span = span[sizeof(ulong)..];
79 |
80 | // write status
81 | BinaryPrimitives.WriteInt32LittleEndian(span, (int)Status);
82 | span = span[sizeof(int)..];
83 |
84 | // write payload type
85 | BinaryPrimitives.WriteInt32LittleEndian(span, (int)PayloadType);
86 | span = span[sizeof(int)..];
87 |
88 | // write payload length
89 | BinaryPrimitives.WriteInt32LittleEndian(span, length.PayloadLength);
90 | span = span[sizeof(int)..];
91 |
92 | // for performance reasons, we use the PayloadWriter to write the payload to the network buffer
93 | // reduced memory copying
94 | if (!PayloadWriter.HasPayload)
95 | {
96 | Payload.Span.CopyTo(span);
97 | writer.Advance(totalLength);
98 | }
99 | else
100 | {
101 | writer.Advance(length.HeadLength + 4);
102 | PayloadWriter.PayloadWriteTo(writer);
103 | }
104 | }
105 |
106 | ///
107 | /// Try deserialize
108 | ///
109 | ///
110 | ///
111 | ///
112 | ///
113 | public static (bool Success, AppResponse? Request) TryDeserialize(
114 | ReadOnlySequence sequence,
115 | out SequencePosition consumed,
116 | AppResponse? reuse = null)
117 | {
118 | // The data format is as follows:
119 | // 4 bytes total length
120 | // N bytes header
121 | // N bytes payload
122 |
123 | consumed = default;
124 |
125 | // if the sequence length is less than 4, it means that the request is not complete
126 | if (sequence.Length < 4)
127 | {
128 | return (false, null);
129 | }
130 |
131 | // read the total length of the request,
132 | // if the sequence length is less than the total length,
133 | // it means that the request is not complete
134 | var reader = new SequenceReader(sequence);
135 | reader.TryReadLittleEndian(out int totalLength);
136 | if (sequence.Length < totalLength + 4)
137 | {
138 | return (false, null);
139 | }
140 |
141 | // Deserialize the response
142 | var response = reuse ?? new AppResponse();
143 | response.TotalLength = totalLength;
144 |
145 | // read id
146 | reader.TryReadLittleEndian(out long id);
147 | response.Id = (ulong)id;
148 |
149 | // read status
150 | reader.TryReadLittleEndian(out int status);
151 | response.Status = (ResponseStatus)status;
152 |
153 | // read payload type
154 | reader.TryReadLittleEndian(out int payloadType);
155 | response.PayloadType = (PayloadType)payloadType;
156 |
157 | // read payload length
158 | reader.TryReadLittleEndian(out int payloadLength);
159 |
160 | // for performance reasons, we use the PooledArray to store the payload
161 | // PooledArray use memory pool
162 | var payload = new PooledArray(payloadLength);
163 | reader.Sequence.Slice(reader.Position, payloadLength).CopyTo(payload.Span);
164 |
165 | // The payload is stored in the PooledArray,
166 | // so we need to register the PooledArray to the DisposeManager,
167 | // when the AppRequest is disposed, the PooledArray will be automatically returned to the memory pool
168 | response.RegisterForDispose(payload);
169 | response.Payload = payload;
170 |
171 | reader.Advance(payloadLength);
172 |
173 | consumed = reader.Position;
174 |
175 | return (true, response);
176 | }
177 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Protocol/Protocol/Empty.cs:
--------------------------------------------------------------------------------
1 | namespace SatelliteRpc.Protocol.Protocol;
2 |
3 | ///
4 | /// Is a flag, means no payload
5 | ///
6 | public record Empty;
--------------------------------------------------------------------------------
/src/SatelliteRpc.Protocol/Protocol/Login.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package ServerProto;
4 |
5 | message LoginReqProto
6 | {
7 | string user = 1;
8 | string password = 2;
9 | optional int32 Sn = 6;//请求标识
10 | }
11 |
12 | message LoginRespProto
13 | {
14 | bool isOk = 1;
15 | optional string errMsg = 2;
16 | optional int32 Sn = 4;//请求标识
17 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Protocol/Protocol/PayloadType.cs:
--------------------------------------------------------------------------------
1 | namespace SatelliteRpc.Protocol.Protocol;
2 |
3 | ///
4 | /// Payload type
5 | ///
6 | public enum PayloadType
7 | {
8 | ///
9 | /// Protobuf
10 | ///
11 | Protobuf = 0,
12 |
13 | ///
14 | /// Json
15 | ///
16 | Json = 1
17 |
18 | // add more payload type here, like custom payload type
19 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Protocol/Protocol/PayloadWriter.cs:
--------------------------------------------------------------------------------
1 | using System.Buffers;
2 | using System.Diagnostics.CodeAnalysis;
3 |
4 | namespace SatelliteRpc.Protocol.Protocol;
5 |
6 | public struct PayloadWriter
7 | {
8 | ///
9 | /// Empty payload writer
10 | ///
11 | public static readonly PayloadWriter Empty = new PayloadWriter
12 | {
13 | GetPayloadSize = () => 0,
14 | PayloadWriteTo = _ => { }
15 | };
16 |
17 | ///
18 | /// Has payload
19 | ///
20 | [MemberNotNullWhen(true, nameof(GetPayloadSize), nameof(PayloadWriteTo))]
21 | public bool HasPayload => PayloadWriteTo is not null && GetPayloadSize is not null;
22 |
23 | ///
24 | /// Get or set calculate payload size method
25 | ///
26 | public Func? GetPayloadSize { get; set; }
27 |
28 | ///
29 | /// Get or set payload write to method
30 | ///
31 | public Action>? PayloadWriteTo { get; set; }
32 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Protocol/Protocol/ResponseStatus.cs:
--------------------------------------------------------------------------------
1 | namespace SatelliteRpc.Protocol.Protocol;
2 |
3 | ///
4 | /// Response status
5 | ///
6 | public enum ResponseStatus
7 | {
8 | ///
9 | /// Success
10 | ///
11 | Success = 0,
12 |
13 | ///
14 | /// Not found
15 | /// it means the request is not found service or method
16 | ///
17 | NotFound = 1,
18 |
19 | ///
20 | /// Bad request
21 | /// it means the request is not valid
22 | ///
23 | BadRequest = 2,
24 |
25 | ///
26 | /// Internal error
27 | /// it means the request is valid, but the server has an internal error
28 | ///
29 | InternalError = 3
30 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Protocol/SatelliteRpc.Protocol.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net7.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 | all
14 | runtime; build; native; contentfiles; analyzers; buildtransitive
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/SatelliteRpc.Server/Configuration/IRpcServerBuilder.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 |
3 | namespace SatelliteRpc.Server.Configuration;
4 |
5 | ///
6 | /// Builder for rpc server
7 | ///
8 | public interface IRpcServerBuilder
9 | {
10 | ///
11 | /// Service collection
12 | ///
13 | IServiceCollection Services { get; }
14 |
15 | ///
16 | /// Configure satellite rpc server
17 | ///
18 | ///
19 | ///
20 | IRpcServerBuilder ConfigureSatelliteRpcServer(Action configure);
21 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Server/Configuration/RpcServerBuilder.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 |
3 | namespace SatelliteRpc.Server.Configuration;
4 |
5 | ///
6 | /// Class that provides the functionality to construct an RPC server.
7 | /// This class implements the IRpcServerBuilder interface.
8 | ///
9 | public class RpcServerBuilder : IRpcServerBuilder
10 | {
11 | ///
12 | /// Gets the service collection that this RPC server will use.
13 | ///
14 | public IServiceCollection Services { get; }
15 |
16 | ///
17 | /// Constructs a new instance of RpcServerBuilder with the provided service collection.
18 | ///
19 | /// The service collection to use for this RPC server.
20 | public RpcServerBuilder(IServiceCollection services)
21 | {
22 | Services = services;
23 | }
24 |
25 | ///
26 | /// Configures the SatelliteRpcServer with the provided options.
27 | ///
28 | /// An action that configures the options for the SatelliteRpcServer.
29 | /// Returns the IRpcServerBuilder instance after it has been configured.
30 | public IRpcServerBuilder ConfigureSatelliteRpcServer(Action configure)
31 | {
32 | Services.Configure(configure);
33 | return this;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/SatelliteRpc.Server/Configuration/SatelliteRpcServerOptions.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 |
3 | namespace SatelliteRpc.Server.Configuration;
4 |
5 | public class SatelliteRpcServerOptions
6 | {
7 | ///
8 | /// Rpc server listen port
9 | ///
10 | public int Port { get; set; } = 58888;
11 |
12 | ///
13 | /// Rpc server listen address
14 | ///
15 | public IPAddress Host { get; set; } = IPAddress.Parse("127.0.0.1");
16 |
17 | ///
18 | /// Response write channel max count
19 | ///
20 | public int WriteChannelMaxCount { get; set; } = 1024;
21 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Server/Exceptions/NotFoundException.cs:
--------------------------------------------------------------------------------
1 | namespace SatelliteRpc.Server.Exceptions;
2 |
3 | ///
4 | /// Not found
5 | /// it means the request is not found service or method
6 | ///
7 | public class NotFoundException : ApplicationException
8 | {
9 | public NotFoundException(string? message) : base(message)
10 | {
11 | }
12 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Server/Exceptions/ParametersBindException.cs:
--------------------------------------------------------------------------------
1 | namespace SatelliteRpc.Server.Exceptions;
2 |
3 | ///
4 | /// Parameters bind exception
5 | /// it means the request parameters bind error, maybe the request parameters is not valid
6 | ///
7 | public class ParametersBindException : ApplicationException
8 | {
9 | public ParametersBindException(string? message, Exception? innerException) : base(message, innerException)
10 | {
11 | }
12 |
13 | public ParametersBindException(string? message) : base(message)
14 | {
15 | }
16 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Server/Extensions/HostBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Hosting;
2 | using SatelliteRpc.Server.Configuration;
3 |
4 | namespace SatelliteRpc.Server.Extensions;
5 |
6 | ///
7 | /// The IHostBuilder extensions
8 | ///
9 | public static class HostBuilderExtensions
10 | {
11 | ///
12 | /// Use satellite rpc server
13 | ///
14 | ///
15 | ///
16 | ///
17 | public static IHostBuilder UseSatelliteRpcServer(
18 | this IHostBuilder builder,
19 | Action? configure = null)
20 | {
21 | // we use kestrel server, so we need to use web host
22 | builder.ConfigureServices(services =>
23 | {
24 | var serverBuilder = new RpcServerBuilder(services);
25 | configure?.Invoke(serverBuilder);
26 | serverBuilder.AddRpcService();
27 | serverBuilder.AddRpcConnectionHandler();
28 | serverBuilder.AddRpcServiceMiddleware();
29 | serverBuilder.ConfigureSatelliteRpcServer(_ => { });
30 | }).ConfigureWebHostDefaults(webBuilder => { webBuilder.UseRpcKestrelServer(); });
31 |
32 |
33 | return builder;
34 | }
35 | }
--------------------------------------------------------------------------------
/src/SatelliteRpc.Server/Extensions/RpcServerBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | using SatelliteRpc.Server.Configuration;
2 | using SatelliteRpc.Server.RpcService;
3 | using SatelliteRpc.Server.Transport;
4 | using SatelliteRpc.Shared.Application;
5 |
6 | namespace SatelliteRpc.Server.Extensions;
7 |
8 | ///
9 | /// The IRpcServerBuilder extensions
10 | ///
11 | public static class RpcServerBuilderExtensions
12 | {
13 | ///
14 | /// Adds a RPC connection handler to the RPC server builder.
15 | ///
16 | /// The RPC server builder.
17 | /// An optional action to configure the application builder.
18 | /// The RPC server builder with the added connection handler.
19 | public static IRpcServerBuilder AddRpcConnectionHandler(
20 | this IRpcServerBuilder builder,
21 | Action>? configure = null)
22 | {
23 | builder.Services.AddRpcConnectionHandler(configure);
24 | return builder;
25 | }
26 |
27 | ///
28 | /// Adds a RPC service to the RPC server builder.
29 | ///
30 | /// The RPC server builder.
31 | /// The RPC server builder with the added RPC service.
32 | public static IRpcServerBuilder AddRpcService(this IRpcServerBuilder builder)
33 | {
34 | builder.Services.AddRpcService();
35 | return builder;
36 | }
37 |
38 | ///
39 | /// Adds a RPC service middleware to the RPC server builder.
40 | ///
41 | /// The RPC server builder.
42 | /// An optional action to configure the application builder.
43 | /// The RPC server builder with the added service middleware.
44 | public static IRpcServerBuilder AddRpcServiceMiddleware(
45 | this IRpcServerBuilder builder,
46 | Action>? configure = null)
47 | {
48 | builder.Services.AddRpcServiceMiddleware(configure);
49 | return builder;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/SatelliteRpc.Server/Extensions/ServiceCollectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using Microsoft.Extensions.DependencyInjection;
3 | using Microsoft.Extensions.DependencyInjection.Extensions;
4 | using SatelliteRpc.Protocol.PayloadConverters;
5 | using SatelliteRpc.Server.RpcService;
6 | using SatelliteRpc.Server.RpcService.DataExchange;
7 | using SatelliteRpc.Server.RpcService.Endpoint;
8 | using SatelliteRpc.Server.RpcService.Middleware;
9 | using SatelliteRpc.Server.Transport;
10 | using SatelliteRpc.Shared.Application;
11 |
12 | namespace SatelliteRpc.Server.Extensions;
13 |
14 | ///
15 | /// Contains extension methods for IServiceCollection to add RPC-related services.
16 | ///
17 | public static class ServiceCollectionExtensions
18 | {
19 | ///
20 | /// Adds a singleton instance of the service to the specified .
21 | ///
22 | /// The to add the service to.
23 | /// An optional configuration action to apply to the .
24 | /// The same service collection so that multiple calls can be chained.
25 | public static IServiceCollection AddRpcConnectionHandler(
26 | this IServiceCollection services,
27 | Action>? configure = null)
28 | {
29 | services.TryAddSingleton(
30 | provider => new RpcConnectionApplicationHandlerBuilder(provider, configure));
31 | return services;
32 | }
33 |
34 | ///
35 | /// Adds a singleton instance of the service to the specified .
36 | ///
37 | /// The to add the service to.
38 | /// An optional configuration action to apply to the .
39 | /// The same service collection so that multiple calls can be chained.
40 | public static IServiceCollection AddRpcServiceMiddleware(
41 | this IServiceCollection services,
42 | Action>? configure = null)
43 | {
44 | services.TryAddSingleton(
45 | provider => new RpcServiceMiddlewareBuilder(provider, configure));
46 | return services;
47 | }
48 |
49 | ///
50 | /// Adds various RPC services to the specified .
51 | ///
52 | /// The to add the services to.
53 | /// The same service collection so that multiple calls can be chained.
54 | public static IServiceCollection AddRpcService(this IServiceCollection services)
55 | {
56 | services.TryAddSingleton();
57 | services.TryAddSingleton();
58 | services.TryAddSingleton();
59 | services.TryAddSingleton();
60 | services.TryAddSingleton(provider =>
61 | {
62 | // Search the entry assembly, find RPC services that implement IRpcService to build RpcServiceEndpointDataSource
63 | var rpcServiceType = typeof(IRpcService);
64 | var rpcServiceTypes = Assembly
65 | .GetEntryAssembly()!
66 | .GetTypes()
67 | .Where(type => rpcServiceType.IsAssignableFrom(type) && !type.IsAbstract);
68 |
69 | var dataSource = ActivatorUtilities.CreateInstance(provider);
70 | foreach (var serviceType in rpcServiceTypes)
71 | {
72 | // Get all public methods of the RPC service
73 | var methods =
74 | serviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
75 |
76 | foreach (var method in methods)
77 | {
78 | // Add endpoint to the data source
79 | dataSource.AddEndpoint(RpcServiceEndpoint.FromMethodInfo(serviceType, method));
80 | }
81 | }
82 |
83 | return dataSource;
84 | });
85 |
86 | return services;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/SatelliteRpc.Server/Extensions/WebHostBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Connections;
2 | using Microsoft.AspNetCore.Hosting;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using Microsoft.Extensions.Options;
5 | using SatelliteRpc.Server.Configuration;
6 | using SatelliteRpc.Server.Transport;
7 |
8 | namespace SatelliteRpc.Server.Extensions;
9 |
10 | ///
11 | /// Provides extension methods for the IWebHostBuilder interface to configure RPC services.
12 | ///
13 | public static class WebHostBuilderExtensions
14 | {
15 | ///
16 | /// Configures the specified IWebHostBuilder to use the Kestrel server and RPC services.
17 | ///
18 | /// The IWebHostBuilder to configure.
19 | /// The IWebHostBuilder that was passed into the method, with the configuration applied.
20 | public static IWebHostBuilder UseRpcKestrelServer(
21 | this IWebHostBuilder builder)
22 | {
23 | builder.UseKestrel(options =>
24 | {
25 | var serverOptions = options.ApplicationServices.GetService>();
26 |
27 | // Configure Kestrel to listen on the specified host and port,
28 | // and to use the RpcConnectionHandler for handling connections
29 | options.Listen(
30 | serverOptions!.Value.Host,
31 | serverOptions.Value.Port,
32 | listenOptions => { listenOptions.UseConnectionHandler(); });
33 | });
34 |
35 | // Configure the application to use the RpcMvcMiddleware
36 | // and ignore the default middleware
37 | builder.Configure(_ => { });
38 |
39 | return builder;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/SatelliteRpc.Server/Observability/ObservabilityMiddleware.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using SatelliteRpc.Server.RpcService;
3 | using SatelliteRpc.Server.RpcService.Middleware;
4 | using SatelliteRpc.Shared.Application;
5 |
6 | namespace SatelliteRpc.Server.Observability;
7 |
8 | ///
9 | /// Middleware for providing observability support to service requests.
10 | ///
11 | public class ObservabilityMiddleware : IRpcServiceMiddleware
12 | {
13 | ///
14 | /// Invokes the middleware with the specified context.
15 | ///
16 | /// The delegate representing the remaining middleware in the request pipeline.
17 | /// The context of the current service request.
18 | /// A task representing the asynchronous operation.
19 | public async ValueTask InvokeAsync(ApplicationDelegate next, ServiceContext context)
20 | {
21 | // Create a new Activity for the RPC request, adding relevant information as tags.
22 | var activity = new Activity("RPC request")
23 | .AddTag("requestId", context.RawContext.Request.Id)
24 | .AddTag("method", context.RawContext.Request.Path)
25 | .AddTag("service", context.Endpoint.ServiceName);
26 |
27 | try
28 | {
29 | activity.Start();
30 | await next(context);
31 | }
32 | catch (Exception ex)
33 | {
34 | // If an exception occurs, add it as an event to the activity
35 | activity.AddEvent(new ActivityEvent("Exception occurred",
36 | tags: new ActivityTagsCollection(new[]
37 | {
38 | new KeyValuePair("exception", ex)
39 | }!)));
40 | throw;
41 | }
42 | finally
43 | {
44 | activity.Stop();
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/SatelliteRpc.Server/RpcService/DataExchange/DefaultRpcDataExchange.cs:
--------------------------------------------------------------------------------
1 | using SatelliteRpc.Protocol.PayloadConverters;
2 | using SatelliteRpc.Protocol.Protocol;
3 | using SatelliteRpc.Server.Exceptions;
4 | using SatelliteRpc.Server.RpcService.Endpoint;
5 | using SatelliteRpc.Server.Transport;
6 |
7 | namespace SatelliteRpc.Server.RpcService.DataExchange;
8 |
9 | ///
10 | /// Default implementation of the IRpcDataExchange interface.
11 | /// This class is responsible for converting payloads to parameters and vice versa for RPC calls.
12 | ///
13 | public class DefaultRpcDataExchange : IRpcDataExchange
14 | {
15 | ///
16 | /// Source of payload converters. Used to convert payloads to parameters and vice versa.
17 | ///
18 | private readonly PayloadConverterSource _converterSource;
19 |
20 | ///
21 | /// Initializes a new instance of the class.
22 | ///
23 | /// The source of payload converters.
24 | public DefaultRpcDataExchange(PayloadConverterSource converterSource)
25 | {
26 | _converterSource = converterSource;
27 | }
28 |
29 | ///
30 | /// Binds the parameters for an RPC service endpoint.
31 | /// Converts the payload in the raw context to the types expected by the endpoint.
32 | ///
33 | /// The RPC service endpoint.
34 | /// The raw RPC context.
35 | /// An array of parameters to be used in the RPC call.
36 | public object?[] BindParameters(RpcServiceEndpoint endpoint, RpcRawContext rawContext)
37 | {
38 | try
39 | {
40 | var parameters = new object?[endpoint.ParameterTypes.Length];
41 | for (var i = 0; i < endpoint.ParameterTypes.Length; i++)
42 | {
43 | var parameterType = endpoint.ParameterTypes[i];
44 |
45 | // Special case for CancellationToken, use the cancellation token from the raw context
46 | if (parameterType == typeof(CancellationToken) || parameterType == typeof(CancellationToken?))
47 | {
48 | parameters[i] = rawContext.Cancel;
49 | }
50 | else
51 | {
52 | var converter = _converterSource.GetConverter(rawContext.Request.PayloadType);
53 | parameters[i] = converter.Convert(rawContext.Request.Payload, parameterType);
54 | }
55 | }
56 |
57 | return parameters;
58 | }
59 | catch (Exception ex)
60 | {
61 | throw new ParametersBindException("Failed to bind parameters", ex);
62 | }
63 | }
64 |
65 | ///
66 | /// Gets a payload writer for the given result and raw context.
67 | /// The payload writer is used to convert the result of an RPC call to a payload.
68 | ///
69 | /// The result of the RPC call.
70 | /// The raw RPC context.
71 | /// A payload writer for the result.
72 | public PayloadWriter GetPayloadWriter(object? result, RpcRawContext rawContext)
73 | {
74 | var converter = _converterSource.GetConverter(rawContext.Request.PayloadType);
75 | return converter.CreatePayloadWriter(result);
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/SatelliteRpc.Server/RpcService/DataExchange/IRpcDataExchange.cs:
--------------------------------------------------------------------------------
1 | using SatelliteRpc.Protocol.Protocol;
2 | using SatelliteRpc.Server.RpcService.Endpoint;
3 | using SatelliteRpc.Server.Transport;
4 |
5 | namespace SatelliteRpc.Server.RpcService.DataExchange;
6 |
7 | ///
8 | /// The IRpcDataExchange interface defines the methods needed to bind parameters for RPC calls and to
9 | /// get a payload writer for the results of those calls.
10 | ///
11 | public interface IRpcDataExchange
12 | {
13 | ///
14 | /// Binds the parameters for an RPC service call.
15 | ///
16 | /// The endpoint of the RPC service.
17 | /// The raw context of the RPC call, which contains the raw payload.
18 | /// An array of objects representing the parameters to be used for the RPC call.
19 | object?[] BindParameters(RpcServiceEndpoint endpoint, RpcRawContext rawContext);
20 |
21 | ///
22 | /// Gets a PayloadWriter to write the result of an RPC call to a payload.
23 | ///
24 | /// The result of the RPC call.
25 | /// The raw context of the RPC call, which contains the raw payload.
26 | /// A PayloadWriter that can be used to write the result to a payload.
27 | PayloadWriter GetPayloadWriter(object? result, RpcRawContext rawContext);
28 | }
29 |
--------------------------------------------------------------------------------
/src/SatelliteRpc.Server/RpcService/Endpoint/DefaultRpcEndPointResolver.cs:
--------------------------------------------------------------------------------
1 | using SatelliteRpc.Server.Exceptions;
2 |
3 | namespace SatelliteRpc.Server.RpcService.Endpoint;
4 |
5 | ///
6 | /// The DefaultRpcEndPointResolver class is an implementation of the IEndpointResolver interface.
7 | /// It resolves RPC service endpoints using a data source.
8 | ///
9 | public class DefaultRpcEndPointResolver : IEndpointResolver
10 | {
11 | // The data source used to resolve endpoints.
12 | private readonly RpcServiceEndpointDataSource _dataSource;
13 |
14 | ///
15 | /// Initializes a new instance of the DefaultRpcEndPointResolver class.
16 | ///
17 | /// The data source used to resolve endpoints.
18 | public DefaultRpcEndPointResolver(RpcServiceEndpointDataSource dataSource)
19 | {
20 | _dataSource = dataSource;
21 | }
22 |
23 | ///
24 | /// Gets an RPC service endpoint for a given path.
25 | ///
26 | /// The path for which to get the endpoint.
27 | /// The RPC service endpoint for the given path.
28 | /// Thrown when no endpoint is found for the given path.
29 | public RpcServiceEndpoint GetEndpoint(string path)
30 | {
31 | var endPoint = _dataSource.GetEndpoint(path);
32 | if (endPoint is null)
33 | {
34 | throw new NotFoundException($"No endpoint found for path: {path}");
35 | }
36 |
37 | return endPoint;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/SatelliteRpc.Server/RpcService/Endpoint/EndpointInvokeMiddleware.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using SatelliteRpc.Server.RpcService.Middleware;
3 | using SatelliteRpc.Shared.Application;
4 |
5 | namespace SatelliteRpc.Server.RpcService.Endpoint;
6 |
7 | ///
8 | /// Middleware for invoking an endpoint in a RPC service.
9 | /// This class is responsible for retrieving the service instance associated with a given endpoint and invoking it.
10 | ///
11 | public class EndpointInvokeMiddleware : IRpcServiceMiddleware
12 | {
13 | ///
14 | /// The service provider used to resolve service instances.
15 | ///
16 | private readonly IServiceProvider _serviceProvider;
17 |
18 | ///
19 | /// Initializes a new instance of the class.
20 | ///
21 | /// The service provider used to resolve service instances.
22 | public EndpointInvokeMiddleware(IServiceProvider serviceProvider)
23 | {
24 | _serviceProvider = serviceProvider;
25 | }
26 |
27 | ///
28 | /// Invokes the middleware in the RPC service pipeline.
29 | ///
30 | /// The next middleware delegate in the pipeline. Currently not used in this middleware.
31 | /// The context for the current service call.
32 | /// A that represents the asynchronous operation.
33 | public async ValueTask InvokeAsync(ApplicationDelegate _, ServiceContext context)
34 | {
35 | var endpoint = context.Endpoint;
36 | var endpointService = ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, endpoint.ServiceType);
37 |
38 | // Check if the endpoint's return type is Task
39 | if (endpoint.ReturnIsTask)
40 | {
41 | // If it is, just invoke the endpoint and await the result
42 | await endpoint.InvokeAsync(endpointService, context.Arguments);
43 | }
44 | else
45 | {
46 | // If it's not, invoke the endpoint, await the result, and store it in the context
47 | var result = await endpoint.InvokeAsync(endpointService, context.Arguments);
48 | context.Result = result;
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/SatelliteRpc.Server/RpcService/Endpoint/IEndpointResolver.cs:
--------------------------------------------------------------------------------
1 | namespace SatelliteRpc.Server.RpcService.Endpoint;
2 |
3 | ///
4 | /// Defines a mechanism for resolving RPC service endpoints.
5 | ///
6 | public interface IEndpointResolver
7 | {
8 | ///
9 | /// Resolves the for a given path.
10 | ///
11 | /// The path for which to resolve the endpoint.
12 | /// The resolved .
13 | RpcServiceEndpoint GetEndpoint(string path);
14 | }
15 |
--------------------------------------------------------------------------------
/src/SatelliteRpc.Server/RpcService/Endpoint/RpcServiceEndpoint.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 |
3 | namespace SatelliteRpc.Server.RpcService.Endpoint;
4 |
5 | ///
6 | /// Represents a remote procedure call (RPC) service endpoint.
7 | ///
8 | public class RpcServiceEndpoint
9 | {
10 | ///
11 | /// Gets the name of the service.
12 | ///
13 | public string ServiceName { get; }
14 |
15 | ///
16 | /// Gets the name of the method.
17 | ///
18 | public string MethodName { get; }
19 |
20 | ///
21 | /// Gets the type of the service.
22 | ///
23 | public Type ServiceType { get; }
24 |
25 | ///
26 | /// Gets the types of the parameters of the method.
27 | ///
28 | public Type[] ParameterTypes { get; }
29 |
30 | ///
31 | /// Gets the return type of the method.
32 | ///
33 | public Type ReturnType { get; }
34 |
35 | ///
36 | /// Gets a value indicating whether the return type is a Task.
37 | ///
38 | public bool ReturnIsTask => ReturnType == typeof(Task);
39 |
40 | ///
41 | /// Gets a value indicating whether the return type is a generic Task.
42 | ///
43 | public bool ReturnIsTaskT => ReturnType.IsGenericType && ReturnType.GetGenericTypeDefinition() == typeof(Task<>);
44 |
45 | ///
46 | /// Gets the method invoker function.
47 | ///
48 | public Func