閱讀本文大概須要 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,這幾篇文章包括示例代碼應該有值得參考的地方。