作者引言
很高興啊,我們來到了IceRPC之試試的新玩法"打洞",讓防火牆哭去吧
試試RPCs的新玩法"打洞"
比較典型的玩法:RPC數據流從客戶端流向服務端,現在來嘗試用
IceRPC
來玩一個新的花樣"打洞"。
概述
對於 IceRPC,客戶端是發起連接的實體, 而服務器是接受連接的實體。
建立連接後,通常會從客戶端到服務端生成RPCs通道:
- 客戶端創建請求,並將該請求發送到服務器
- 服務端接受此請求,並將此請求發送到已提供的"服務實現"
- 服務端返回響應,IceRPC 將此響應帶回客戶端
IceRPC 提供的幾乎所有示例,都是這個客戶端到服務器的模式。儘管如此,我們可以使用 IceRPC 試試另一種發送方式。
獲取調用器invoker
使用IceRPC,需要一個調用器
invoker
來發送請求,並接收相應的響應。 IceRPC(C#)提供了 ClientConnection 和 ConnectionCache 類, 用來建立網絡連接的兩個"終端"調用器invoker
。 當使用這些調用器之一發送請求時, 請求會從底層連接的客戶端,傳輸到服務器端。
"終端"調用器是實際發送請求,並接收響應的調用器invoker
。 相比之下,管道Pipeline
和攔截器interceptors
是非終端調用器:他們處理請求和響應,但需要實際的調用者,來完成這項工作。
服務端到客戶端調用所需的調用器invoker
是 IConnectionContext.Invoker
。 從傳入請求中檢索連接上下文
。 如下所示:
// In a dispatcher implementation
public ValueTask<OutgoingResponse> DispatchAsync(
IncomingRequest request,
CancellationToken cancellationToken)
{
// The invoker represents the connection over which we received this request.
IInvoker invoker = request.ConnectionContext.Invoker;
...
}
如果正在使用 Slice 實施 IceRPC 服務, 需要在調度管道中,安裝調度信息中間件UseDispatchInformation()
,以將此連接上下文
公開爲 IDispatchInformationFeature
的一部分。 如下所示:
// Router setup in composition root / main Program
Router router = new Router()
.UseDispatchInformation()
.Map<IGreeterService>(new Chatbot());
// Slice Service implementation
public ValueTask<string> GreetAsync(
string name,
IFeatureCollection features,
CancellationToken cancellationToken)
{
IDispatchInformationFeature? dispatchInfo = features.Get<IDispatchInformationFeature>();
Debug.Assert(dispatchInfo is not null); // installed by the DispatchInformation middleware
// The invoker represents the connection over which we received this Greet request.
IInvoker invoker = dispatchInfo.ConnectionContext.Invoker;
...
}
然後,一旦有了終端調用器,就可以使用該調用器發送請求。 如果使用 Slice,將使用此調用器構建 Slice 代理,然後使用此代理調用操作。 如下所示:
IInvoker invoker = ...; // some invoker
var alarm = new AlarmProxy(invoker); // use Alarm's default path.
await alarm.SomeOpAsync();
推送通知用例
使用IceRPC開發推送通知功能: 當服務端中發生某些事件時(從服務端"推送
push
"),客戶端希望收到來自服務端的通知。 它不想定期發送請求,來檢查是否發生此事件("拉取pull
")。
服務器無法打開與客戶端的連接(由於防火牆或其他網絡限制), 因此,我們希望使用現有的客戶端到服務器連接來執行這些通知。
我們可以使用以下 Slice 接口對這種交互進行模擬:
// Implemented by a service in the client
interface Alarm {
abnormalTemperature(value: float32) -> string
}
// Implemented by a service in the server
interface Sensor {
getCurrentTemperature() -> float32
// Monitors the current temperature and calls `abnormalTemperature` on Alarm when the
// current temperature moves outside [low..high].
monitorTemperature(low: float32, high: float32)
}
並實現傳感器Sensor
服務如下:
// Implementation of Sensor service
public ValueTask MonitorTemperatureAsync(
float low,
float high,
IFeatureCollection features,
CancellationToken cancellationToken)
{
IDispatchInformationFeature? dispatchInfo = features.Get<IDispatchInformationFeature>();
Debug.Assert(dispatchInfo is not null); // installed by DispatchInformation middleware
// We use Alarm's default path for this proxy.
var alarm = new AlarmProxy(dispatchInfo.ConnectionContext.Invoker);
// We enqueue the information and monitor the temperature in a separate task.
_monitor.Add(low, high, alarm);
}
在客戶端中,我們實現警報Alarm
服務,將其映射到路由器Router
內,然後在客戶端連接的選項中設置調度程序ClientConnection
:
// Client side
Router router = new Router.Map<IAlarmService>(new PopupAlarm()); // use Alarm's default path.
await using var connection = new ClientConnection(
new ClientConnectionOptions
{
Dispatcher = router, // client-side dispatcher
ServerAddress = new ServerAddress(new Uri("icerpc://..."))
});
// Use connection as usual to create a SensorProxy and call MonitorTemperatureAsync.
[SliceService]
internal partial class PopupAlarm : IAlarmService
{
public ValueTask<string> AbnormalTemperatureAsync(
float value,
IFeatureCollection features,
CancellationToken cancellationToken)
{
// Show a popup with the abnormal temperature.
...
return new("Roger"); // acknowledge alarm
}
}
底層調用器invoker
連接上下文
提供的調用器是綁定到特定網絡連接的"原始"調用器。 如果網絡連接因任何原因關閉,則該調用器將不再可用。 當使用此類調用器時,需要自己處理此類連接故障。ClientConnection
和ConnectionCache
更易於使用,因爲它們可以根據需要,重建底層連接。
替代方案:字節流拉動Stream pulls
基於 HTTP 的 RPC 框架,比如 gRPC,不能讓 RPC 反過來推送,這裏還有一個替代方案!
如果無法將通知推送到客戶端,可以從客戶端拉取這些通知。 它在網絡上使用大約相同數量的字節,但要不優雅。 如下所示:
// Implemented by a service in the server
interface Sensor {
getCurrentTemperature() -> float32
// Monitors the current temperature and streams back any value outside
// the acceptable range.
monitorTemperature(low: float32, high: float32) -> stream float32
}
通過這種方法,客戶端會迭代 monitorTemperature 返回的流:每個新值都是一個新通知。
獨特優勢
RPC可以得到另一方式的迴應。 該回應告訴調用者
caller
,請求已由客戶端中的服務,成功發送和處理。
如果只是將回應流stream responses
回您的客戶端, 服務器不用任何確認: 它就知道產生了迴應流,也可能知道該流已成功通過了網絡,但它不知道客戶端是否成功接收並處理了該流。
本質上,流響應類似於從服務器到客戶端的單向請: 語法不同,但沒有功能差異。另一方面,流響應無法模擬從服務器到客戶端的雙向 RPC。
雲路由用例(分佈式)
從服務器到客戶端製作 RPC 的另一個用例是通過雲服務路由, 如圖所示 Thermostat【git源碼】例子。
這個例子是現實世界中,常見難題的一個簡化版本: 比如你有一個客戶端應用 (像手機應用app) 需要連接一個設備 (比如恆溫器thermostat
)。 該設備位於防火牆後面,不接受傳入連接。 如何建立彼此通信呢?
解決方案:引入客戶端和設備都連接的中介服務器。 該服務器通常部署在"雲中",並將請求從客戶端路由到設備(反之亦然,如果需要)。 從客戶端到設備的請求,通過客戶端到服務器的連接,然後通過服務器到設備的連接。 這種方式對 IceRPC 來說,非常好處理:
通過此示例,客戶端可以更改恆溫器thermostat
上的設定點,並等待恆溫器的確認:例如"確定"或故障—,因爲指定的設定點太低:
恆溫器Thermostat
示例在服務器 DeviceConnection
類中實現自己的終端調用器
/// <summary>Represents the server-side of the connection from the device to this server. This
/// connection remains valid across re-connections from the device.</summary>
internal class DeviceConnection : IInvoker
{
private volatile IInvoker? _invoker;
public async Task<IncomingResponse> InvokeAsync(
OutgoingRequest request,
CancellationToken cancellationToken = default)
{
if (_invoker is IInvoker invoker)
{
try
{
return await invoker.InvokeAsync(request, cancellationToken);
}
catch (ObjectDisposedException)
{
// throw NotFound below
}
}
throw new DispatchException(StatusCode.NotFound, "The device is not connected.");
}
/// <summary>Sets the invoker that represents the latest connection from the device.</summary>
internal void SetInvoker(IInvoker invoker) => _invoker = invoker;
}
設備連接:從設備到服務器的最新連接。 擁有一個,在連接中留下來的終端調用器非常有用: 它允許恆溫器服務器創建管道和代理,無需每次設備重新連接時,重新創建。
結論
反向調用生成 RPC 是一個強大的功能,是 IceRPC 與其他 RPC 框架區分的主要功能。 可以利用此功能構建,具有有意義的語義的網絡應用程序,而且這些應用程序可以在防火牆上開心地正常工作"打洞"。
作者結語
- 一直做,不停做,才能提升速度
- 翻譯的不好,請手下留情,謝謝
- 如果對我有點小興趣,如可加我哦,一起探討人生,探討道的世界。
- 覺得還不錯的話,點個贊哦