.Net 8.0 下的新RPC,IceRPC之試試的新玩法"打洞"

作者引言

很高興啊,我們來到了IceRPC之試試的新玩法"打洞",讓防火牆哭去吧

試試RPCs的新玩法"打洞"

比較典型的玩法:RPC數據流從客戶端流向服務端,現在來嘗試用IceRPC來玩一個新的花樣"打洞"。

概述

對於 IceRPC,客戶端是發起連接的實體, 而服務器是接受連接的實體。

建立連接後,通常會從客戶端到服務端生成RPCs通道:

  1. 客戶端創建請求,並將該請求發送到服務器
  2. 服務端接受此請求,並將此請求發送到已提供的"服務實現"
  3. 服務端返回響應,IceRPC 將此響應帶回客戶端

A RPC from client to server

IceRPC 提供的幾乎所有示例,都是這個客戶端到服務器的模式。儘管如此,我們可以使用 IceRPC 試試另一種發送方式。

獲取調用器invoker

使用IceRPC,需要一個調用器invoker來發送請求,並接收相應的響應。 IceRPC(C#)提供了 ClientConnection 和 ConnectionCache 類, 用來建立網絡連接的兩個"終端"調用器invoker。 當使用這些調用器之一發送請求時, 請求會從底層連接的客戶端,傳輸到服務器端。

"終端"調用器是實際發送請求,並接收響應的調用器invoker。 相比之下,管道Pipeline和攔截器interceptors是非終端調用器:他們處理請求和響應,但需要實際的調用者,來完成這項工作。

服務端到客戶端調用所需的調用器invokerIConnectionContext.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")。

服務器無法打開與客戶端的連接(由於防火牆或其他網絡限制), 因此,我們希望使用現有的客戶端到服務器連接來執行這些通知。

Push notification example

我們可以使用以下 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

連接上下文提供的調用器是綁定到特定網絡連接的"原始"調用器。 如果網絡連接因任何原因關閉,則該調用器將不再可用。 當使用此類調用器時,需要自己處理此類連接故障。 ClientConnectionConnectionCache更易於使用,因爲它們可以根據需要,重建底層連接。

替代方案:字節流拉動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 example

通過此示例,客戶端可以更改恆溫器thermostat上的設定點,並等待恆溫器的確認:例如"確定"或故障—,因爲指定的設定點太低:

Thermostat client

恆溫器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 框架區分的主要功能。 可以利用此功能構建,具有有意義的語義的網絡應用程序,而且這些應用程序可以在防火牆上開心地正常工作"打洞"。

作者結語

  • 一直做,不停做,才能提升速度
  • 翻譯的不好,請手下留情,謝謝
  • 如果對我有點小興趣,如可加我哦,一起探討人生,探討道的世界
  • 覺得還不錯的話,點個
    image
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章