閱讀本文大概須要 11 分鐘。git
上一篇介紹了一些預備知識,包括 JSON-RPC 介紹和實現了 JSON-RPC 的 StreamJsonRpc 介紹,講到了 StreamJsonRpc 能夠經過 .NET 的 Stream 類和 WebSocket 類實現 JSON-RPC 協議的通訊。本篇就先選擇其中的 Stream 類來說解,經過具體的示例講解如何使用 StreamJsonRpc 實現 RPC 調用。github
先新建兩個 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 通信要經歷四個步驟:創建鏈接、發送請求、接收請求、斷開鏈接,其中發送請求和接收請求能夠歸爲數據通信,下面按照這幾個步驟順序來逐步講解。json
使用 Stream 實現 JSON-RPC 協議的通信,要求該 Stream 必須是一個全雙工 Stream(可同時接收數據和發送數據)或是一對半雙工 Stream(本文不做討論)。實現了全雙工的 Stream 類在 .NET 中有 PipeStream
、NetworkStream
等,本示例用的是 NamedPipeClientStream
類和 NamedPipeServerStream
,前者用於客戶端,後者用於服務端。bash
先看服務端代碼示例:服務器
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
來標識區分它們。async
再來看客戶端代碼示例:函數
var stream = new NamedPipeClientStream(".", "StringJsonRpc", PipeDirection.InOut, PipeOptions.Asynchronous); Console.WriteLine("正在鏈接服務器..."); await stream.ConnectAsync(); Console.WriteLine("已創建鏈接!");
和服務器相似,客戶端使用的是 NamedPipeClientStream
類來創建鏈接,在其構造參數中須要指定服務端的地址(這裏用了.
表明本機)和通信管道的名稱。Stream 實例經過 ConnectAsync
方法主動向服務器請求鏈接。spa
若是網絡是通的,客戶端和服務端就能成功創建鏈接。下面就要實現客戶端和服務端之間的數據通信了,即客戶端發送請求和服務端接收並處理請求。
客戶端與服務端創建鏈接後,數據不會平白無故從一端流到另外一端,要實現兩端的數據通信還須要先把通信管道架設起來,在其兩端設定對應的控制和處理程序。工程上這個聽起來好像不簡單,但對於 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 的使用,並會盡可能以更貼合實際應用場景的示例來說解。