StreamJsonRpc 是一個實現了 JSON-RPC 通信協議的 .NET 庫
.NET 開源項目 StreamJsonRpc 介紹 [上篇]
StreamJsonRpc 是一個實現了 JSON-RPC 通信協議的開源 .NET 庫,在介紹 StreamJsonRpc 之前,我們先來了解一下 JSON-RPC。
JSON-RPC 介紹
JSON-RPC 是一個無狀態且輕量級的遠程過程調用(RPC)協議,其使用 JSON(RFC 4627)作為數據格式。
目前 JSON-RPC 的版本已發展到 2.0,JSON-RPC 2.0 與 1.0 的約定規范是不一樣的。2.0 包含一個名為 jsonrpc
且值為 2.0
的成員,而 1.0 版本是不包含的。所以我們可以很容易在兩個版本間區分出 2.0。
JSON-RPC 在客戶端與服務端之間交換的所有成員名應是區分大小寫的,函數、方法、過程都認為是可互換的。客戶端被定義為請求對象的來源及響應對象的處理程序;服務端被定義為響應對象的起源和請求對象的處理程序。
請求對象
發送一個請求對象至服務端代表一個 RPC 調用,JSON-RPC 2.0 規定一個請求對象包含下列成員:
- jsonrpc:指定 JSON-RPC 協議版本的字符串,必須准確寫為“2.0”。
- method:包含所要調用方法名稱的字符串,以 rpc 開頭的方法名,用英文句號連接的為預留給 rpc 內部的方法名及擴展名,且不能在其他地方使用。
- params:調用方法所需要的結構化參數值,該成員參數可以被省略。
- id:已建立客戶端的唯一標識,值必須包含一個字符串、數值或 NULL 空值。如果不包含該成員則被認定為是一個通知。該值一般不為 NULL,若為數值則不應該包含小數。
沒有包含 id
成員的請求對象為通知,作為通知的請求對象表明客戶端對服務端響應不感興趣,服務端可以不響應請求對象給客戶端。
下面是幾個請求對象的 JSON 結構示例(“-->”表示發送,“<--”表示響應,下同):
--> { "jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1 } --> { "jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 4} --> {"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]} // 通知
響應對象
當客戶端發起一個 RPC 調用時,除通知之外,服務端都必須回復響應。響應也表示為一個 JSON 對象,使用以下成員:
- jsonrpc:指定 JSON-RPC 協議版本的字符串,必須准確寫為“2.0”。
- result:調用成功時響應給客戶端的結果,當調用發生錯誤時可以不包含該成員。
- error:調用發生錯誤時返回給客戶端的錯誤信息,在調用失敗時必須包含該成員。
- id:對應請求對象的“id”,其值必須與請求對象中的“id”值一致。
響應對象必須包含 result 或 error 成員之一。
響應對象的 error 成員的結構包含下列成員:
- code:使用數值表示該異常的錯誤類型,必須為整數。、
- message:對該錯誤的簡單描述字符串,該描述應盡量限定在簡短的一句話。
- data:包含關於錯誤的附加信息,可忽略。
其中 -32768 至 -32000 為保留的預定義錯誤代碼,各保留錯誤代碼的含義請查看文末參考鏈接[1]。
下面是幾個響應對象的 JSON 結構示例:
<-- {"jsonrpc": "2.0", "result": 19, "id": 1} <-- {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "1"} <-- {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null} // 無效調用
批量調用
當需要同時發送多個請求對象時,客戶端可以發送一個包含所有請求對象的數組。
當批量調用的所有請求對象處理完成時,服務端則需要返回一個包含相對應的響應對象數組。每個響應對象都應對應每個請求對象,除非是通知的請求對象。服務端可以並發的,可以以任意順序和任意寬度並行處理這些批量調用。而客戶端應該是基於各個響應對象中的 id 成員來匹配對應的請求對象。
若批量調用沒有需要返回的響應對象,則服務端不需要返回任何結果。
下面是一個批量請求及響應的 JSON 結構示例:
--> [
{"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"}, {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]}, {"foo": "boo"}, {"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"}, {"jsonrpc": "2.0", "method": "get_data", "id": "9"} ] <-- [ {"jsonrpc": "2.0", "result": 7, "id": "1"}, {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}, {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "5"}, {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"} ]
當批量請求對象都是通知時,服務端不需要返回結果。
StreamJsonRpc 庫介紹
StreamJsonRpc 是一個實現了 JSON-RPC 通信協議的 .NET 庫,支持 .NET Core。它把 RPC 的調用封裝為公開的 .NET API,可以很方便的進行 RPC 請求的發送和接收操作。StreamJsonRpc 是微軟官方的一個開源庫,目前 Star 數接近 300,貌似知道的人不多或者用的人不多。GitHub 地址:
github.com/microsoft/vs-streamjsonrpc
StreamJsonRpc 可以在 Stream、WebSocket 或 System.IO.Pipelines 管道上工作,獨立於底層傳輸。除了包含 JSON-RPC 規范所需的特性外,它額外還有如下優點:
- 請求取消
- .NET 事件作為通知
- 動態客戶端代理生成
- 支持緊湊的 MessagePack 二進制序列化
- 易於實現插件式架構的消息處理和格式化
使用 StreamJsonRpc 主要有四個基本步驟:建立 JSON-RPC 連接、發送 RPC 請求、接收 RPC 請求、斷開連接。
這一篇主要介紹一些預備知識,下一篇將通過示例演示並詳細介紹 StreamJsonRpc 的使用,敬請期待!
參考:
[1].jsonrpc.org/specification
[2].github.com/microsoft/vs-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 中有 PipeStream
、NetworkStream
等,本示例用的是 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 的使用,並會盡量以更貼合實際應用場景的示例來講解。
大家好,這是 .NET 開源項目 StreamJsonRpc 介紹的最后一篇。上篇介紹了一些預備知識,包括 JSON-RPC 協議介紹,StreamJsonRpc 是一個實現了 JSON-RPC 協議的庫,它基於 Stream、WebSocket 和自定義的全雙工管道傳輸。中篇通過示例講解了 StreamJsonRpc 如何使用全雙工的 Stream 作為傳輸管道實現 RPC 通訊。本篇(下篇)將繼續通過示例講解如何基於 WebSocket 傳輸管道實現 RPC 通訊。
准備工作
為了示例的完整性,本文示例繼續在中篇創建的示例基礎上進行。該示例的 GitHub 地址為:
github.com/liamwang/StreamJsonRpcSamples
我們繼續添加三個項目,一個是名為 WebSocketSample.Client 的 Console 應用,一個是名為 WebSocketSample.Server 的 ASP.NET Core 應用,還有一個名為 Contract 的契約類庫(和 gRPC 類似)。
你可以直接復制並執行下面的命令一鍵完成大部分准備工作:
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 類:
public class HelloRequest { public string Name { get; set; } }
用於服務端響應的數據的 HelloResponse 類:
public class HelloResponse { public string Message { get; set; } }
用於約定服務端和客戶端行為的 IGreeter 接口:
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,這幾篇文章包括示例代碼應該有值得參考的地方。