65.9K
CodeProject 正在变化。 阅读更多。
Home

gRPC、.NET 8.0 和 Kestrel 简单示例

starIconstarIconstarIconstarIconstarIcon

5.00/5 (11投票s)

2024年5月8日

MIT

22分钟阅读

viewsIcon

12522

我将演示在 Kestrel ASP.NET 服务器中添加 gRPC 功能,并使用各种客户端测试 gRPC 服务的示例。

ASP.NET 8.0 下的 Web gRPC,Kestrel 与 HTML/JavaScript 和 Avalonia 客户端

致谢

此致敬意,作为我送给 Kahua.com 的好朋友们的告别礼物,包括

  • Colin Whitlock - 他不仅是一位出色的 CTO,还是一位出色的经理、架构师和公司开发人员。
  • Jon Sinsel - 一位极其敏锐的 C# 大师,他非常耐心地向我介绍了 Uno Platform 和 Kahua 代码。
  • Adam Love - 最好的 Web、移动向导(也是一位出色的 C# 开发人员),如果没有他的帮助,我可能仍然在琢磨如何部署 ASP.NET 应用程序。
  • Jonas Mayor - 杰出的 C# 软件工程师,他向我介绍了 IIS URL 重写模块和 ASP.NET 中间件。

引言

gRPC

gRPC 是“Google Remote Procedure Calls”的缩写。它是一种用于服务器和客户端之间通信的高性能多平台技术。

以下是 gRPC 的优势

  1. gRPC 消息比基于文本的协议(REST 和 SOAP)小得多,因此 gRPC 通信速度快得多。
  2. gRPC 可用于 Web 通信。
  3. gRPC 支持服务器端。这很重要,因为许多其他流行的 Web 协议(例如 REST)不支持流。
  4. gRPC (浏览器除外)支持客户端以及双向流(客户端和服务器同时通过已建立的流交换信息)。浏览器中的 gRPC 不支持客户端流是一个相当糟糕的限制,可能需要切换到其他技术,例如 SignalR 用于多会话客户端。
  5. 服务器和客户端 gRPC 几乎可以在任何平台上运行,并使用各种不同的软件语言,包括但不限于 C#、Java、JavaScript、Python。
  6. gRPC 非常简单易学。

 

Kestrel 和 ASP.NET 8.0

gRPC 服务作为 Kestrel ASP.NET 服务器的一部分实现。

Kestrel 是一个完全用 .NET Core 编写的 Microsoft Web 服务器,因此它是 100% 多平台的。

Kestrel 非常适合处理 ASP.NET 和 gRPC 请求/响应,但缺少一些 Web 服务器功能,因此通常部署在另一个 Web 服务器之后,该服务器充当 ASP 和 gRPC 请求/响应的反向代理。

在 Windows 上,反向代理服务器通常是 IIS,而在 Linux 上可以是 Apache 或其他任何东西。

IIS 和 Kestrel 之间的集成在 .NET 8.0 中自然实现,几乎不需要代码更改。

Avalonia

Avalonia 是一个出色的开源 .NET 框架,用于使用 C# 和 XAML 在多个平台(包括 Windows、macOS、Linux、WebAssembly、Android 和 iOS)上编写 UI。

除了多平台之外,Avalonia 框架比 WPF 或任何 JavaScript/TypeScript 框架都要好得多,功能也更强大。

Avalonia 允许创建速度快、质量高的应用程序,并且在各种平台上的行为方式差异很小。

Avalonia 可用于使用 WebAssembly 创建 Web 应用程序。

本文的主要目的

本文提供了简单且解释清晰的示例,用于使用 Windows 上的 IIS/Kestrel ASP.NET 功能构建和部署 gRPC 服务,并由各种 gRPC 客户端使用。以下是本文中介绍的 gRPC 客户端

  1. C# 控制台程序(在任何平台上的 C# 桌面应用程序中都完全相同)。
  2. HTML/JavaScript Web 浏览器客户端。
  3. C# 非 Blazor Avalonia WebAssembly 客户端。我倾向于不使用 Blazor 的原因是,从 Web 上的不同来源我了解到 C# WebAssembly 的非 Blazor 技术及其与 HTML/JavaScript 的交互更稳定。

关于使用 ASP.NET 的重要说明

我想强调的是,ASP.NET 通常用于动态生成 HTML/JavaScript 页面。

为了应用程序的速度和清晰度,我更喜欢仅使用 ASP.NET 提供后端服务,几乎不生成任何 HTML/JavaScript。

上述规则的一个例外是 - 有时,我只在初始状态(页面首次加载时)通过代码生成将常量全局配置参数添加到 ASP.NET 页面。

AWebPros.com 网站演示 IIS/Kestrel 下运行的 gRPC 示例

使用本文中描述的代码,我构建了一个网站 ASP gRPC 示例,演示了带有 IIS/Kestrel Web 服务器的 Web HTML/JavaScript 和 Avalonia WebAssembly 客户端。

本文主要关注 gRPC 相关代码。

我构建和部署一个真实的 APS.NET 网站的奇妙旅程,我计划在未来的文章中进行描述。特别是,我计划涵盖以下主题

  1. WebAssembly、Avalonia 和 ASP.NET。
  2. 安装托管包,以便您的 ASP.NET 可以在 IIS 下运行。
  3. 在托管模型内外使用 IIS/Kestrel。
  4. 使用 Cors。
  5. 获取并部署免费 SSL 证书,以便您的网站不会被 Web 浏览器标记为不安全。
  6. 连接 ASP.NET 响应压缩以加速 WebAssembly 文件。
  7. 使用 Publish 部署 ASP.NET 网站。

浏览器中没有 gRPC 客户端流(不幸的是)

grpc-web(唯一可用于基于 gRPC 浏览器的客户端的 gRPC 框架)存在一个已知限制 - 它只允许服务器端流 (gRPC-Web 流)。

不允许客户端流双向流

通过 WebAssembly 使用 gRPC 似乎允许客户端和双向流,但实际上这是不正确的。

正如我们将在下面的示例中了解到的,当您尝试从 Web 浏览器客户端进行流式传输时,消息不会逐个发送,而是会在客户端积累,直到客户端指示流式传输结束。然后所有累积的消息会一起发送到服务器。

gRPC ASP.NET 代码示例

源代码

本文的源代码位于 ASP.NET gRCP 示例

为了最大限度地重用各种示例之间的代码,我构建了一个包罗万象的解决方案 - AspGrpcTests.sln(您无需打开它)以及围绕它的一些过滤解决方案 (.slnf) 文件。 .slnf 文件过滤出特定于每个示例的功能。

仅 Kestrel 代码示例

打开并运行仅 Kestrel 解决方案

Kestrel 可以作为独立进程或 Windows 服务使用,而无需 IIS。此示例(位于 GrpcKestrelOnlyImplementation.slnf 解决方案过滤器中)演示了作为控制台进程运行的仅 Kestrel 服务器与本地 C# 控制台客户端之间的 gRPC 通信。

要运行此示例,请在 Visual Studio 2022 中打开 GrpcKestrelOnlyImplementation.slnf。您可以在解决方案资源管理器中看到以下项目和文件

GrpcServerProcess 是包含 Kestrel 服务器的项目,ConsoleTestClient 是启动本地 gRPC C# 客户端的项目。

首先启动服务器进程(右键单击 GrpcServerProcess 并选择 Debug->Run Without Debugging)。这将启动 Kestrel 服务器作为 Windows 上的一个进程。请注意,由于服务器的输出不是 HTML 格式,它将启动一个浏览器,显示类似“找不到此本地页面”的错误。如果您希望服务器继续运行,请不要担心,也不要关闭浏览器。

然后以类似的方式启动控制台客户端(项目 ConsoleTestClient)。这是将在客户端控制台上打印的内容

Unary server call sample:
Hello Joe Doe!!!


Streaming Server Sample:
Hello Joe Doe 1
Hello Joe Doe 2
Hello Joe Doe 3
Hello Joe Doe 4
Hello Joe Doe 5
Hello Joe Doe 6
Hello Joe Doe 7
Hello Joe Doe 8
Hello Joe Doe 9
Hello Joe Doe 10
Hello Joe Doe 11
Hello Joe Doe 12
Hello Joe Doe 13
Hello Joe Doe 14
Hello Joe Doe 15
Hello Joe Doe 16
Hello Joe Doe 17
Hello Joe Doe 18
Hello Joe Doe 19
Hello Joe Doe 20


Streaming Server Sample with Error:
Hello Joe Doe 1
Hello Joe Doe 2
Hello Joe Doe 3
Hello Joe Doe 4
Hello Joe Doe 5
Hello Joe Doe 6
Hello Joe Doe 7
Hello Joe Doe 8
Hello Joe Doe 9
Hello Joe Doe 10
Hello Joe Doe 11
Status(StatusCode="Internal", Detail="Error Status Detail (for the client)")


Streaming Client Sample:
Hello Client_1, Client_2, Client_3


Bidirectional Streaming Client/Server Sample:
Hello Client_1_1
Hello Client_1_2
Hello Client_1_3
Hello Client_1_4
Hello Client_2_1
Hello Client_2_2
Hello Client_2_3
Hello Client_2_4
Hello Client_3_1
Hello Client_3_2
Hello Client_3_3
Hello Client_3_4

此示例演示了五种 gRPC 场景

  1. 一元示例 - 客户端向服务器发送“Joe Doe”字符串,服务器返回“Hello Joe Doe!”。
  2. 流式服务器示例 - 客户端发送单个字符串(“Joe Doe”),并返回多个问候语。
  3. 带错误的流式服务器示例 - 与上面的流式服务器示例相同,只是服务器在流中间(第 11 次迭代后)抛出 RpcException
  4. 流式客户端示例 - 客户端发送多个字符串(“Client_1”、“Client_2”和“Client_3”),服务器将它们连接起来并返回“Client_1, Client_2, Client_3”字符串。
  5. 双向流式示例 - 客户端发送 3 个请求(“Client_1”、“Client_2”和“Client_3”),服务器对每个客户端请求响应 4 次(所有操作同时发生 - 新的客户端请求可以在服务器响应旧请求时到达)。

 

代码概述说明

由于在其他示例中使用了相似或相同的代码,我将在此示例中提供 gRPC 服务器和客户端代码的非常详细的审查,而在未来的示例中,我将只强调与当前代码的不同之处。

源代码概述

此示例的源代码包含 4 个项目

  1. GrpcServerProcess - 启动和运行 Kestrel 服务器。
  2. GreeterImpl - 一个可重用库,包含 gRPC 服务器功能实现(GrpcServerProcess 依赖于它)。
  3. Protos - 包含在服务器(通过 GreeterImpl)和客户端之间共享的 Greeter.proto gRPC proto 文件。
  4. ConsoleTestClient - 用于针对服务器运行所有客户端 gRPC 测试的控制台客户端(它也依赖于 Protos 项目)。

 

项目依赖关系反映在下图中

Protos 项目包含 Greeter.proto 文件,该文件定义了 gRPC 方法的 gRPC proto

syntax = "proto3";

// specifies the C# namespace for C# generated code
option csharp_namespace = "simple";

service Greeter
{
	// client takes HelloRequest and returns HelloReply
	rpc SayHello (HelloRequest) returns (HelloReply);

	// streams multiple server replies to a single client request
	rpc ServerStreamHelloReplies (HelloRequest) returns (stream HelloReply);

	// streams multiple server replies to a single client request
	// (throwing a server exception in the middle)
	rpc ServerStreamHelloRepliesWithError (HelloRequest) returns (stream HelloReply);

	// streams multiple client request producing a single server reply
	// when the client stream is complete. 
	rpc ClientStreamHelloRequests(stream HelloRequest) returns (HelloReply);

	// streams multiple server replies for each of the streamed client requests.
	// The replies start streaming before the client stream is completed, 
	// providing simultaneous bi-directional communications. 
	rpc ClientAndServerStreamingTest(stream HelloRequest) returns (stream HelloReply);
}

// HelloRequest has only one string field - name
message HelloRequest
{
	string name = 1;
}

// HelloReply has only one string name - msg
message HelloReply
{
	string msg = 1;
}  

所有 Greeter 方法都将 HelloRequest 消息作为请求,并返回 HelloReply 消息作为回复。我可以使用内置的 string protobuf 类型,但想展示一个更复杂的示例,其中请求和回复类型在 proto 文件中定义。

请注意,在 HelloRequestHelloReply proto 中的字段旁边指定了一个整数,例如

string name = 1;  

此整数应唯一地定义一个 protobuf 字段,以便如果向类型添加新字段,它们应具有不同的整数 - 在这种情况下,新的扩展类型将向后兼容。

最有趣的服务器代码位于可重用 GreeterImpl 项目下的 GreeterImplementation.cs 文件中。

看看 GreeterImpl.csproj 文件。它展示了如何添加对不同项目包含的 proto 文件的引用

<ItemGroup>
	<Protobuf Include="..\Protos\Greeter.proto" GrpcServices="Server" />
    ...
</ItemGroup>  

我们只需提供文件的相对路径。

GrpcServices="Server" 表示将为 proto 方法生成服务器骨架。请注意,GreeterImpl 项目还引用了 Grpc.AspNetCore 包,该包包含生成服务器骨架所需的引用。

GreeterImpl 项目的 GreeterImplementation 类继承自生成的骨架 simple.Greeter.GreeterBase 类。它为所有 gRPC 服务器方法提供了覆盖实现,例如,这是最简单的 - 单请求/单回复 SayHello(...) 方法的覆盖

public override async Task<helloreply> SayHello(HelloRequest request, ServerCallContext context)
{
    string name = request.Name;

    return new HelloReply { Msg = $"Hello {name}!!!" };
}  
</helloreply>

GrpcServerProcess 是实际启动支持 GreeterImplementations 类中定义的 gRPC 方法实现的 Kestrel 服务器的项目。

本地服务器端口在其 appsettings.json 文件中定义

"Kestrel": {
  "Endpoints": {
    "Grpc": {
      "Url": "https://:55003",
      "Protocols": "Http2"
    }
  }
}  

其其余功能包含在 Program.cs 文件中

using GrpcServerProcess;

// create builder
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();

// add grpc service
builder.Services.AddGrpc();

// create the Kestrel application
var app = builder.Build();

// specifies GreeterImplementation as the gRPC service to run and 
// channel to it all the requests that specify 55003 port
app.MapGrpcService<GreeterImplementation>().RequireHost("*:55003");

// runs Kestrel server. 
app.Run();  

ConsoleTestClient 项目也引用 Greeter.proto 文件,但带有 GrpcServices="Client":这指示 Visual Studio 为 Greeter 方法生成客户端代理。

ConsoleTestClient 还需要提供对 Grpc.Net.Client、Google.Protobuf 和 Grpc.Tools 包的引用

<ItemGroup>
    <PackageReference Include="Grpc.Net.Client" Version="2.62.0" />
    <PackageReference Include="Google.Protobuf" Version="3.25.1" />
    <PackageReference Include="Grpc.Tools" Version="2.62.0">
        <PrivateAssets>all</PrivateAssets>
        <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <Protobuf Include="..\Protos\Greeter.proto" GrpcServices="Client" />
</ItemGroup>  

真正有趣的自定义代码是 gRPC 方法的服务器实现和客户端对这些方法的调用。服务器实现包含在 GreeterImplementations.cs 文件中(在可重用 GreeterImpl 项目下),客户端调用包含在 ConsoleTestClient 项目的 Program.cs 文件中。

在下面的后续部分中,我将逐一解释这些方法。

单请求/单回复 SayHello(...) 方法

这是最简单的实现和调用的方法(因为没有服务器端或客户端流)。

这是 Protos 项目下 Greeter.proto 文件中此方法的 protobuf 代码

service Greeter
{
	// client takes HelloRequest and returns HelloReply
	rpc SayHello (HelloRequest) returns (HelloReply);
    ...
}

这是客户端代码(来自 ConsoleTestClient/Program.cs 文件)

// get the channel connecting the client to the server
var channel =
    GrpcChannel.ForAddress("https://:55003");

// create the GreeterClient service
var greeterGrpcClient = new Greeter.GreeterClient(channel);

string greetingName = "Joe Doe";

Console.WriteLine($"Unary server call sample:");
// call SetHello RPC on the server asynchronously and wait for the reply.
var reply =
    await greeterGrpcClient.SayHelloAsync(new HelloRequest { Name = greetingName });

Console.WriteLine(reply.Msg);  

上面代码片段顶部的代码将 gRPC 客户端连接到“https://:55003”URL 的 grpc 服务器,并创建 greeterGrpcClient 对象以调用服务器方法。

服务器调用本身只占一行

// call SetHello RPC on the server asynchronously and wait for the reply.
var reply = await greeterGrpcClient.SayHelloAsync(new HelloRequest { Name = greetingName });  

我们创建 HelloRequest 对象,将其名称设置为“Joe Doe”,将其发送到服务器并等待回复。

reply.Msg 将包含“Hello Joe Doe!”问候字符串。

服务器实现(位于 GreeterImplementation.cs 文件中)也非常简单

public override async Task<helloreply> SayHello(HelloRequest request, ServerCallContext context)
{
    string name = request.Name;

    // return HelloReply with its message set to the greeting string
    return new HelloReply { Msg = $"Hello {name}!!!" };
}  
</helloreply>

客户端调用将导致以下消息打印到控制台

Hello Joe Doe!!!  

服务器流示例

有两个服务器流示例 - 一个是普通的 ServerStreamHelloReplies(...) 方法,另一个是服务器在将流式响应发送回客户端 ServerStreamHelloRepliesWithError(...) 期间抛出 RpcExeption 的方法。

这是这两个方法的 protobuf 代码

service Greeter
{
    ...
    // streams multiple server replies to a single client request
    rpc ServerStreamHelloReplies (HelloRequest) returns (stream HelloReply);

    // streams multiple server replies to a single client request
    // (throwing a server exception in the middle)
    rpc ServerStreamHelloRepliesWithError (HelloRequest) returns (stream HelloReply);        
    ...
} 

这是普通方法(无服务器错误)的客户端代码

// get the serverStreaming call containing an asynchronous stream
var serverStreamingCall = greeterGrpcClient.ServerStreamHelloReplies(new HelloRequest { Name = greetingName });

await foreach(var response in serverStreamingCall.ResponseStream.ReadAllAsync())
{
    // for each async response, print its Msg property
    Console.WriteLine(response.Msg);
} 

此客户端方法调用的结果将是打印到控制台的消息流

Hello Joe Doe 1
Hello Joe Doe 2
Hello Joe Doe 3
Hello Joe Doe 4
Hello Joe Doe 5
Hello Joe Doe 6
Hello Joe Doe 7
Hello Joe Doe 8
Hello Joe Doe 9
Hello Joe Doe 10
Hello Joe Doe 11
Hello Joe Doe 12
Hello Joe Doe 13
Hello Joe Doe 14
Hello Joe Doe 15
Hello Joe Doe 16
Hello Joe Doe 17
Hello Joe Doe 18
Hello Joe Doe 19
Hello Joe Doe 20  

带错误的服务器流的客户端代码非常相似,只是它包含在 try/catch 块中

// get the serverStreaming call containing an asynchronous stream
var serverStreamingCallWithError = greeterGrpcClient.ServerStreamHelloRepliesWithError(new HelloRequest { Name = greetingName });
try
{
    await foreach (var response in serverStreamingCallWithError.ResponseStream.ReadAllAsync())
    {
        // for each async response, print its Msg property
        Console.WriteLine(response.Msg);
    }
}
catch(RpcException exception)
{
    // prints the exception message
    Console.WriteLine(exception.Message);
}  

这是它打印到控制台的内容

Hello Joe Doe 1
Hello Joe Doe 2
Hello Joe Doe 3
Hello Joe Doe 4
Hello Joe Doe 5
Hello Joe Doe 6
Hello Joe Doe 7
Hello Joe Doe 8
Hello Joe Doe 9
Hello Joe Doe 10
Hello Joe Doe 11
Status(StatusCode="Internal", Detail="Error Status Detail (for the client)")  

服务器在第 11 次迭代后抛出错误,发送给客户端的错误消息是“Error Status Detail (for the client)”(服务器可以向其日志写入另一个更详细的错误消息)。

现在让我们看看 GreeterImplementation.cs 文件中服务器流方法的服务器实现

public override async Task ServerStreamHelloReplies
(
    HelloRequest request,
    IServerStreamWriter<HelloReply> responseStream,
    ServerCallContext context)
{
    await ServerStreamHelloRepliesImpl(request, false, responseStream, context);
}


public override async Task ServerStreamHelloRepliesWithError
(
    HelloRequest request,
    IServerStreamWriter<HelloReply> responseStream,
    ServerCallContext context)
{
    await ServerStreamHelloRepliesImpl(request, true, responseStream, context);
}  

两种方法都调用 ServerStreamHelloRepliesImp(...) 方法,一个传递参数 throwException 设置为 false,另一个设置为 true

private async Task ServerStreamHelloRepliesImpl
(
    HelloRequest request,
    bool throwException,
    IServerStreamWriter<HelloReply> responseStream,
    ServerCallContext context)
{
    // get the name from the client request
    string name = request.Name;

    for (int i = 0; i < 20; i++)
    {
        await Task.Delay(200); // delay by 0.2 of a second

        // write the reply asynchronously
        await responseStream.WriteAsync(new HelloReply { Msg = $"Hello {name} {i + 1}" });

        // cancel stream if cancellation is requested
        context.CancellationToken.ThrowIfCancellationRequested();

        // throw the RpcException (propagated to the client) 
        // after the 11th iteration if throwException argument is passed as true
        if (i == 10 && throwException)
        {
            // sets the status code and Error messages for the client and the server. 
            // the status code and the error message for the client will be sent over to the client,
            // while the error message for the server can be logged or acted upon in various ways 
            // on the server.
            throw new RpcException(new Status(StatusCode.Internal, "Error Status Detail (for the client)"), "ERROR: Cannot Continue with streaming (server error)!");
        }
    }
}  

请注意,该行

// cancel stream if cancellation is requested
context.CancellationToken.ThrowIfCancellationRequested();  

用于从客户端取消服务器流。客户端取消将在我们讨论 Web 客户端时演示。

客户端流示例

下一个示例(方法 Greeter.ClientStreamHelloRequests(...))演示了从客户端异步流式传输,在服务器上积累结果,并在客户端流式传输结束后向客户端返回单个值结果。

这是此方法的 protobuf 声明

service Greeter
{
    ...
    // streams multiple client request producing a single server reply
    // when the client stream is complete. 
    rpc ClientStreamHelloRequests(stream HelloRequest) returns (HelloReply);
    ...
}  

这是客户端代码

var clientSreamingCall = greeterGrpcClient.ClientStreamHelloRequests();

for(int i = 0; i < 3;  i++)
{
    // stream requests from the client to server
    await clientSreamingCall.RequestStream.WriteAsync(new HelloRequest { Name = $"Client_{i + 1}" });
}

// inform the server that the client streaming ended
await clientSreamingCall.RequestStream.CompleteAsync();

// get the resulting HelloReply from the server
var clientStreamingResponse = await clientSreamingCall;

// print the resulting message
Console.WriteLine(clientStreamingResponse.Msg);  

服务器返回字符串“Hello ”,后跟来自客户端消息的逗号分隔连接

Hello Client_1, Client_2, Client_3  

这是服务器代码

public override async Task<HelloReply> ClientStreamHelloRequests(IAsyncStreamReader<HelloRequest> requestStream, ServerCallContext context)
{
    string message = "Hello ";

    bool first = true;

    // for each message from the client (read asynchronously)
    await foreach (var inputMessage in requestStream.ReadAllAsync())
    {
        if (!first)
        {
            // if not the first message prepend it with ", " string
            message += ", ";
        }

        // add the Name from the message
        message += inputMessage.Name;
        first = false;
    }

    // after streaming ended return the HelloReply the corresponding Msg property
    return new HelloReply { Msg = message };
}  

请注意,服务器在消息流式传输时接收每个客户端消息,而无需等待客户端流式传输结束。您可以通过在调试器中运行服务器并在 await foreach(...) 循环中设置断点来观察它。

双向流示例

最后我们来到了最有趣和最复杂的示例 - 一个演示同时流式传输客户端请求和服务器回复的示例:服务器不等待客户端完成其流式传输,它在每个客户端请求到达后开始回复。

protobuf 方法称为 Greeter.ClientAndServerStreamingTest(...)

service Greeter
{
    ...
    // streams multiple server replies for each of the streamed client requests.
    // The replies start streaming before the client stream is completed, 
    // providing simultaneous bi-directional communications. 
    rpc ClientAndServerStreamingTest(stream HelloRequest) returns (stream HelloReply);
}    

请注意,请求和回复都进行了流式传输。

这是调用双向流功能的客户端实现

var clientServerStreamingCall = greeterGrpcClient.ClientAndServerStreamingTest();

// the task to be run to get and print the streamed responses to the server
var readTask = Task.Run(async () =>
{
    await foreach (var reply in clientServerStreamingCall.ResponseStream.ReadAllAsync())
    {
        // for every server reply, simply print the message
        Console.WriteLine(reply.Msg);
    }
});

// stream 3 requests to the server. 
for (int i = 0; i < 3; i++)
{
    // write the request into the request stream.
    await clientServerStreamingCall.RequestStream.WriteAsync(new HelloRequest { Name = $"Client_{i + 1}" });

    // delay by one second - it is important to 
    // make sure that the server responds immediately after receiving the first
    // client request and does not wait for the client stream to complete
    await Task.Delay(1000);
}

await Task.WhenAll(readTask, clientServerStreamingCall.RequestStream.CompleteAsync());  

顶部定义的 readTask 将用于逐个获取流式回复。

这是客户端打印到控制台的内容

Hello Client_1_1
Hello Client_1_2
Hello Client_1_3
Hello Client_1_4
Hello Client_2_1
Hello Client_2_2
Hello Client_2_3
Hello Client_2_4
Hello Client_3_1
Hello Client_3_2
Hello Client_3_3
Hello Client_3_4

对于来自客户端的每个字符串,服务器会返回 4 个回复,例如,对于 Client_1,回复将是 Client_1_1、Client_1_2、Client_1_3 和 Client_1_4。

请注意,流式传输客户端消息到服务器的时间间隔相当长 - 1 秒,但服务器回复几乎没有延迟。这是因为服务器在收到第一个客户端消息后开始回复,并继续回复,而无需等待客户端流式传输完成。

这就是真正的双向流式传输,不幸的是,它无法从浏览器中工作,如下所述。

基于浏览器的代码示例概述

其余的 ASP.NET gRPC 代码示例将基于浏览器。一个示例演示 HTML/JavaScript 使用 gRPC 与服务器交换信息,另一个演示 Avalonia WebAssembly。

这些示例的服务器 gRPC 代码将与之前的示例完全相同 - 位于 GreeterImpl 项目下的 GreeterImplementation.cs 文件中。因此,我们将主要关注客户端代码和 ASP.NET 特定的服务器代码。

HTML/JavaScript ASP.NET gRPC 示例

运行示例

启动解决方案过滤器 AspGrpcServerWithRazorClient.slnf。将 AspGrpcServerWithRazorClient 项目设为启动项目。构建项目并在调试器下运行它。

这是您将看到的页面

您可以尝试更改名称(或将其设置为“Nick”),然后单击“Get Single Greeting”按钮,您将在浏览器中看到打印的“Hello Nick!!!”

现在尝试点击“Get Multiple Greetings”按钮。您将看到从服务器流式传输的多个问候语,从“Hello Nick 1”到“Hello Nick 20”,最后将打印“STREAMING ENDED”(如果您让它运行到最后)

您还可以随时按“Cancel Streaming”按钮取消服务器流。

按下“Get Multiple Greetings with ERROR”按钮将调用服务器上的方法,该方法将在第 11 次迭代后抛出 RpcException。这是显示结果错误的方式

创建用于托管示例的 ASP.NET 项目

我通过选择“ASP.NET Core Web App (Razor Pages)”项目类型创建了 ASP.NET 项目 AspGrpcServerWithRazorClient.csproj

然后我修改了它的 Program.cs、Index.cshtml 和 _Layout.cshtml,如下文详细说明。

从 Protobuf 代码生成 JavaScript 客户端代理

作为以下所有内容的先决条件,请安装 nodeJS 和 npm,例如,通过访问 Node JS 安装,下载 node .msi 安装程序并在您的 Windows 机器上运行它。

为了构建服务器,您需要

  1. 安装 protoc protobuffer 编译器,例如从 Protoc 编译器下载,通过下载和解压缩 protoc-26.1-win64.zip(或根据您的机器选择其他文件)。确保 protoc.exe 在您的路径中。
  2. 使用 protoc 编译器从 Greeter.proto protobuf 文件生成 JavaScript 客户端代理。Protoc 将生成 nodeJS JavaScript 代理。这是需要从 Protos 文件夹(Protos 项目的)内的命令行运行的命令,以生成客户端代理
    protoc -I=. Greeter.proto --js_out=import_style=commonjs:..\AspGrpcServerWithRazorClient\wwwroot\dist --grpc-web_out=import_style=commonjs,mode=grpcwebtext:..\AspGrpcServerWithRazorClient\wwwroot\dist
          
    此行也包含在同一文件夹中的 README.txt 文件中。
  3. 文件 Greeter_grpc_web_pb.js 和 Greeter_pb.js 将在 AspGrpcServerWithRazorClient\wwwroot\dist 文件夹下创建。

    这些文件是 nodeJS 模块,不适合从浏览器运行。
  4. 为了将客户端代理转换为 ES 模块(可以在浏览器中运行),我使用“WebPack 任务运行器”VS2022 扩展

    您也需要安装此扩展。
  5. 有一个小的 JavaScript 文件 AspGrpcServerWithRazorClient/wwwroot/dist/client.js,它引用了 JavaScript 客户端从生成的 Greeter_..._pb.js 文件中所需的所有功能
            const { HelloRequest, HelloReply } = require('./Greeter_pb.js');
            const { GreeterClient } = require('./Greeter_grpc_web_pb.js');
            
            global.HelloRequest = HelloRequest;
            global.HelloReply = HelloReply;
            global.GreeterClient = GreeterClient;
          
    此文件由 webpack 用于生成可供浏览器客户端使用的 main.js 文件。
  6. 为了使用 webpack 文件生成,我们需要在项目级别创建 webpack.config.js 文件。这是它的内容
            const path = require('path');
    
            module.exports = {
                mode: 'development',
                entry: './wwwroot/dist/client.js',
                output: {
                    path: path.resolve(__dirname, 'wwwroot/dist'),
                    filename: 'main.js',
                },
            };
          
    它将指示 webpack 获取 client.js 及其所依赖的所有文件,并根据它们创建 main.js 文件(位于相同的 dist 文件夹下),该文件可供 JavaScript 浏览器客户端使用。
  7. 要进行转换 - 右键单击解决方案资源管理器中的 webpack.config.js 文件,然后选择“任务运行器资源管理器”

    它应该打开任务运行器资源管理器实用程序。单击“运行开发”或“运行生产”并选择“运行”菜单选项

    它将生成可在浏览器 JavaScript 代码中使用的文件 main.js。

 

HTML/JavaScript 客户端代码概述

使用上一小节中生成的代理的 HTML/JavaScript 客户端代码位于 AspGrpcServerWithRazorClient/Pages/Index.cshtml 文件中。该文件几乎全是 HTML/JavaScript,没有 ASP 代码生成。

文件的 HTML 部分定义了标签、按钮和 div(作为添加文本的占位符)

<h1>ASP gRPC Samples</h1>
<label>Enter Name:</label>
<input type="text" id="TheName" value="Nick">
<h2>Non-Streaming (Single Value) gRPC Sample:</h2>
<button type="button" id="GetSingleGreetingButton">Get Single Greeting</button><br /><br />
<div id="TheSingleValueResponse" style="font-weight:bold;font-size:20px;font-style:italic"></div>

<h2>Server Streaming gRPC Sample (multiple greetings from the Server):</h2>
<button type="button" id="GetMultipleGreetingsButton">Get Multiple Greeting</button>
<button type="button" id="GetMultipleGreetingsWithErrorButton">Get Multiple Greeting with ERROR</button>
<button type="button" id="CancelStreamingButton">Cancel Streaming</button>
<br /><br />
<div id="TheStreamingResponse" style="font-weight:bold;font-size:20px;font-style:italic"></div>
<label id="TheErrorLabel" style="visibility:collapse">Streaming Error:</label>
<div id="TheStreamingError"></div>
<div id="TheStreamingEnded"></div>  

然后添加了两个模块 -

  1. 上一小节中生成的 main.js 文件。 main.js 模块用于获取客户端代理代码)。
  2. jquery.min.js - 用于查找 HTML 树节点并修改它们。

 

<script src="./dist/main.js"></script>
<script src="./dist/jquery.min.js"></script>  

最后,在 <script type="text/javascript"> HTML 标签中包含 JavaScript 客户端代码。

创建 gRPC Greeter 客户端的客户端代码

// get url of the current ASP website
var location = window.location;
var url = location.origin;

// use this url to create the client. 
var greeterServiceClient = new GreeterClient(url);  

创建客户端 greeterServiceClient 后,我们将其用于 gRPC 服务调用。

从 JavaScript 客户端调用单个请求/单个回复 SayHello(...) 服务

我们如何从 JavaScript 客户端调用 SayHello(...) 服务

// create request object
var request = new HelloRequest();

// Read the name from TheName TextBox
var name = $("#TheName").val();

// set the name parameter of the request object
request.setName(name);

// send hello request and set up a callback to be fired 
// once the response is obtained.
greeterServiceClient.sayHello(request, {}, (err, response) => {
    // this is the body of sayHello service callback 
    // fired on response coming from gRPC server

    // get the message from the response, assign 
    // it as text to "#TheSingleValueResponse" div area
    var msg = response.getMsg();
    $("#TheSingleValueResponse").text(msg);
});  

这段代码本身是使用 jQuery 分配给“#GetSingleGreetingButton”点击的回调

$("#GetSingleGreetingButton").on(
    "click",
    () => {
        ...
    });

从客户端调用流式 gRPC 服务

对于服务器流调用,我们定义一个全局变量 var stream = null;,然后用它来调用流式服务并在需要时取消它们。

为了代码重用,有一个 JavaScript getStreamedGreetings(greeterServiceClient, throwError) 方法,其第二个参数是一个布尔标志 - 应设置为 true 以调用在第 11 次迭代后抛出错误的 gRPC 流式服务,设置为 false 以允许流式服务一直运行到最后。

以下是将流式服务调用回调分配给相应按钮的方式

// No error coming from the server
$("#GetMultipleGreetingsButton").on(
    "click",
    () => getStreamedGreetings(greeterServiceClient, false)
);

// Server error after 11th iteration
$("#GetMultipleGreetingsWithErrorButton").on(
    "click",
    () => getStreamedGreetings(greeterServiceClient, true)
);  

这是 getStreamedGreetings(...) 方法的实现(带详细注释)

getStreamedGreetings = (greeterServiceClient, throwError) => {

    // reset current STREAMING ENDED message text to empty
    $("#TheStreamingEnded").text('');
        
    // make the current error label invisible
    $("#TheErrorLabel").css("visility", "collapse");

    // reset the streaming error message to emtpy
    $("#TheStreamingError").text('');

    // create a HelloRequest object
    var request = new HelloRequest();

    // get the game from TheName TextBox
    var name = $("#TheName").val();

    // set the Name parameter of the request
    request.setName(name);

    // calling the streaming server methods returns the client handle to 
    // the stream (which we assign to stream global variable)
    if (!throwError) {
        // call the service that does not throw an Exception
        stream = greeterServiceClient.serverStreamHelloReplies(request, {});
    }
    else {
        // call the service that does throws RpcException after 11th iteration
        stream = greeterServiceClient.serverStreamHelloRepliesWithError(request, {});
    }

    // On stream message arriving, assign the text received contained with that last message
    // to the the corresponding text area (div) element
    stream.on('data', (response) => {
        var msg = response.getMsg();
        $("#TheStreamingResponse").text(msg);
    });

    // do something on server status changing
    stream.on('status', (status) => {

    });

    // assing error text on server error
    stream.on('error', (err) => {
        $("#TheErrorLabel").css("visility", "visible");
        $("#TheStreamingError").text(err);
        stream = null;
    });

    // signify that the server streaming ended
    stream.on('end', () => {
        $("#TheStreamingEnded").text("STREAMING ENDED");
        stream = null;
    })  

取消服务器流

要取消服务器流,只需调用 stream.cancel("Cancellation Message")

用于构建和启动 ASP.NET 服务器的 ASP.NET 代码

此代码位于 AspGrpcServerWithRazorClient 项目下文档齐全的 Program.cs 文件中

using GrpcServerProcess;

// create ASP.NET application builder.
var builder = WebApplication.CreateBuilder(args);

// Add a service generating razor pages
builder.Services.AddRazorPages();

// add a service for grpc
builder.Services.AddGrpc();

// build the application
var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

// use default file (Index.cshtml) when no path is specified after 
// server:port combination
app.UseDefaultFiles();

// allow using static files (e.g. .js, wasm etc)
app.UseStaticFiles();

// use grpc-web 
app.UseGrpcWeb();

// allow razor pages generation
app.MapRazorPages();

// create the GreeterImplementation service and allow it to be accessed from grpc-web
app.MapGrpcService<GreeterImplementation>().EnableGrpcWeb();

// start the ASP.NET server
app.Run();  

ASP.NET Avalonia/WebAssembly gRPC 代码示例

运行示例

要运行示例,请打开 AspGrpcWithAvaloniaClients.slnf 解决方案筛选器文件,将 AspGrpcServer 设为启动项目,然后重新生成并启动它。

这是您将看到的屏幕(一两秒后)

上面两个示例(请求/回复和服务器示例)的行为将与上一节中的几乎完全相同

下面两个示例对应于客户端流和双向客户端-服务器流,目的是显示它们在浏览器中无法正常工作。

确实,如果您按下“Test Streaming Client”或“Test Streaming Client and Server”按钮,服务器将获取消息并仅在客户端流完成时开始响应(客户端流示例为 5 秒,双向示例为 3 秒)。这实际上意味着客户端流不起作用(即使 C# 为这些 protobuf 方法生成了客户端代理)。

创建客户端 C# Avalonia 项目

要创建 Avalonia WebAssembly 项目,我使用 创建 Avalonia Web Assembly 项目 中的说明

  1. 我通过运行以下命令安装 wasm-tools(或确保它们已安装)
    dotnet workload install wasm-tools        
          
    从命令行。
  2. 我通过运行以下命令更新到最新的 avalonia dotnet 模板
    dotnet new install avalonia.templates        
          
  3. 我为项目创建了一个文件夹(在我的情况下它称为 AvaGrpcClient),并使用命令行进入该文件夹。
  4. 在该文件夹中,我从命令行运行
    dotnet new avalonia.xplat        
          
  5. 这将创建共享项目 AvaGrpcClient(在同名文件夹中)和许多特定于平台的项目。
  6. 我删除了大多数特定于平台的项目,只留下 AvaGrpcClient.Browser(用于构建 Avalonia WebAssembly 包)和 AvaGrpcClient.Display(如果需要,用于调试和更快的原型设计)。

 

客户端 Avalonia 代码

为了让项目 AvaGrpcClient 生成 gRPC 客户端代理,我将对 protobuf 文件的引用添加到 AvaGrpcClient.csproj 文件中,并将 GrpcServices 标志设置为 Client

  <Protobuf Include="..\..\Protos\Greeter.proto" GrpcServices="Client" />

我还添加了对 Grpc 所需包的引用,包括 Grpc.Net.Client.Web 包,该包专门用于在浏览器中运行 grpc

<ItemGroup>
    <PackageReference Include="Grpc.Net.Client" Version="2.62.0" />
    <PackageReference Include="Google.Protobuf" Version="3.25.1" />
    <PackageReference Include="Grpc.Net.Client.Web" Version="2.62.0" />
    <PackageReference Include="Grpc.Tools" Version="2.62.0">
        <PrivateAssets>all</PrivateAssets>
        <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    ...
</ItemGroup>  

几乎所有特定于示例的客户端代码都位于 MainView.xaml.cs 和 MainView.xaml 文件中。 MainView.xaml 指定了视觉布局,而所有与服务器交互的代码都在 MainView.xaml.cs 文件中。

与客户端的连接在 MainView 类构造函数中建立

var channel =
    GrpcChannel.ForAddress
    (
        CommonData.Url!, // server address
        new GrpcChannelOptions
        {
            // indicates the browser grpc connection
            HttpHandler = new GrpcWebHandler(new HttpClientHandler()) 
        });

// create the GreeterClient service
_greeterGrpcClient = new Greeter.GreeterClient(channel);  

...
 _serverStreamCancellationTokenSource = new CancellationTokenSource();

请注意 CommonData.Url 静态字符串属性。它应包含用于连接到 Grpc 调用的 URL(在我的情况下与 ASP.NET 服务器 URL 相同)。

CommonData.Url 属性设置为传递给 AvaGrpcClient.Browser 解决方案(实际创建 WebAssembly 浏览器代码作为许多 .wasm 文件的解决方案)中包含的 Program.Main(...) 方法的第一个参数

internal sealed partial class Program
{
    private static async Task Main(string[] args)
    {
        // note we are assigning the first argument to CommonData.Url
        CommonData.Url = args[0];

        // replace the div with id "out" by the MainView object instance
        await BuildAvaloniaApp()
            .WithInterFont()
            .UseReactiveUI()
            .StartBrowserAppAsync("out");
    }

    public static AppBuilder BuildAvaloniaApp()
        => AppBuilder.Configure<App>();
}  

还要注意调用以下代码

// replace the div with id "out" by the MainView object instance
await BuildAvaloniaApp()
    .WithInterFont()
    .UseReactiveUI()
    .StartBrowserAppAsync("out");  

本质上用 Avalonia 浏览器应用程序(在我们的例子中是 MainView 对象实例)替换了 ID 为“out”的 HTML 元素。

当按下相应按钮时触发的回调在 MainView.xaml.cs 文件中定义为方法。

这是从客户端调用单请求/单响应(一元)服务 SayHello 的代码

private async void TestUnaryHelloButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
    // call simple single request/single response SayHello service
    var reply =
        await _greeterGrpcClient.SayHelloAsync(new HelloRequest { Name = GreetingName });

    // display the result
    HelloResultText.Text = reply.Msg;
}  

其中 GreetingName 属性是 MainView.xaml 文件中定义的 TextBoxText

private string GreetingName => NameToEnter.Text ?? string.Empty;  

这是用于测试服务器流的文档化客户端代码

// test server streaming
private async void TestStreamingServerButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
    // set initial values to empty strings
    StreamingServerResultsText.Text = string.Empty;
    StreamingErrorText.Text = string.Empty;
    try
    {
        // get the server stream container
        var serverStreamingResponsesContainer = 
            _greeterGrpcClient.ServerStreamHelloReplies(new HelloRequest { Name = GreetingName });
            
        // foreach of the async responses from the server
        await foreach (var response in serverStreamingResponsesContainer.ResponseStream.ReadAllAsync(_serverStreamCancellationTokenSource.Token))
        {
            // change the text of the TextBox
            StreamingServerResultsText.Text = response.Msg;
        }
    }
    catch(RpcException exception)
    {
        // if an exception is throws, show the exception message
        StreamingErrorText.Text = $"ERROR: {exception.Message}";
    }
}  

用于测试带错误的服务器流的代码完全相同,只是调用了不同的服务 ServerStreamHelloRepliesWithError(...)

// foreach of the async responses from the server
var serverStreamingResponsesContainer = _greeterGrpcClient.ServerStreamHelloRepliesWithError(new HelloRequest { Name = GreetingName });  

以下方法从客户端取消服务器流

private void TestStreamingServerCancelButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
    // send signal to the server to cancel streaming 
    _serverStreamCancellationTokenSource?.Cancel();

    // change the streaming token
    _serverStreamCancellationTokenSource = new CancellationTokenSource();
}  

下一个方法演示了使用客户端流 API。由于我们在 C# 中工作,客户端流 API 已生成并可以使用 - 但不幸的是,它无法从浏览器进行流式传输。它会等到客户端流完成,然后将所有累积的消息一起发送

// test streaming client to the server
// unfortunately the client in a browser does not stream,
// it accumulates all the messages on the browser, and
// sends them together after the client indicates the end of streaming
private async void TestStreamingClientButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
    // reset the UI text to emtpy
    StreamingClientResultsText.Text = string.Empty;

    // create stream container
    var clientStreamContainer = _greeterGrpcClient.ClientStreamHelloRequests();

    for (int i = 0; i < 5; i++)
    {
        // push the messages into the request streams
        await clientStreamContainer.RequestStream.WriteAsync(new HelloRequest { Name = $"Client_{i + 1}" });

        await Task.Delay(1000);
    }

    // indicate the completion of the client streaming
    // Unfortunately it is only at this point that all the client messages
    // will be sent to the server. Essentially that means that there is 
    // no client streaming
    await clientStreamContainer.RequestStream.CompleteAsync();

    // get the server response
    var clientStreamingResponse = await clientStreamContainer;

    // set the visual text of the server response
    StreamingClientResultsText.Text = clientStreamingResponse.Msg;
}

这是演示双向(客户端和服务器)流 API 的代码,它也存在同样的问题(浏览器不支持客户端流 - 浏览器会累积客户端消息,并在客户端关闭“流”时将它们全部一起发送)

// Bi-Directional (Client and Server) streaming test. 
// Unfortunately the client messages are accumulated on the client side
// and sent to the server together only after
// the client indicates the end of streaming
// Which essentially means that there is no client streaming
private async void TestStreamingClientServerButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
    // create server and client streamd container
    var clientServerStreamContainer = _greeterGrpcClient.ClientAndServerStreamingTest();

    // reset the server reply text to empty
    StreamingClientServerResultsText.Text = string.Empty;

    // create an async task to asynchronously process the server responses 
    // as they come
    var readTask = Task.Run(async () =>
    {
        await foreach (var reply in clientServerStreamContainer.ResponseStream.ReadAllAsync())
        {
            // for each server response we assing it to show in the client browser
            await Dispatcher.UIThread.InvokeAsync(() => { StreamingClientServerResultsText.Text += reply.Msg + "\n"; });
        }
    });

    // push 3 client requests into the stream
    // unfortunately they'll accumulated on the client and sent together 
    // only after the client call RequestStream.CompleteAsync() method
    for (int i = 0; i < 3; i++)
    {
        await clientServerStreamContainer.RequestStream.WriteAsync(new HelloRequest { Name = $"Client_{i + 1}" });

        await Task.Delay(1000);
    }

    // wait for both the client and the server processing to finish.
    await Task.WhenAll(readTask, clientServerStreamContainer.RequestStream.CompleteAsync());
}  

ASP.NET 服务器代码

服务器 ASP.NET 代码位于 AspGrpcServer 项目中。

关于该项目有两点需要了解

  1. 我通过右键单击解决方案并选择“项目依赖项”菜单项,然后选择主项目 AspGrpcServer 并使其依赖于 AvaGrpcClient.Browser,从而使其依赖于 AvaGrpcClient.Browser 项目。

    这确保了每次重建服务器时,AvaGrpcClient.Browser 项目都会在此之前构建,并且由于 AvaGrpcClient.Browser 项目依赖于 AvaGrpcClient,因此 AvaGrpcClient 项目将在此之前构建。
  2. AspGrpcServer 项目中定义了一个构建后事件(请参阅 AspGrpcServer.csproj 文件的底部)。此事件将 AvaGrpcClient.Browser 构建创建的 AppBundle/_framework 文件夹复制到 AspGrpcServer/wwwroot/_framework 文件夹。 注意 - 此文件夹(_framework)应被版本控制工具忽略。

 

我修改了 Shared/_Layout.cshtml 代码使其更简单,我还修改了 Index.cshtml 文件以包含一个 <div id="out">,它将被 Avalonia WebAssembly MainView 对象实例替换(如上所述)。

这是 Index.cshtml 的代码

<body style="margin: 0px; overflow: hidden">
    <!-- out object to be replaced by the Avalonia C# view -->
    <div id="out">
        <div id="avalonia-splash">
            <div class="center">
                <h2 class="purple">
                    Powered by
                    <a class="highlight" href="https://www.avaloniaui.net/" target="_blank">Avalonia UI</a>
                </h2>
            </div>
            <img class="icon" src="~/Logo.svg" alt="Avalonia Logo" />
        </div>
    </div>
    <!-- load the mainForAvalonia.js module to trigger replacing 
       div with id="out" with Avalonia MainView object instance
    -->
    <script type='module' src="~/mainForAvalonia.js"></script>
</body>

在上面代码的最后一行 - 请注意我们正在加载 mainForAvalonia.js 模块。此模块/文件包含实际创建 Wasm 并在浏览器中运行 wasm 项目的 JavaScript 代码。

这是 mainForAvalonia.js 文件中包含的 JavaScript 代码

import { dotnet } from './_framework/dotnet.js'

const is_browser = typeof window != "undefined";
if (!is_browser) throw new Error(`Expected to be running in a browser`);

const { getConfig, runMain } = await dotnet
    .withDiagnosticTracing(false)
    .withApplicationArgumentsFromQuery()
    .create();

const config = getConfig();

// the first argument is the main assembly name
// the second argument is a string array of args to be passed to the
// C# Program.Main(string[] args)
// we pass to it only one argument containing the ASP.NET server URL
// The same argument is set to CommonData.Url static C# property and 
// used the address to create the gRPC channel in C#
await runMain(config.mainAssemblyName, [window.location.origin]);  

请注意最后一行 - 这是我们将当前服务器 URL 作为创建 gRPC 客户端通信通道的 URL 传递给服务器的地方。

现在看看服务器的启动文件 AspGrpcServer/Program.cs。与上一个(HTML/JavaScript)示例中的类似文件相比,差异非常小。唯一的区别是我们需要添加 MIME 类型以允许将 .wasm 和一些其他文件返回给客户端。这是实现方式

// add the content types that might be needed for 
// sending .wasm files back to the client. 
var contentTypeProvider = new FileExtensionContentTypeProvider();
var dict = new Dictionary<string, string>
    {
        {".pdb" , "application/octet-stream" },
        {".blat", "application/octet-stream" },
        {".bin", "application/octet-stream" },
        {".dll" , "application/octet-stream" },
        {".dat" , "application/octet-stream" },
        {".json", "application/json" },
        {".wasm", "application/wasm" },
        {".symbols", "application/octet-stream" }
    };
foreach (var kvp in dict)
{
    contentTypeProvider.Mappings[kvp.Key] = kvp.Value;
}
app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = contentTypeProvider });

我们构建一个 FileExtensionContentTypeProvider 对象,将所需的新的 MIME 类型插入其中,然后将其作为 StaticFileOptions 对象的 ContentTypeProvider 属性传递给 app.UseStaticFiles(...) 方法。 

结论

这是一篇我提供了简单示例和详细解释所有 gRPC 通信用例的文章,包括各种客户端(包括 C# Console、HTML/JavaScritp 和通过 WebAssembly 客户端的 C# 浏览器)和支持 gRPC 的 ASP.NET 服务器之间的通信。

© . All rights reserved.