1. 程式人生 > >.NET 開源專案 StreamJsonRpc 介紹[下篇]

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

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

大家好,這是 .NET 開源專案 StreamJsonRpc 介紹的最後一篇。[上篇](https://mp.weixin.qq.com/s/3V4mCdFvNzUzqkqKm-rzpw)介紹了一些預備知識,包括 JSON-RPC 協議介紹,StreamJsonRpc 是一個實現了 JSON-RPC 協議的庫,它基於 Stream、WebSocket 和自定義的全雙工管道傳輸。[中篇](https://mp.weixin.qq.com/s/NgifmBGV7ipxNMCyDmG0FA)通過示例講解了 StreamJsonRpc 如何使用全雙工的 Stream 作為傳輸管道實現 RPC 通訊。本篇(下篇)將繼續通過示例講解如何基於 WebSocket 傳輸管道實現 RPC 通訊。 ## 準備工作 為了示例的完整性,本文示例繼續在[中篇](https://mp.weixin.qq.com/s/NgifmBGV7ipxNMCyDmG0FA)建立的示例基礎上進行。該示例的 GitHub 地址為: [github.com/liamwang/StreamJsonRpcSamples](https://github.com/liamwang/StreamJsonRpcSamples) 我們繼續新增三個專案,一個是名為 WebSocketSample.Client 的 Console 應用,一個是名為 WebSocketSample.Server 的 ASP.NET Core 應用,還有一個名為 Contract 的契約類庫(和 gRPC 類似)。 ![ ](https://img2020.cnblogs.com/blog/191097/202007/191097-20200715201442433-108032433.png) 你可以直接複製並執行下面的命令一鍵完成大部分準備工作: ```bash 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 專案中新增三個類用來約定客戶端和服務端通訊的資料結構和介面。 用於客戶端傳送的資料的 HelloRequest 類: ```cs public class HelloRequest { public string Name { get; set; } } ``` 用於服務端響應的資料的 HelloResponse 類: ```cs public class HelloResponse { public string Message { get; set; } } ``` 用於約定服務端和客戶端行為的 IGreeter 介面: ```cs public interface IGreeter { Task SayHelloAsync(HelloRequest request); } ``` 接下來和中篇一樣,通過建立連線、傳送請求、接收請求、斷開連線這四個步驟演示和講解一個完整的基於 WebSocket 的 RPC 通訊示例。 ## 建立連線 上一篇講到要實現 JSON-RPC 協議的通訊,要求傳輸管道必須是全雙工的。而 WebSocket 就是標準的全雙工通訊,所以自然可以用來實現 JSON-RPC 協議的通訊。.NET 本身就有現成的 WebSocket 實現,所以在建立連線階段和 StreamJsonRpc 沒有關係。我們只需要把 WebSocket 通訊管道架設好,然後再使用 StreamJsonRpc 來發送和接收請求即可。 客戶端使用 WebSocket 建立連線比較簡單,使用 `ClientWebSocket` 來實現,程式碼如下: ```cs 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 中介軟體: ```cs public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseRouting(); app.UseWebSockets(); // 增加此行,引入 WebSocket 中介軟體 app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } ``` 再新建一個 Controller 並定義一個 Action 用來路由對映 WebSocket 請求: ```cs public class RpcController : ControllerBase { ... [Route("/rpc/greeter")] public async Task 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 引數外還有多個過載方法。比如: ```cs public static T Attach(Stream stream); public static T Attach(IJsonRpcMessageHandler handler); ``` 第二個過載方法可以實現更靈活的 Attach 方式,你可以 Attach 一個交由 WebSocket 傳輸資料的管道,也可以 Attach 給一個自定義實現的 TCP 全雙工傳輸管道(此方式本文不講,但文末會直接給出示例)。現在我們需要一個實現了 `IJsonRpcMessageHandler` 介面的處理程式,StreamJsonRpc 已經實現好了,它是 `WebSocketMessageHandler` 類。通過 Attach 該例項,可以拿到一個用於呼叫 RPC 服務的物件。程式碼示例如下: ```cs Console.WriteLine("開始向服務端傳送訊息..."); var messageHandler = new WebSocketMessageHandler(webSocket); var greeterClient = JsonRpc.Attach(messageHandler); var request = new HelloRequest { Name = "精緻碼農" }; var response = await greeterClient.SayHelloAsync(request); Console.WriteLine($"收到來自服務端的響應:{response.Message}"); ``` 你會發現,定義客戶端和服務端契約的好處是可以實現強型別程式設計。接下來看服務端如何接收並處理客戶端傳送的訊息。 ## 接收請求 和前一篇一樣,我們先定義一個 GreeterServer 類用來處理接收到的客戶端訊息。 ```cs public class GreeterServer : IGreeter { private readonly ILogger _logger; public GreeterServer(ILogger logger) { _logger = logger; } public Task SayHelloAsync(HelloRequest request) { _logger.LogInformation("收到並回復了客戶端訊息"); return Task.FromResult(new HelloResponse { Message = $"您好, {request.Name}!" }); } } ``` 同樣,WebSocket 服務端也需要使用 Attach 來告訴 StreamJsonRpc 資料如何通訊,而且使用的也是 `WebSocketMessageHandler` 類,方法與客戶端類似。在前一篇中,我們 Attach 一個 Stream 呼叫的方法是: ```cs public static JsonRpc Attach(Stream stream, object? target = null); ``` 同理,我們推測應該也有一個這樣的靜態過載方法: ```cs public static JsonRpc Attach(IJsonRpcMessageHandler handler, object? target = null); ``` 可惜,StreamJsonRpc 並沒有提供這個靜態方法。既然 Attach 方法返回的是一個 JsonRpc 物件,那我們是否可以直接例項化該物件呢?檢視該類的定義,我們發現是可以的,而且有我們需要的建構函式: ```cs public JsonRpc(IJsonRpcMessageHandler messageHandler, object? target); ``` 接下來就簡單了,一切和前一篇的 Stream 示例都差不多。在 RpcController 的 Greeter Action 中例項化一個 JsonRpc,然後開啟訊息監聽。 ```cs public class RpcController : ControllerBase { private readonly ILogger _logger; private readonly GreeterServer _greeterServer; public RpcController(ILogger logger, GreeterServer greeterServer) { _logger = logger; _greeterServer = greeterServer; } [Route("/rpc/greeter")] public async Task 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 方法。客戶端完整示例程式碼如下: ```cs 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(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 的啟動方式改成自啟動。 ![ ](https://img2020.cnblogs.com/blog/191097/202007/191097-20200715203513168-761745528.png) 另外,為了更方便地同時執行客戶端和服務端應用,可以把解決方案設定成多啟動。右鍵解決方案,選擇“Properties”,把對應的專案設定“Start”即可。 ![ ](https://img2020.cnblogs.com/blog/191097/202007/191097-20200715202446183-1806619492.png) 如果你用的是 VS Code,也是支援多啟動除錯的,具體方法你自行 Google。如果你用的是 `dotnet run` 命令執行專案可忽略以上設定。 專案執行後的截圖如下: ![ ](https://img2020.cnblogs.com/blog/191097/202007/191097-20200715224827257-1003905483.png) 你也可以自定義實現 TCP 全雙工通訊管道,但比較複雜而且也很少這麼做,所以就略過不講了。但我在 GitHub 的示例程式碼也放了一個自定義全雙工管道實現的示例,感興趣的話你可以克隆下來研究一下。 ![ ](https://img2020.cnblogs.com/blog/191097/202007/191097-20200715224142892-442190843.png) 該示例執行截圖: ![ ](https://img2020.cnblogs.com/blog/191097/202007/191097-20200715223542031-1422704591.png) ## 本篇總結 本文通過示例演示瞭如何使用 StreamJsonRpc 基於 WebSocket 資料傳輸實現 JSON-RPC 協議的 RPC 通訊。其中客戶端和服務端有共同的契約部分,實現了強型別程式設計。通過示例我們也清楚了 StreamJsonRpc 這個庫為了實現 RPC 通訊做了哪些工作,其實它就是在現有傳輸管道(Stream、WebSocket 和 自定義 TCP 連線)上進行資料通訊。正如前一篇所說,由於 StreamJsonRpc 把大部分我們不必要知道的細節做了封裝,所以在示例中感覺不到 JSON-RPC 協議帶來的統一規範,也沒看到具體的 JSON 格式的資料。其實只要遵循了 JSON-RPC 協議實現的客戶端或服務端,不管是用什麼語言實現,都是可以互相通訊的。 希望這三篇關於 StreamJsonRpc 的介紹能讓你有所收穫,如果你在工作中計劃使用 StreamJsonRpc,這幾篇文章包括示例程式碼應該有值得參考的地方。