Skip to content

Commit d4ca12e

Browse files
authored
Merge pull request #168 from dotnet-campus/t/lvyi/doc
写一个更容易入门的 IPC 对象文档
2 parents 6d087eb + 7c2dd9f commit d4ca12e

File tree

9 files changed

+323
-9
lines changed

9 files changed

+323
-9
lines changed

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@
1111

1212
库中提供了较为底层的通信方案,也提供了高级的封装方案(基于Json数据格式的通信方案),完整文档可参阅:
1313

14-
- [使用 .NET Remoting 模式的对象远程调用的 IPC 通讯方式](./docs/IpcRemotingObject.md)
15-
- [使用直接路由和 Json 通讯格式的 IPC 通讯方式](./docs/JsonIpcDirectRouted.md)
14+
- 🌀 远程对象调用
15+
- [基础入门教程](./docs/IpcObject.01.md)
16+
- [通讯方式详解](./docs/IpcObject.02.md)
17+
- 🌐 直接路由
18+
- [通讯方式详解](./docs/JsonIpcDirectRouted.md)
1619

1720
### 案例:直接路由Json通信(需要2.0.0-alpha版本以上)
1821

demo/IpcRemotingObjectDemo/IpcRemotingObjectClientDemo/IFoo.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,24 @@
22

33
namespace IpcRemotingObjectServerDemo; // Must the same namespace
44

5+
/// <summary>
6+
/// 可跨进程调用的接口演示。
7+
/// </summary>
58
[IpcPublic]
6-
interface IFoo
9+
public interface IFoo
710
{
11+
/// <summary>
12+
/// 属性演示。支持 get/set 属性、get 只读属性,支持跨进程报告异常。
13+
/// </summary>
14+
string Name { get; set; }
15+
16+
/// <summary>
17+
/// 方法演示。支持参数、返回值,支持跨进程报告异常。
18+
/// </summary>
819
int Add(int a, int b);
20+
21+
/// <summary>
22+
/// 异步方法(更推荐)演示。支持参数、返回值,支持跨进程报告异常。
23+
/// </summary>
924
Task<string> AddAsync(string a, int b);
1025
}

demo/IpcRemotingObjectDemo/IpcRemotingObjectServerDemo/Foo.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
class Foo : IFoo
44
{
5+
public string Name { get; set; } = "Foo";
6+
57
public int Add(int a, int b)
68
{
79
Console.WriteLine($"a({a})+b({b})={a + b}");

demo/IpcRemotingObjectDemo/IpcRemotingObjectServerDemo/IFoo.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,24 @@
22

33
namespace IpcRemotingObjectServerDemo;
44

5+
/// <summary>
6+
/// 可跨进程调用的接口演示。
7+
/// </summary>
58
[IpcPublic(IgnoresIpcException = true, Timeout = 1000)]
6-
interface IFoo
9+
public interface IFoo
710
{
11+
/// <summary>
12+
/// 属性演示。支持 get/set 属性、get 只读属性,支持跨进程报告异常。
13+
/// </summary>
14+
string Name { get; set; }
15+
16+
/// <summary>
17+
/// 方法演示。支持参数、返回值,支持跨进程报告异常。
18+
/// </summary>
819
int Add(int a, int b);
920

21+
/// <summary>
22+
/// 异步方法(更推荐)演示。支持参数、返回值,支持跨进程报告异常。
23+
/// </summary>
1024
Task<string> AddAsync(string a, int b);
1125
}

docs/IpcObject.01.md

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
# 使用「远程对象调用」的方式做进程间通信
2+
3+
假设你有一个接口 `IFoo`,有 A、B 两个进程想做进程间通信。通过 IPC 的「远程对象调用」的方式,你可以让 A 进程调用 B 进程的 `IFoo` 接口方法,就像调用本地对象一样。
4+
5+
## 快速入门
6+
7+
先定义好一个接口,这个接口将可被远程调用:
8+
9+
```csharp
10+
/// <summary>
11+
/// 可跨进程调用的接口演示。
12+
/// </summary>
13+
[IpcPublic]
14+
public interface IFoo
15+
{
16+
/// <summary>
17+
/// 属性演示。支持 get/set 属性、get 只读属性,支持跨进程报告异常。
18+
/// </summary>
19+
string Name { get; set; }
20+
21+
/// <summary>
22+
/// 方法演示。支持参数、返回值,支持跨进程报告异常。
23+
/// </summary>
24+
int Add(int a, int b);
25+
26+
/// <summary>
27+
/// 异步方法(更推荐)演示。支持参数、返回值,支持跨进程报告异常。
28+
/// </summary>
29+
Task<string> AddAsync(string a, int b);
30+
}
31+
```
32+
33+
现在,我们有两个进程 A 和 B:
34+
35+
- A 进程是调用端,想调用 B 进程的 `IFoo` 接口方法。
36+
- B 进程是被调用端,提供 `IFoo` 接口的实现。
37+
38+
为了实现这样的跨进程调用,我们需要在 A 进程和 B 进程分别进行一些 IPC 的初始化。
39+
40+
```csharp
41+
// A 进程 Program.cs
42+
43+
// 1. 初始化 IPC
44+
var ipcProvider = new IpcProvider("IPC-A");
45+
// 2. 启动 IPC(以支持双向通信)
46+
ipcProvider.StartServer();
47+
// 3. 连接进程 B
48+
var peer = await ipcProvider.GetAndConnectToPeerAsync("IPC-B");
49+
50+
// 获取来自 B 进程的 IFoo 接口的「代理」(Proxy)
51+
var foo = ipcProvider.CreateIpcProxy<IFoo>(peer);
52+
53+
// 现在,开始自由地享受 IFoo 的远程调用吧!就好像它从来都是自己进程内的对象一样
54+
Console.WriteLine(foo.Name);
55+
Console.WriteLine(foo.Add(1, 2));
56+
Console.WriteLine(await foo.AddAsync("a", 1));
57+
Console.Read();
58+
```
59+
60+
```csharp
61+
// B 进程 Program.cs
62+
63+
// 1. 初始化 IPC
64+
var ipcProvider = new IpcProvider("IPC-B");
65+
66+
// 创建 IFoo 的实际对象,然后为其创建一个「对接」(Joint)
67+
ipcProvider.CreateIpcJoint<IFoo>(new Foo());
68+
69+
// 2. 启动 IPC(以支持双向通信)
70+
// 这里,我们提前创建好了「对接」,再启动 IPC;这样 A 进程连接 B 进程时,就能马上使用 IFoo 了
71+
ipcProvider.StartServer();
72+
73+
Console.Read();
74+
```
75+
76+
```csharp
77+
// B 进程,IFoo 的实际实现
78+
class Foo : IFoo
79+
{
80+
public string Name { get; set; } = "Foo";
81+
82+
public int Add(int a, int b)
83+
{
84+
Console.WriteLine($"a({a})+b({b})={a + b}");
85+
return a + b;
86+
}
87+
88+
public async Task<string> AddAsync(string a, int b)
89+
{
90+
return await Task.Run(() =>
91+
{
92+
Console.WriteLine($"a({a})+b({b})={a + b}");
93+
return a + b;
94+
});
95+
}
96+
}
97+
```
98+
99+
以上,就是「远程对象调用」所需要的所有示例代码了。所有代码来自本仓库 <https://github.com/dotnet-campus/dotnetCampus.Ipc/tree/main/demo/IpcRemotingObjectDemo>
100+
101+
## 进阶用法
102+
103+
### `IpcPublic`
104+
105+
上述 IPC 初始化的部分不变,实现不变的情况下,接口 `IFoo` 上的 `[IpcPublic]` 接口还有更多玩法。
106+
107+
```csharp
108+
// IgnoresIpcException = true
109+
// - A 进程调用 IFoo 的「代理」时,忽略所有的 IPC 异常(即对方进程断开、IFoo 接口出现方法签名的变更等)
110+
// - 但 A 进程仍然能收到 B 进程 IFoo 实现类的业务异常(如 ArgumentNullException)
111+
// Timeout = 1000
112+
// - A 进程调用 IFoo 的「代理」时,最多等待 1000 毫秒
113+
// - 超出时间没有返回,则方法会立即返回;如果方法带有返回值,则会返回返回类型的默认值
114+
[IpcPublic(IgnoresIpcException = true, Timeout = 1000)]
115+
```
116+
117+
这些是对 `IFoo` 这个接口的全局设置。当然,还可以对它内部的每个成员单独设置更多属性:
118+
119+
```csharp
120+
// DefaultReturn = "Error"
121+
// - A 进程调用 IFoo「代理」的此方法时,如果真发生了 IPC 异常,则会返回指定的默认值
122+
// - 在这里,是返回 "Error",而不是 string 的默认值 null(这可以避免破坏可空性)
123+
[IpcMethod(DefaultReturn = "Error", IgnoresIpcException = true, Timeout = 2000)]
124+
Task<string> AddAsync(string a, int b);
125+
```
126+
127+
特别的,对于 void 返回值的方法,还有一个属性 `WaitsVoid`
128+
129+
```csharp
130+
// WaitsVoid = true
131+
// - 默认情况下,IPC 不会等待 void 方法返回(因为库作者 @walterlv 认为如果你想等待,改用异步方法更好)
132+
// - 这意味着你甚至还收不到 B 进程此方法实现的异常
133+
// - 但如果你确实希望这个 void 像一个本地 void 一样等待,可以设置此属性
134+
[IpcMethod(WaitsVoid = true)]
135+
void Add(int a, int b);
136+
```
137+
138+
哦,对了,属性上也有属性可以设置哦:
139+
140+
```csharp
141+
// IsReadonly = true
142+
// - 神奇吧,一个 get/set 属性设成只读有什么作用?
143+
// - 这意味着 A 进程通过「代理」获取此属性的值时,会假设此属性不会再变了,于是缓存起来,只拿这一次;以后都使用这次的缓存
144+
[IpcProperty(IsReadonly = true)]
145+
string Name { get; set; }
146+
```
147+
148+
你可能注意到我们还有一个 `IpcEvent` 可以标在事件上,不过很遗憾地告诉你,目前还没实现事件。所以你会看到我们写了个分析器告诉你不要这么做。
149+
150+
### `IpcShape`
151+
152+
好了,现在更麻烦的事来了。假设你有三个进程 A、B、C:
153+
154+
- A 仍然是调用端
155+
- B 仍然是被调用端
156+
- 新增了一个 C,跟 A 一样是调用端,但希望用不同的方式调用 `IFoo` 这个接口怎么办?
157+
158+
我们前面介绍了 `[IpcPublic]` 特性,它可以用来标记接口及其成员,以详细定制各个成员的 IPC 行为。但是,它一旦在接口上标记了,就意味着所有进程对这个接口的调用都会遵循这个标记的规则。
159+
160+
有没有什么方法,能够允许我 A 和 C 进程使用不同的规则来调用 `IFoo` 接口呢?
161+
162+
答案就是 `[IpcShape]` 特性:
163+
164+
- 你可以在 C 进程里额外定义一个 `IFoo` 接口的空实现
165+
- 然后逐一设置这个空实现中你希望与 `IFoo` 接口中所定义的不同的调用规则
166+
167+
```csharp
168+
[IpcShape(typeof(IFoo))]
169+
internal class IpcFooShape : IFoo
170+
{
171+
// IFoo 接口上没有设置此属性,所以 A 进程是默认方式访问这个属性的
172+
// 但是 C 进程通过 IpcShape,在不影响 A 进程访问规则的情况下,定制了 C 进程下的访问规则
173+
[IpcProperty(IsReadonly = true)]
174+
public string Name { get; set; } = null!;
175+
176+
public int Add(int a, int b) => throw null!;
177+
public async Task<string> AddAsync(string a, int b) => throw null!;
178+
}
179+
```
180+
181+
那么 C 进程的初始化需要有所变化:
182+
183+
```diff
184+
var ipcProvider = new IpcProvider("IpcRemotingObjectClientDemo");
185+
ipcProvider.StartServer();
186+
var peer = await ipcProvider.GetAndConnectToPeerAsync("IpcRemotingObjectServerDemo");
187+
188+
// 获取来自 B 进程的 IFoo 接口的「代理」(Proxy)
189+
-- var foo = ipcProvider.CreateIpcProxy<IFoo>(peer);
190+
++ // 不过这次,我们使用了 IpcFooShape 这个「形状」(Shape)作为「代理」(Proxy)
191+
++ var foo = ipcProvider.CreateIpcProxy<IpcFooShape>(peer);
192+
193+
Console.WriteLine(foo.Name);
194+
Console.WriteLine(foo.Add(1, 2));
195+
Console.WriteLine(await foo.AddAsync("a", 1));
196+
Console.Read();
197+
```
198+
199+
## 高级用法
200+
201+
DotNetCampus.Ipc「远程对象调用」支持你嵌套 IPC 对象,这意味着你可以实现更加复杂的 IPC 需求。
202+
203+
```csharp
204+
/// <summary>
205+
/// 嵌套 IPC 类型演示。
206+
/// </summary>
207+
[IpcPublic]
208+
public interface IBar
209+
{
210+
/// <summary>
211+
/// 这是一个超复杂的方法,参数和返回值都是 IPC 对象。
212+
/// </summary>
213+
Task<IBaz> AddAsync(IQux qux, IQuux quux);
214+
}
215+
```
216+
217+
想想看,这里的每个类型,谁是调用方,谁是被调用方(实现方)?
218+
219+
假设 A 进程试图调用 B 进程的 `IBar` 接口:
220+
221+
| 类型 | A 进程 | B 进程 | 描述 |
222+
| ------- | ---------------- | ---------------- | -------------------------------------------- |
223+
| `IBar` | 代理 | 实现(需要对接) | A 进程调用 `IBar` 的代理以访问 B 进程 |
224+
| `IBaz` | 代理 | 对接(无需对接) | A 进程拿到了 B 进程的 `IBaz` 的代理 |
225+
| `IQux` | 实现(无需对接) | 代理 | A 进程有一个 IQux 的实现,会传给 B 进程去用 |
226+
| `IQuux` | 实现(无需对接) | 代理 | A 进程有一个 IQuux 的实现,会传给 B 进程去用 |
227+
228+
这里的 `IBaz` `IQux` `IQuux` 都需要标记 `[IpcPublic]``[IpcShape]`(当然,我们的分析器也会提示你需要标的)。但好在你不需要额外编写对接代码;我的意思是 IPC 的初始化代码里,你只需要处理 `IBar` 这一个接口就够了,剩下的 DotNetCampus.Ipc 会帮你完成。
229+
230+
## 异常处理
231+
232+
本 IPC 库有两种类型的异常:
233+
234+
- `IpcLocalException`: 表示异常发生在本地进程中
235+
- `IpcRemoteException`: 表示异常发生在远端进程中,或者发生在 IPC 通信过程中
236+
237+
你可能在初始化等过程中收到各种异常,不过「远程对象调用」中只会收到以下这些:
238+
239+
- 本地异常 `IpcLocalException`
240+
- 「远程对象调用」几乎不会发生本地异常
241+
- 代理异常,如果 IPC 接口的实现方法内抛出了以下这几种异常,则会在调用方也代理出相同类型的异常(这个列表详见 [这里](../src/dotnetCampus.Ipc/CompilerServices/GeneratedProxies/Models/GeneratedProxyExceptionModel.cs),你也可以提 PR 修改这个列表)
242+
- `ArgumentException`
243+
- `ArgumentNullException`
244+
- `BadImageFormatException`
245+
- `InvalidCastException`
246+
- `InvalidOperationException`
247+
- `NotImplementedException`
248+
- `NotSupportedException`
249+
- `NullReferenceException`
250+
- 远端异常 `IpcRemoteException`
251+
- `IpcInvokingException`: 如果 IPC 接口的实现方法内抛出了上述异常之外的其他异常,则会包装成此异常
252+
- `IpcInvokingTimeoutException`: 远程对象调用超时(如前面所说,设置 `Timeout` 属性后可以支持超时)
253+
254+
所以,大多数情况下,你只需要像一个本地对象一样去处理异常即可。
255+
256+
不过,如果你想更加可靠一些处理异常,我们正计划做「自动代理」功能,以便能更好地用通用的方式来处理「远程对象调用」中发生的远端**非业务性**异常。功能计划中,文件夹已经建好了,请耐心等待。
257+
258+
## 性能和 AOT 兼容性
259+
260+
1. DotNetCampus.Ipc 库使用源生成器生成「代理」(Proxy)和「对接」(Joint)的代码,旨在提升性能和确保 AOT 兼容性。
261+
- 目前还仍有少量代码在使用反射,不过我们计划很快将其完全消除
262+
2. DotNetCampus.Ipc 已移除 Newtonsoft.Json 库,完全使用 System.Text.Json 并配合源生成器来做跨进程对象的传输,旨在大幅减少 AOT 之后的大小。
263+
- **想要享受到完全移除 Newtonsoft.Json 库的好处,你的项目框架至少要到 .NET 8**
264+
265+
## 最佳实践
266+
267+
为了更好地发挥 IPC「远程对象调用」的代码编写直观性优势,同时又避免不太喜欢的行为,库作者 @walterlv 推荐:
268+
269+
1. IPC 接口中尽量全部使用异步方法
270+
- 除非你希望这个对象用起来更加像一个本地对象一样,拥有属性、同步的方法
271+
2. 如果一定要用同步方法,也请避免使用 void 返回值
272+
- 如果真用了 void 返回值,请认真考虑一下要不要设置 `WaitsVoid` 属性
273+
274+
好了,就这些。其他你随便。
File renamed without changes.

src/dotnetCampus.Ipc/CompilerServices/GeneratedProxies/Models/GeneratedProxyExceptionModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public void Throw()
7878
}
7979
else
8080
{
81-
throw new IpcRemoteException(
81+
throw new IpcInvokingException(
8282
$"远端抛出了异常 {typeName}: {Message}\n如需抛出普通异常,请联系 IPC 库作者将异常添加到 ExceptionRebuilders 中。",
8383
StackTrace);
8484
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using dotnetCampus.Ipc.CompilerServices.GeneratedProxies.Models;
2+
3+
namespace dotnetCampus.Ipc.Exceptions;
4+
5+
/// <summary>
6+
/// 当获取设置属性值或调用方法时,如果实现方法内抛出了异常,且不是常见的异常类型(参见 <see cref="GeneratedProxyExceptionModel"/>),则会用此异常包装。
7+
/// </summary>
8+
internal class IpcInvokingException(string message, string? remoteStackTrace) : IpcRemoteException(message, remoteStackTrace);

src/dotnetCampus.Ipc/Exceptions/IpcInvokingTimeoutException.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
using System;
2-
3-
using dotnetCampus.Ipc.CompilerServices.Attributes;
1+
using dotnetCampus.Ipc.CompilerServices.Attributes;
42

53
namespace dotnetCampus.Ipc.Exceptions;
64

75
/// <summary>
86
/// 当获取设置属性值或调用方法时,如果指定了 <see cref="IpcMemberAttribute.Timeout"/> 但未在超时时间完成前完成调用,则会抛出此异常。
97
/// </summary>
10-
internal class IpcInvokingTimeoutException : IpcLocalException
8+
internal class IpcInvokingTimeoutException : IpcRemoteException
119
{
1210
public IpcInvokingTimeoutException(string memberName, TimeSpan timeout) : base()
1311
{

0 commit comments

Comments
 (0)