.NET 開源專案 StreamJsonRpc 介紹[中篇]

精緻碼農發表於2020-07-06

閱讀本文大概需要 11 分鐘。

.NET 開源專案 StreamJsonRpc 介紹[中篇]

上一篇介紹了一些預備知識,包括 JSON-RPC 介紹和實現了 JSON-RPC 的 StreamJsonRpc 介紹,講到了 StreamJsonRpc 可以通過 .NET 的 Stream 類和 WebSocket 類實現 JSON-RPC 協議的通訊。本篇就先選擇其中的 Stream 類來講解,通過具體的示例講解如何使用 StreamJsonRpc 實現 RPC 呼叫。

準備工作

先新建兩個 Console 應用,分別命名為 StreamSample.Client 和 StreamSample.Server,並均新增 StreamJsonRpc 包引用。

mkdir StreamJsonRpcSamples                              # 建立目錄
cd StreamJsonRpcSamples                                 # 進入目錄
dotnet new sln -n StreamJsonRpcSamples                  # 新建解決方案
dotnet new console -n StreamSample.Client               # 建新客戶端應用
dotnet new console -n StreamSample.Server               # 新建服務端應用
dotnet sln add StreamSample.Client StreamSample.Server  # 將應用新增到解決方案
dotnet add StreamSample.Client package StreamJsonRpc    # 為客戶端安裝 StreamJsonRpc 包
dotnet add StreamSample.Server package StreamJsonRpc    # 為服務端安裝 StreamJsonRpc 包

上篇 提到了實現 JSON-RPC 通訊要經歷四個步驟:建立連線、傳送請求、接收請求、斷開連線,其中傳送請求和接收請求可以歸為資料通訊,下面按照這幾個步驟順序來逐步講解。

建立連線

使用 Stream 實現 JSON-RPC 協議的通訊,要求該 Stream 必須是一個全雙工 Stream(可同時接收資料和傳送資料)或才是一對半雙工 Stream(本文不作討論)。實現了全雙工的 Stream 類在 .NET 中有 PipeStreamNetworkStream 等,本示例用的是 NamedPipeClientStream 類和 NamedPipeServerStream,前者用於客戶端,後者用於服務端。

先看服務端程式碼示例:

int clientId = 1;

var stream = new NamedPipeServerStream("StringJsonRpc",
    PipeDirection.InOut,
    NamedPipeServerStream.MaxAllowedServerInstances,
    PipeTransmissionMode.Byte,
    PipeOptions.Asynchronous);

Console.WriteLine("等待客戶端連線...");
await stream.WaitForConnectionAsync();
Console.WriteLine($"已與客戶端 #{clientId} 建立連線");

這裡使用了 NamedPipeServerStream 類,其第一個構造引數指定了該 Stream 管道的名稱,方便客戶端使用該名稱查詢。其它引數就不解釋了,其各自的含義可以在你編寫程式碼時通過智慧提示瞭解。

Stream 例項通過 WaitForConnectionAsync 來等待一個客戶端連線。由於該服務端可以連線多個客戶端,這裡使用自增長的 clientId 來標識區分它們。

再來看客戶端程式碼示例:

var stream = new NamedPipeClientStream(".",
    "StringJsonRpc",
    PipeDirection.InOut,
    PipeOptions.Asynchronous);

Console.WriteLine("正在連線伺服器...");
await stream.ConnectAsync();
Console.WriteLine("已建立連線!");

和伺服器類似,客戶端使用的是 NamedPipeClientStream 類來建立連線,在其構造引數中需要指定服務端的地址(這裡用了.代表本機)和通訊管道的名稱。Stream 例項通過 ConnectAsync 方法主動向伺服器請求連線。

如果網路是通的,客戶端和服務端就能成功建立連線。下面就要實現客戶端和服務端之間的資料通訊了,即客戶端傳送請求和服務端接收並處理請求。

資料通訊

客戶端與服務端建立連線後,資料不會無緣無故從一端流到另一端,要實現兩端的資料通訊還需要先把通訊管道架設起來,在其兩端設定對應的控制和處理程式。工程上這個聽起來好像不簡單,但對於 StreamJsonRpc 來說是件非常簡單的事。最簡單的方法是使用 JsonRpc 類的 Attach 靜態方法來架設兩端的 Stream 管道,該方法返回一個 JsonRpc 例項可以用來控制資料的通訊。

對於服務端,架設管道的同時還要為管道上的請求新增監聽和對應的處理程式,比如定義一個名為 GreeterServer 的類來處理“打招呼”的請求:

public class GreeterServer
{
    public string SayHello(string name)
    {
        Console.WriteLine($"收到【{name}】的問好,並回復了他");
        return $"您好,{name}!";
    }
}

然後例項化該類,把它傳給 JsonRpc 類的 Attach 靜態方法:

static async Task Main(string[] args)
{
    ...
    _ = ResponseAsync(stream, clientId);
    clientId++;
}

static Task ResponseAsync(NamedPipeServerStream stream, int clientId)
{
    var jsonRpc = JsonRpc.Attach(stream, new GreeterServer());
    return jsonRpc.Completion;
}

這裡我們單獨定義了一個 ResponseAsync 方法用來處理客戶端請求,在 Main 函式中我們不用關心該方法返回的 Task 任務,所以使用了棄元

對於客戶端也是類似的,使用 JsonRpc 類的 Attach 靜態方法來完成管道架設,並呼叫 JsonRpc 例項的 InvokeAsync 方法向服務端傳送指定請求。程式碼示例如下:

...
Console.WriteLine("我是精緻碼農,開始向服務端問好...");
var jsonRpc = JsonRpc.Attach(stream);
var message = await jsonRpc.InvokeAsync<string>("SayHello", "精緻碼農");
Console.WriteLine($"來自服務端的響應:{message}");

這樣就實現了客戶端呼叫服務端的方法,但客戶端需要知道服務端的方法簽名。這裡只是為示例演示,在實際情況中,客戶端和服務端需要先約定好介面,這樣客戶端就可以面向介面實現強型別程式設計,不必關心服務端處理程式的具體資訊。

注意到沒,從建立連線到實現資料通訊,客戶端和服務端都是對應的,而且使用的類和方法都是相似的。

斷開連線

當客戶端或伺服器端在不需要傳送請求或響應請求時,則可以呼叫 JsonRpc 例項的 Dispose 方法斷開並釋放連線。

jsonRpc.Dispose();

如果需要斷開連線,一般是由客戶端這邊發起,比如對於控制檯應用按 Ctrl + C 結束任務便會斷開與服務端的連線。那服務端如何知道某個客戶端斷開了連線呢?可以手動等待 JsonRpc 例項的 Completion 任務完成,比如:

static async Task ResponseAsync(NamedPipeServerStream stream, int clientId)
{
    var jsonRpc = JsonRpc.Attach(stream, new GreeterServer());
    await jsonRpc.Completion;
    Console.WriteLine($"客戶端 #{clientId} 的已斷開連線");
    jsonRpc.Dispose();
    await stream.DisposeAsync();
}

這裡為了保險起見,我還手動把 stream 也釋放掉了。

除了主動斷開連線,客戶端或伺服器丟擲未 catch 的異常也會致使連線中斷,在實際情況中針對這種異常的連線中斷可能需要編寫重試機制,這裡就不展開討論了。

完整程式碼

以上為了講解方便,程式碼只貼了與上下文相關的部分,最後我再把完整程式碼貼一下吧。

服務端 StreamSample.Server 下的 Program.cs:

class Program
{
    static async Task Main(string[] args)
    {
        int clientId = 1;

        while (true)
        {
            var stream = new NamedPipeServerStream("StringJsonRpc",
                PipeDirection.InOut,
                NamedPipeServerStream.MaxAllowedServerInstances,
                PipeTransmissionMode.Byte,
                PipeOptions.Asynchronous);

            Console.WriteLine("等待客戶端連線...");
            await stream.WaitForConnectionAsync();
            Console.WriteLine($"已與客戶端 #{clientId} 建立連線");

            _ = ResponseAsync(stream, clientId);

            clientId++;
        }
    }

    static async Task ResponseAsync(NamedPipeServerStream stream, int clientId)
    {
        var jsonRpc = JsonRpc.Attach(stream, new GreeterServer());
        await jsonRpc.Completion;
        Console.WriteLine($"客戶端 #{clientId} 的已斷開連線");
        jsonRpc.Dispose();
        await stream.DisposeAsync();
    }
}

public class GreeterServer
{
    public string SayHello(string name)
    {
        Console.WriteLine($"收到【{name}】的問好,並回復了他");
        return $"您好,{name}!";
    }
}

客戶端 StreamSample.Client 下的 Program.cs:

class Program
{
    static async Task Main(string[] args)
    {
        var stream = new NamedPipeClientStream(".",
            "StringJsonRpc",
            PipeDirection.InOut,
            PipeOptions.Asynchronous);

        Console.WriteLine("正在連線伺服器...");
        await stream.ConnectAsync();
        Console.WriteLine("已建立連線!");

        Console.WriteLine("我是精緻碼農,開始向服務端問好...");
        var jsonRpc = JsonRpc.Attach(stream);
        var message = await jsonRpc.InvokeAsync<string>("SayHello", "精緻碼農");
        Console.WriteLine($"來自服務端的響應:{message}");

        Console.ReadKey();
    }
}

完整程式碼已放到 GitHub,地址為:

github.com/liamwang/StreamJsonRpcSamples

兩個客戶端和服務端一起執行的截圖:

本篇總結

本文通過一個簡單但完整的示例講解了如何使用 StreamJsonRpc 來實現基於 JSON-RPC 協議的 RPC 呼叫。由於服務端和客戶端都使用的是 StreamJsonRpc 庫來實現的,所以在示例中感覺不到 JSON-RPC 協議帶來的統一規範,也沒看到具體的 JSON 格式的資料。這是因為 StreamJsonRpc 庫都已經幫我們封裝好了,兩端都基於 C#,示例使用的也是簡單的 Stream 方式,隱藏了我們不必關心的細節。其實只要符合 JSON-RPC 協議標準,C# 寫的服務端也可以由其它語言實現的客戶端來呼叫,反之亦然。

關注我一段時間的朋友都知道,我的文章篇幅一般不會太長,主要是方便大家利用零碎時間把它一次性看完。StreamJsonRpc 的使用遠不止本文講的這些,比如還有基於 WebSocket 進行資料傳輸的方式。來想通過兩篇講完,但講了一半就已經超出了預期的篇幅長度。所以我把本文定為[中篇],如果有時間我會繼續寫[下篇],下篇主要會講 StreamJsonRpc + WebSocket 的使用,並會盡量以更貼合實際應用場景的示例來講解。

相關文章