引入
gRPC 是谷歌推出的一個高效能優秀的 RPC 框架,基於 HTTP/2 實現。並且該框架對 .NET Core 有著優秀的支援。
最近在做一個專案正好用到了 gRPC,遇到了需要串流傳輸的問題。
專案建立
首先還是需要安裝 .net core sdk,可以去 http://dot.net 下載。這裡我使用的是 2.2.103 版本的 sdk。
mkdir RpcStreaming cd RpcStreaming dotnet new console dotnet add package Grpc // 新增 gRPC 包 dotnet add package Grpc.Tools // 新增 gRPC 工具包 dotnet add package Google.Protobuf // 新增 Protobuf 支援
然後為了支援 protobuf 語言,我們需要修改專案配置檔案,在專案中引入 .proto 檔案以便生成對應的程式碼。
<Project Sdk="Microsoft.NET.Sdk"> ... <PropertyGroup> ... <LangVersion>latest</LangVersion> ... </PropertyGroup> <ItemGroup> ... <Protobuf Include="**/*.proto" /> ... </ItemGroup> ... </Project>
這裡我們使用了 wildcard 語法匹配了專案內的全部 proto 檔案用於生成對應的程式碼。
編寫 Proto 檔案
我們在專案目錄下建立一個 .proto 檔案,用於描述 rpc 呼叫和訊息型別。比如:RpcStreaming.proto
內容如下:
1 synatx = "proto3"; 2 service RpcStreamingService { 3 rpc GetStreamContent (StreamRequest) returns (stream StreamContent) {} 4 } 5 message StreamRequest { 6 string fileName = 1; 7 } 8 message StreamContent { 9 bytes content = 1; 10 }
做 RPC 請求時,我們向 RPC 伺服器傳送一個 StreamRequest 的 message,其中包含了檔案路徑;為了讓伺服器以流式傳輸資料,我們在 returns 內加一個 “stream”。
編寫 Server 端程式碼
為了編寫 RPC 呼叫服務端程式碼,我們需要重寫自動生成的 C# 虛擬函式。
首先我們進入 ./obj/Debug/netcoreapp2.2 看看自動生成了什麼程式碼。
RpcStreaming.cs 中包含訊息型別的定義,RpcStreamingGrpc.cs 中包含了對應 rpc 呼叫的函式原型。
我們查詢一下我們剛剛在 proto 檔案中宣告的 GetStreamContent。
可以在裡面找到一個上方文件註釋為 “Base class for server-side implementations RpcStreamingServiceBase” 的抽象類 RpcStreamingServiceBase,裡面包含了我們要找的東西。
public virtual global::System.Threading.Tasks.Task GetStreamContent(global::StreamRequest request, grpc::IServerStreamWriter<global::StreamContent> responseStream, grpc::ServerCallContext context) { throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, "")); }
這樣就簡單了,我們新建一個類 RpcServiceImpl,繼承 RpcStreamingService.RpcStreamingServiceBase,然後實現對應的方法即可。
為了串流,我們需要將資料流不斷寫入 response,這裡給一個簡單的示例。
1 using System; 2 using System.IO; 3 using System.Threading.Tasks; 4 using Google.Protobuf; 5 using Grpc.Core; 6 namespace RpcStreaming 7 { 8 public class RpcStreamingServiceImpl : RpcStreamingService.RpcStreamingServiceBase 9 { 10 public override Task GetStreamContent(StreamRequest request, IServerStreamWriter<StreamContent> response, ServerCallContext context) 11 { 12 return Task.Run(async () => 13 { 14 using (var fs = File.Open(request.FileName, FileMode.Open)) // 從 request 中讀取檔名並開啟檔案流 15 { 16 var remainingLength = fs.Length; // 剩餘長度 17 var buff = new byte[1048576]; // 緩衝區,這裡我們設定為 1 Mb 18 while (remainingLength > 0) // 若未讀完則繼續讀取 19 { 20 var len = await fs.ReadAsync(buff); // 非同步從檔案中讀取資料到緩衝區中 21 remainingLength -= len; // 剩餘長度減去剛才實際讀取的長度 22 23 // 向流中寫入我們剛剛讀取的資料 24 await response.WriteAsync(new StreamContent 25 { 26 Content = ByteString.CopyFrom(buff, 0, len) 27 }); 28 } 29 } 30 }); 31 } 32 } 33 }
啟動 RPC Server
首先需要:
1 using Google.Protobuf; 2 using Grpc.Core;
然後我們在 Main 函式中構建並啟動 RPC Server,監聽 localhost:23333
1 new Server 2 { 3 Services = { RpcStreamingService.BindService(new RpcStreamingServiceImpl()) }, // 繫結我們的實現 4 Ports = { new ServerPort("localhost", 23333, ServerCredentials.Insecure) } 5 }.Start(); 6 Console.ReadKey();
這樣服務端就構建完成了。
編寫客戶端呼叫 RPC API
方便起見,我們先將 Main 函式改寫為 async 函式。
1 // 原來的 Main 函式 2 static void Main(string[] args) { ... } 3 // 改寫後的 Main 函式 4 static async Task Main(string[] args) { ... }
另外,還需要:
1 using System; 2 using System.IO; 3 using System.Threading.Tasks; 4 using Google.Protobuf; 5 using Grpc.Core;
然後我們在 Main 函式中新增呼叫程式碼:
1 var channel = new Channel("localhost:23333", ChannelCredentials.Insecure); // 建立到 localhost:23333 的 channel 2 var client = new RpcStreamingService.RpcStreamingServiceClient(channel); // 建立 client 3 // 呼叫 RPC API 4 var result = client.GetStreamContent(new StreamRequest { FileName = "你想獲取的檔案路徑" }); 5 var iter = result.ResponseStream; // 拿到響應流 6 using (var fs = new FileStream("寫獲取的資料的檔案路徑", FileMode.Create)) // 新建一個檔案流用於存放我們獲取到資料 7 { 8 while (await iter.MoveNext()) // 迭代 9 { 10 iter.Current.Content.WriteTo(fs); // 將資料寫入到檔案流中 11 } 12 }
測試
dotnet run
會發現,我們想要獲取的檔案的資料被不斷地寫到我們指定的檔案中,每次 1 Mb。在我的電腦上測試,內網的環境下傳輸速度大概 80~90 Mb/s,幾乎跑滿了我的千兆網路卡,速度非常理想。