淺議 C# 客戶端和服務端通信的幾種方法: Rest 和 Grpc 和其他
在C#客戶端和C#服務器之間進行通信的方法有很多。一些功能強大,而其他功能則不是很多。有些非常快,有些則不是。知道不同的選擇很重要,這樣您才能決定最適合自己的選擇。
本文將介紹當今最流行的技術,以及為何如此廣泛地使用它們。我們將討論REST,gRPC及其兩者之間的所有內容。
最佳方案
讓我們考慮一下我們希望如何在最佳環境中使客戶端與服務器之間的通信看起來像。我在想像這樣的東西:
- // on client side
- public void Foo()
- {
- var server = new MyServer(new Uri("https://www.myserver.com/");)
- int sum = server.Calculator.SumNumbers(12,13);
- }
- // on server side
- class CalculatorController : Controller{
- public int SumNumbers(int a, int b)
- {
- return a + b;
- }
- }
我當然想要完整的Intellisense。當我單擊server并. 希望Visual Studio顯示所有控制器時。當我單擊CalculatorController和時.,我想查看所有操作。我還想要一流的性能,很少的網絡負載和雙向通信。而且我想要一個能夠完美處理版本控制的強大系統,這樣我就可以毫不費力地部署新的客戶端版本和新的服務器版本。
要求太多嗎?
請注意,我在這里談論的是無狀態API。這等效于C#項目,其中只有兩種類型的類:
?靜態類,只有靜態方法。
?POCO類[1]僅具有類型為基本類型或其他POCO類的字段和屬性。
在API中使用狀態會帶來復雜性,而這正是萬惡之源。因此,為了本文的方便,讓我們保持美好和無狀態。
傳統REST
REST API出現在2000年代初期,席卷了整個互聯網。到目前為止,它是創建Web服務的最流行的方法。
REST為客戶端到服務器的請求定義了一組固定的操作GET,POST,PUT和DELETE。每個請求都將通過包含有效負載(通常為JSON)的響應來回答。請求包含在查詢本身中的參數,或者在它是POST請求時包含為有效負載(通常為JSON)的參數。
有一個稱為RESTful API的標準,它定義了以下規則(您實際上不必使用它):
- GET用于檢索資源
- PUT用于更改資源狀態
- POST用于創建資源
- DELETE用于刪除資源
如果您到目前為止還不熟悉REST,則上面的解釋可能不會減少它,因此這里有一個示例。在.NET中,內置了對REST的支持。實際上,默認情況下,ASP.NET Web API被構建為REST Web服務。這是典型的客戶端和ASP.NET服務器的外觀:
在服務器中:
- [Route("People")]
- public class PeopleController : Controller
- {
- [HttpGet]
- public Person GetPersonById(int id)
- {
- Person person = _db.GetPerson(id);
- return person;//Automatically serialized to JSON
- }
- }
在客戶中:
- var client = new HttpClient();
- string resultJson =
- await client.GetStringAsync("https://www.myserver.com/People/GetPersonById?id=123");
- Person person = JsonConvert.DeserializeObject<Person>(resultJson);
REST非常方便,但是并沒有達到最佳方案。因此,讓我們看看是否可以做得更好。
ReFit
ReFit不能替代REST。相反,它建立在REST之上,并允許我們像調用簡單方法一樣調用服務器端點。這是通過在客戶端和服務器之間共享接口來實現的。在服務器端,您的控制器將實現一個接口:
- public interface IMyEmployeeApi
- {
- [Get("/employee/{id}")]
- Task<Employee> GetEmployee(string id);
- }
然后,在客戶端,您需要包括相同的接口并使用以下代碼:
- var api = RestService.For<IMyEmployeeApi>("https://www.myserver.com");
- var employee = await api.GetEmployee("abc");
就這么簡單。除了幾個NuGet軟件包外,無需運行困難的自動化程序或使用任何第三方工具。
這更接近最佳方案。現在,我們有了IntelliSense,并且客戶端和服務器之間的合同很牢固。但是還有另一種選擇,在某些方面甚至更好。
昂首闊步
像ReFit一樣,Swagger也建立在REST之上。OpenAPI[2]或Swagger是REST API的規范。它描述了具有簡單JSON文件的REST Web服務。這些文件是Web服務的API架構。它們包括:
?API中的所有路徑(URL)。
?每個路徑的預期操作(GET,POST等)。每個路徑可以處理不同的操作。例如,單個路徑https://mystore.com/Product可能接受添加產品的POST操作和返回產品的GET操作。
- 每個路徑和操作的預期參數。
- 每個路徑的預期響應。
- 每個參數和響應對象的類型。
該JSON文件實質上是客戶端和服務器之間的合同。這是一個描述一個稱為Swagger Petstore[3]的Web服務的swagger文件的示例(為清楚起見,我刪除了一些部分):
- {
- "swagger":"2.0",
- "info":{
- "version":"1.0.0",
- "title":"Swagger Petstore",
- "description":"A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification",
- },
- "host":"petstore.swagger.io",
- "basePath":"/api",
- "schemes":[
- "http"
- ],
- "consumes":[
- "application/json"
- ],
- "produces":[
- "application/json"
- ],
- "paths":{
- "/pets":{
- "get":{
- "description":"Returns all pets from the system that the user has access to",
- "operationId":"findPets",
- "produces":[
- "application/json",
- "application/xml",
- ],
- "parameters":[
- {
- "name":"tags",
- "in":"query",
- "description":"tags to filter by",
- "required":false,
- "type":"array",
- "items":{
- "type":"string"
- },
- "collectionFormat":"csv"
- },
- {
- "name":"limit",
- "in":"query",
- "description":"maximum number of results to return",
- "required":false,
- "type":"integer",
- "format":"int32"
- }
- ],
- "responses":{
- "200":{
- "description":"pet response",
- "schema":{
- "type":"array",
- "items":{
- "$ref":"#/definitions/Pet"
- }
- }
- },
- ...
讓我們考慮一下這個結果。使用上面的JSON文件,您可以潛在地創建具有完整IntelliSense的C#客戶端。畢竟,您知道所有路徑,操作,它們期望的參數,什么參數類型,什么是響應等等。
有幾種工具可以做到這一點。對于服務器端,可以使用Swashbuckle.AspNetCore[4]將Swagger添加到ASP.NET中并生成所述JSON文件。對于客戶端,您可以使用swagger-codegen[5]和AutoRest[6]來使用這些JSON文件并生成客戶端。讓我們看一個如何做到這一點的例子:
將Swagger添加到ASP.NET服務器
首先添加NuGet包Swashbuckle.AspNetCore[7]。在中ConfigureServices,注冊Swagger生成器:
- services.AddSwaggerGen(options =>
- options.SwaggerDoc("v1", new OpenApiInfo {Title = "My Web API", Version = "v1"}));
在添加Configure方法中Startup.cs:
- app.UseSwagger();
最后,控制器內部的動作應使用[HttpXXX]和[FromXXX]屬性修飾:
- [HttpPost]
- public async Task AddEmployee([FromBody]Employee employee)
- {
- //...
- }
- [HttpGet]
- public async Task<Employee> Employee([FromQuery]string id)
- {
- //...
- }
就像服務器端一樣簡單。運行項目時,swagger.json將生成一個文件,可用于生成客戶端。
使用AutoRest從Swagger生成客戶端
要開始使用AutoRest[8],與安裝NPM[9]:npm install -g autorest。安裝后,您將需要使用AutoRest的命令行界面從該swagger.json文件生成C#客戶端。這是一個例子:
- autorest --input-file="./swagger.json" --output-folder="GeneratedClient" --namespace="MyClient" --override-client-name="MyClient" --csharp
這將產生一個GeneratedClient包含生成的C#文件的文件夾。請注意,名稱空間和客戶端名稱被覆蓋。從這里,將此文件夾添加到Visual Studio中的客戶端項目。
您需要安裝Microsoft.Rest.ClientRuntimeNuGet軟件包,因為生成的代碼取決于該軟件包。安裝后,您可以像使用常規C#類一樣使用API:
- var client = new MyClient();
- Employee employee = client.Employee(id: "abc");
您可以在AutoRest的文檔中[10]閱讀一些細微之處。而且您需要使該過程自動化,因此我建議閱讀Patrik Svensson的教程,[11]以獲得一些好的建議以及Peter Jausovec的這篇文章[12]。
Swagger的問題是JSON文件是在運行時創建的,因此這使得在CI / CD流程中實現自動化有點困難。
傳統REST vs Swagger vs ReFit
進行選擇時,請注意以下幾點。
- 如果您有一個非常簡單的私有REST API,則也許不必理會客戶端生成和共享接口。小任務并不能證明付出額外的努力是合理的。
- Swagger支持多種語言,而ReFit僅支持.NET。Swagger還是許多工具,測試,自動化和UI工具的基礎。如果您要創建一個大型的公共API,它將可能是最佳選擇。
- Swagger比ReFit復雜得多。使用ReFit,只需在服務器和客戶端項目中添加一個接口即可。另一方面,使用ReFit,您必須為每個控制器創建新的接口,而Swagger會自動進行處理。
但是在決定任何事情之前,請檢查與REST無關的第四個選項。
gRPC
gRPC[13](gRPC遠程過程調用)是Google開發的開源遠程過程調用系統。它有點像REST,它提供了一種將請求從客戶端發送到服務器的方式。但這在許多方面都不同,這是相同點和不同點:
- 像REST一樣,gRPC與語言無關。有適用于所有流行語言的工具,包括C#。
- gRPC是契約的基礎,并使用.proto文件來定義契約。這有點類似于Swaggerswagger.json和ReFit的共享界面。可以從那些文件中生成任何編程語言的客戶端。
- gRPC使用協議緩沖區(Protobuf)[14]二進制序列化。這與REST(通常序列化為JSON或XML)不同。二進制序列化較小,因此更快。
- gRPC用于使用HTTP / 2協議創建持久連接。該協議更簡單,更緊湊。REST使用HTTP 1.x協議(通常為HTTP 1.1)。
- HTTP 1.1要求每個請求都進行TCP握手,而HTTP / 2則保持連接打開。
- HTTP / 2連接使用多路復用流。這意味著單個TCP連接可以支持許多流。這些流可以并行執行,而不必像HTTP 1.1中那樣互相等待。
- gRPC允許雙向流。
有兩種使用gRPC的方法。對于.NET Core 3.0,有一個完全托管的庫,稱為.NET的gRPC[15]。對于其中的任何內容,您都可以使用gRPC C#[16],它是使用本機代碼構建的。這并不意味著適用于.NET的gRPC可以替代gRPC C#。讓我們來看一個用于.NET的更新gRPC的示例。
.NET的gRPC的服務器端
這不是教程,而是更多有關預期內容的一般性想法。這是示例控制器在gRPC中的外觀:
- public class GreeterService : Greeter.GreeterBase
- {
- public override Task<HelloReply> SayHello(HelloRequest request,
- ServerCallContext context)
- {
- _logger.LogInformation("Saying hello to {Name}", request.Name);
- return Task.FromResult(new HelloReply
- {
- Message = "Hello " + request.Name
- });
- }
- }
您需要添加以下的Configure在Startup.cs:
- app.UseEndpoints(endpoints =>
- {
- endpoints.MapGrpcService<GreeterService>();
- });
API在.proto文件中描述,該文件是項目的一部分:
- syntax = "proto3";
- service Greeter {
- rpc SayHello (HelloRequest) returns (HelloReply);
- }
- message HelloRequest {
- string name = 1;
- }
- message HelloReply {
- string message = 1;
- }
此.proto文件添加到.csproj:
- <ItemGroup>
- <Protobuf Include="Protos\greet.proto" />
- </ItemGroup>
.NET的gRPC客戶端
客戶端是從.proto文件生成的。代碼本身非常簡單:
- var channel = GrpcChannel.ForAddress("https://localhost:5001");
- var client = new Greeter.GreeterClient(channel);
- var response = await client.SayHello(
- new HelloRequest { Name = "World" });
- Console.WriteLine(response.Message);
gRPC與REST
gRPC聽起來不錯。它在框架下更快,更簡單。那么,我們都應該從REST變為gRPC嗎?答案是,這取決于你的應用場景。
以下是一些注意事項:
從我的印象來看,使用gRPC和ASP.NET仍然不是很好。借助對REST的成熟支持,您會變得更好。就基于契約的通信而言,這很不錯,除了在REST中有我們已經討論過的類似替代方案:Swagger和ReFit。
最大的優勢是性能。根據這些基準[17],在大多數情況下,gRPC更快。特別是對于大型有效載荷,Protobuf序列化確實有所作為。這意味著對于高負載服務器而言,這是一個巨大的優勢。
在大型ASP.NET應用程序中從REST過渡到gRPC將非常困難。但是,如果您具有基于微服務的體系結構,那么逐步完成此過渡就變得容易得多。
其他溝通方式
還有其他一些我完全沒有提及的通信方式,但是值得一提的是:
- GraphQL[18]是Facebook開發的API的查詢語言。它允許客戶端從服務器確切地要求它需要的數據。這樣,您可以在服務器上僅創建一個端點,該端點將非常靈活,并且僅返回客戶端所需的數據。近年來,GraphQL變得非常流行。
- SignalR[19]是一項允許服務器與客戶端之間進行實時雙向通信的技術。SignalR不僅允許客戶端始終向服務器發送請求,還允許服務器向客戶端發送推送通知。這樣可以查看Web應用程序中的實時更新。SignalR在ASP.NET中非常流行。
- TcpClient[20]和TcpListener[21](在中System.Net.Sockets)提供基于TCP的低級連接。基本上,您將建立連接并傳輸字節數組。對于大型應用程序而言,它不是理想的選擇,在大型應用程序中,您可以使用ASP.NET的控制器和操作在大型API中進行訂購。
- UdpClient[22]提供了一種通過UDP協議進行通信的方法。TCP建立連接,然后發送數據,而UDP僅發送數據。TCP確保數據中沒有錯誤,而UDP沒有。UDP可以更有效地快速傳輸數據,您不必擔心它是否可靠且沒有錯誤。一些示例是:視頻流,實時廣播和IP語音(VoIP)。
- WCF[23]是一種較舊的技術,主要在進程之間使用基于SOAP的通信。這是一個龐大的框架,我要說的是它已不再受REST和JSON負載的歡迎。
References
[1] POCO類: https://www.c-sharpcorner.com/UploadFile/5d065a/poco-classes-in-entity-framework/
[2] OpenAPI: https://swagger.io/specification/
[3] Swagger Petstore: https://bfanger.nl/swagger-explained/#operationObject
[4] Swashbuckle.AspNetCore: https://github.com/domaindrivendev/Swashbuckle.AspNetCore
[5] swagger-codegen: https://github.com/swagger-api/swagger-codegen
[6] AutoRest: https://azure.github.io/autorest/
[7] Swashbuckle.AspNetCore: https://www.nuget.org/packages/Swashbuckle.AspNetCore
[8] AutoRest: https://github.com/Azure/autorest
[9] NPM: https://www.w3schools.com/nodejs/nodejs_npm.asp
[10] 文檔中: https://azure.github.io/autorest/client/ops.html
[11] 教程,: https://www.patriksvensson.se/2018/10/generating-api-clients-using-autorest
[12] 文章: https://medium.com/@pjausovec/creating-c-client-library-for-web-api-projects-be132c831f9c
[13] gRPC: https://grpc.io/
[14] 協議緩沖區(Protobuf): https://en.wikipedia.org/wiki/Protocol_Buffers
[15] .NET的gRPC: https://github.com/grpc/grpc-dotnet
[16] gRPC C#: https://github.com/grpc/grpc/tree/master/src/csharp
[17] 根據這些基準: https://www.yonego.com/nl/why-milliseconds-matter/#gref
[18] GraphQL: https://graphql.org/
[19] SignalR: https://github.com/SignalR/SignalR
[20] TcpClient: https://docs.microsoft.com/en-us/dotnet/api/system.net.sockets.tcpclient?view=netframework-4.8
[21] TcpListener: https://docs.microsoft.com/en-us/dotnet/api/system.net.sockets.tcplistener?view=netframework-4.8
[22] UdpClient: https://docs.microsoft.com/en-us/dotnet/api/system.net.sockets.udpclient?view=netframework-4.8
[23] WCF: https://docs.microsoft.com/en-us/dotnet/framework/wcf/whats-wcf