.NET 開源項目 StreamJsonRpc 介紹[下篇]

閱讀本文大概須要 9 分鐘。git

你們好,這是 .NET 開源項目 StreamJsonRpc 介紹的最後一篇。上篇介紹了一些預備知識,包括 JSON-RPC 協議介紹,StreamJsonRpc 是一個實現了 JSON-RPC 協議的庫,它基於 Stream、WebSocket 和自定義的全雙工管道傳輸。中篇經過示例講解了 StreamJsonRpc 如何使用全雙工的 Stream 做爲傳輸管道實現 RPC 通信。本篇(下篇)將繼續經過示例講解如何基於 WebSocket 傳輸管道實現 RPC 通信。github

準備工做

爲了示例的完整性,本文示例繼續在中篇建立的示例基礎上進行。該示例的 GitHub 地址爲:web

github.com/liamwang/StreamJsonRpcSamples編程

咱們繼續添加三個項目,一個是名爲 WebSocketSample.Client 的 Console 應用,一個是名爲 WebSocketSample.Server 的 ASP.NET Core 應用,還有一個名爲 Contract 的契約類庫(和 gRPC 相似)。json

你能夠直接複製並執行下面的命令一鍵完成大部分準備工做:api

dotnet new console -n WebSocketSample.Client # 建新客戶端應用
dotnet new webapi -n WebSocketSample.Server # 新建服務端應用
dotnet new classlib -n Contract # 新建契約類庫
dotnet sln add WebSocketSample.Client WebSocketSample.Server Contract # 將項目添加到解決方案
dotnet add WebSocketSample.Client package StreamJsonRpc # 爲客戶端安裝 StreamJsonRpc 包
dotnet add WebSocketSample.Server package StreamJsonRpc # 爲服務端安裝 StreamJsonRpc 包
dotnet add WebSocketSample.Client reference Contract # 添加客戶端引用 Common 引用
dotnet add WebSocketSample.Server reference Contract # 添加服務端引用 Common 引用

爲了把重點放在實現上,此次咱們依然以一個簡單的功能做爲示例。該示例實現客戶端向服務端發送一個問候數據,而後服務端響應一個消息。爲了更貼合實際的場景,此次使用強類型進行操做。爲此,咱們在 Contract 項目中添加三個類用來約定客戶端和服務端通信的數據結構和接口。bash

用於客戶端發送的數據的 HelloRequest 類:網絡

public class HelloRequest
{
    public string Name { get; set; }
}

用於服務端響應的數據的 HelloResponse 類:數據結構

public class HelloResponse
{
    public string Message { get; set; }
}

用於約定服務端和客戶端行爲的 IGreeter 接口:app

public interface IGreeter
{
    Task<HelloResponse> SayHelloAsync(HelloRequest request);
}

接下來和中篇同樣,經過創建鏈接、發送請求、接收請求、斷開鏈接這四個步驟演示和講解一個完整的基於 WebSocket 的 RPC 通信示例。

創建鏈接

上一篇講到要實現 JSON-RPC 協議的通信,要求傳輸管道必須是全雙工的。而 WebSocket 就是標準的全雙工通信,因此天然能夠用來實現 JSON-RPC 協議的通信。.NET 自己就有現成的 WebSocket 實現,因此在創建鏈接階段和 StreamJsonRpc 沒有關係。咱們只須要把 WebSocket 通信管道架設好,而後再使用 StreamJsonRpc 來發送和接收請求便可。

客戶端使用 WebSocket 創建鏈接比較簡單,使用 ClientWebSocket 來實現,代碼以下:

using (var webSocket = new ClientWebSocket())
{
    Console.WriteLine("正在與服務端創建鏈接...");
    var uri = new Uri("ws://localhost:5000/rpc/greeter");
    await webSocket.ConnectAsync(uri, CancellationToken.None);
    Console.WriteLine("已創建鏈接");
}

服務端創建 WebSocket 鏈接最簡單的方法就是使用 ASP.NET Core,藉助 Kestrel 和 ASP.NET Core 的中間件機制能夠輕鬆搭建基於 WebSocket 的 RPC 服務。只要簡單的封裝還能夠實現同一套代碼同時提供 RPC 服務和 Web API 服務。

首先在服務端項目的 Startup.cs 類的 Configure 方法中引入 WebSocket 中間件:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseRouting();

    app.UseWebSockets(); // 增長此行,引入 WebSocket 中間件

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

再新建一個 Controller 並定義一個 Action 用來路由映射 WebSocket 請求:

public class RpcController : ControllerBase
{
    ...
    [Route("/rpc/greeter")]
    public async Task<IActionResult> Greeter()
    {
        if (!HttpContext.WebSockets.IsWebSocketRequest)
        {
            return new BadRequestResult();
        }

        var socket = await HttpContext.WebSockets.AcceptWebSocketAsync();

        ...
    }
}

這裏的 Greeter 提供的服務既能接收 HTTP 請求也能接收 WebSocket 請求。HttpContext 中的 WebSockets 屬性是一個 WebSocketManager 對象,它能夠用來判斷當前請求是否爲一個 WebSocket 請求,也能夠用來等待和接收 WebSocket 鏈接,即上面代碼中的 AcceptWebSocketAsync 方法。另外客戶端的 WebSocket 的 Uri 路徑須要與 Router 指定的路徑對應。

鏈接已經創建,如今到了 StreamJsonRpc 發揮做用的時候了。

發送請求

客戶端經過 WebSocket 發送請求的方式和前一篇講的 Stream 方式是同樣的。還記得前一篇講到的 JsonRpc 類的 Attach 靜態方法嗎?它告訴 StreamJsonRpc 如何傳輸數據,並返回一個用於調用 RPC 的客戶端,它除了能夠接收 Stream 參數外還有多個重載方法。好比:

public static T Attach<T>(Stream stream);
public static T Attach<T>(IJsonRpcMessageHandler handler);

第二個重載方法能夠實現更靈活的 Attach 方式,你能夠 Attach 一個交由 WebSocket 傳輸數據的管道,也能夠 Attach 給一個自定義實現的 TCP 全雙工傳輸管道(此方式本文不講,但文末會直接給出示例)。如今咱們須要一個實現了 IJsonRpcMessageHandler 接口的處理程序,StreamJsonRpc 已經實現好了,它是 WebSocketMessageHandler 類。經過 Attach 該實例,能夠拿到一個用於調用 RPC 服務的對象。代碼示例以下:

Console.WriteLine("開始向服務端發送消息...");
var messageHandler = new WebSocketMessageHandler(webSocket);
var greeterClient = JsonRpc.Attach<IGreeter>(messageHandler);
var request = new HelloRequest { Name = "精緻碼農" };
var response = await greeterClient.SayHelloAsync(request);
Console.WriteLine($"收到來自服務端的響應:{response.Message}");

你會發現,定義客戶端和服務端契約的好處是能夠實現強類型編程。接下來看服務端如何接收並處理客戶端發送的消息。

接收請求

和前一篇同樣,咱們先定義一個 GreeterServer 類用來處理接收到的客戶端消息。

public class GreeterServer : IGreeter
{
    private readonly ILogger<GreeterServer> _logger;
    public GreeterServer(ILogger<GreeterServer> logger)
    {
        _logger = logger;
    }

    public Task<HelloResponse> SayHelloAsync(HelloRequest request)
    {
        _logger.LogInformation("收到並回復了客戶端消息");
        return Task.FromResult(new HelloResponse
        {
            Message = $"您好, {request.Name}!"
        });
    }
}

一樣,WebSocket 服務端也須要使用 Attach 來告訴 StreamJsonRpc 數據如何通信,並且使用的也是 WebSocketMessageHandler 類,方法與客戶端相似。在前一篇中,咱們 Attach 一個 Stream 調用的方法是:

public static JsonRpc Attach(Stream stream, object? target = null);

同理,咱們推測應該也有一個這樣的靜態重載方法:

public static JsonRpc Attach(IJsonRpcMessageHandler handler, object? target = null);

惋惜,StreamJsonRpc 並無提供這個靜態方法。既然 Attach 方法返回的是一個 JsonRpc 對象,那咱們是否能夠直接實例化該對象呢?查看該類的定義,咱們發現是能夠的,並且有咱們須要的構造函數:

public JsonRpc(IJsonRpcMessageHandler messageHandler, object? target);

接下來就簡單了,一切和前一篇的 Stream 示例都差很少。在 RpcController 的 Greeter Action 中實例化一個 JsonRpc,而後開啓消息監聽。

public class RpcController : ControllerBase
{
    private readonly ILogger<RpcController> _logger;
    private readonly GreeterServer _greeterServer;

    public RpcController(ILogger<RpcController> logger, GreeterServer greeterServer)
    {
        _logger = logger;
        _greeterServer = greeterServer;
    }

    [Route("/rpc/greeter")]
    public async Task<IActionResult> Greeter()
    {
        if (!HttpContext.WebSockets.IsWebSocketRequest)
        {
            return new BadRequestResult();
        }

        _logger.LogInformation("等待客戶端鏈接...");
        var socket = await HttpContext.WebSockets.AcceptWebSocketAsync();
        _logger.LogInformation("已與客戶端創建鏈接");

        var handler = new WebSocketMessageHandler(socket);

        using (var jsonRpc = new JsonRpc(handler, _greeterServer))
        {
            _logger.LogInformation("開始監聽客戶端消息...");
            jsonRpc.StartListening();
            await jsonRpc.Completion;
            _logger.LogInformation("客戶端斷開了鏈接");
        }

        return new EmptyResult();
    }
}

看起來和咱們平時寫 Web API 差很少,區別僅僅是對請求的處理方式。但須要注意的是,WebSocket 是長鏈接,若是客戶端沒有事情能夠處理了,最好主動斷開與服務端的鏈接。若是客戶客戶沒有斷開鏈接,執行的上下文就會停在 await jsonRpc.Completion 處。

斷開鏈接

一般斷開鏈接是由客戶端主動發起的,因此服務端不須要作什麼處理。服務端響應完消息後,只需使用 jsonRpc.Completion 等待客戶端斷開鏈接便可,上一節的代碼示例中已經包含了這部分代碼,就再也不累述了。若是特殊狀況下服務端須要斷開鏈接,調用 JsonRpc 對象的 Dispose 方法便可。

不論是 Stream 仍是 WebSocket,其客戶端對象都提供了 Close 或 Dispose 方法,鏈接會隨着對象的釋放自動斷開。但最好仍是主動調用 Close 方法斷開鏈接,以確保服務端收到斷開的請求。對於 ClientWebSocket,須要調用 CloseAsync 方法。客戶端完整示例代碼以下:

static async Task Main(string[] args)
{
    using (var webSocket = new ClientWebSocket())
    {
        Console.WriteLine("正在與服務端創建鏈接...");
        var uri = new Uri("ws://localhost:5000/rpc/greeter");
        await webSocket.ConnectAsync(uri, CancellationToken.None);
        Console.WriteLine("已創建鏈接");

        Console.WriteLine("開始向服務端發送消息...");
        var messageHandler = new WebSocketMessageHandler(webSocket);
        var greeterClient = JsonRpc.Attach<IGreeter>(messageHandler);
        var request = new HelloRequest { Name = "精緻碼農" };
        var response = await greeterClient.SayHelloAsync(request);
        Console.WriteLine($"收到來自服務端的響應:{response.Message}");

        Console.WriteLine("正在斷開鏈接...");
        await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "斷開鏈接", CancellationToken.None);
        Console.WriteLine("已斷開鏈接");
    }

    Console.ReadKey();
}

在實際項目中可能還須要因異常而斷開鏈接的狀況作處理,好比網絡不穩定可能致使鏈接中斷,這種狀況可能須要加入重試機制。

運行示例

因爲服務端使用的是 ASP.NET Core 模板,VS 默認使用 IIS Express 啓動,啓動後會自動打開網頁,這樣看不到 Console 的日誌信息。因此須要把服務端項目 WebSocketSample.Server 的啓動方式改爲自啓動。

另外,爲了更方便地同時運行客戶端和服務端應用,能夠把解決方案設置成多啓動。右鍵解決方案,選擇「Properties」,把對應的項目設置「Start」便可。

若是你用的是 VS Code,也是支持多啓動調試的,具體方法你自行 Google。若是你用的是 dotnet run 命令運行項目可忽略以上設置。

項目運行後的截圖以下:

你也能夠自定義實現 TCP 全雙工通信管道,但比較複雜並且也不多這麼作,因此就略過不講了。但我在 GitHub 的示例代碼也放了一個自定義全雙工管道實現的示例,感興趣的話你能夠克隆下來研究一下。

該示例運行截圖:

本篇總結

本文經過示例演示瞭如何使用 StreamJsonRpc 基於 WebSocket 數據傳輸實現 JSON-RPC 協議的 RPC 通信。其中客戶端和服務端有共同的契約部分,實現了強類型編程。經過示例咱們也清楚了 StreamJsonRpc 這個庫爲了實現 RPC 通信作了哪些工做,其實它就是在現有傳輸管道(Stream、WebSocket 和 自定義 TCP 鏈接)上進行數據通信。正如前一篇所說,因爲 StreamJsonRpc 把大部分咱們沒必要要知道的細節作了封裝,因此在示例中感受不到 JSON-RPC 協議帶來的統一規範,也沒看到具體的 JSON 格式的數據。其實只要遵循了 JSON-RPC 協議實現的客戶端或服務端,不論是用什麼語言實現,都是能夠互相通信的。

但願這三篇關於 StreamJsonRpc 的介紹能讓你有所收穫,若是你在工做中計劃使用 StreamJsonRpc,這幾篇文章包括示例代碼應該有值得參考的地方。

相關文章
相關標籤/搜索