WPF/WinForms 客户端通过 gRPC 与后端通信:从 Proto 契约到流式调用的完整指南

WPF/WinForms 客户端通过 gRPC 与后端通信:从 Proto 契约到流式调用的完整指南

引言

做过桌面客户端的同学,以前和后端通信基本都是 HTTP + JSON(REST)。但当接口变多、字段变多、对性能有要求时,REST 的痛点就来了:字段冗余、没有强类型契约、序列化开销大。

gRPC 解决了这些问题:用 Protobuf 做序列化(体积小、快),用 .proto 文件做强类型契约(前后端字段不会对不上),还天然支持双向流。

本文面向 WPF/WinForms 开发者,讲清客户端用 gRPC 通信需要掌握的全部知识点。

环境:.NET 6/8,NuGet 包 Grpc.Net.Client + Google.Protobuf + Grpc.Tools

一、什么是 gRPC 和 Proto 契约

gRPC 是 Google 开源的 RPC 框架,基于 HTTP/2,默认用 Protobuf 做序列化。对比 REST:

维度 REST (HTTP + JSON) gRPC
契约 无强类型,靠文档约定 .proto 强类型契约
序列化 JSON 文本,体积大 Protobuf 二进制,体积小、快
通信模式 基本是一问一答 支持单向、服务端流、客户端流、双向流
传输 HTTP/1.1 HTTP/2(多路复用)

核心是 .proto 文件------它同时定义了"有哪些服务"和"消息长什么样":

proto 复制代码
syntax = "proto3";

package greet;

// 定义服务
service Greeter {
  // 一元调用
  rpc SayHello (HelloRequest) returns (HelloReply);
  // 服务端流式
  rpc SayHelloStream (HelloRequest) returns (stream HelloReply);
}

// 定义消息
message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

二、生成强类型客户端

.proto 文件要通过 Grpc.Tools 在编译时自动生成 C# 客户端代码。

2.1 引入包与配置

在客户端项目的 .csproj 里:

xml 复制代码
<ItemGroup>
  <PackageReference Include="Grpc.Net.Client" Version="2.*" />
  <PackageReference Include="Google.Protobuf" Version="3.*" />
  <PackageReference Include="Grpc.Tools" Version="2.*" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
  <!-- 指定 proto 文件,GrpcServices=Client 表示生成客户端 -->
  <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
</ItemGroup>

编译后,会生成一个 Greeter.GreeterClient 类,直接 new 就能用。

关键点:proto 文件是前后端共享的契约源,最好放在公共仓库/子模块里,由后端维护,客户端引用同一份,避免字段对不上。

三、建立连接:GrpcChannel

3.1 创建 Channel 和 Client

csharp 复制代码
using Grpc.Net.Client;
using Grpc.Core;

// 创建通道(Channel 是重资源,要复用,不要每次调用都新建)
using var channel = GrpcChannel.ForAddress("https://localhost:5001");

// 创建客户端
var client = new Greeter.GreeterClient(channel);

// 一元调用
var reply = await client.SayHelloAsync(
    new HelloRequest { Name = "张三" });
Console.WriteLine(reply.Message);

3.2 Channel 的生命周期(重要!)

GrpcChannel 底层是一个 HttpClient连接复用是 gRPC 性能的关键:

  1. 不要每次调用都 new GrpcChannel,否则连接池失效、性能骤降。
  2. 在应用生命周期内全局复用一个 Channel,用单例或依赖注入管理。
  3. 程序退出前正确 Dispose

用 DI 管理最优雅(需引用 Grpc.Net.ClientFactory):

csharp 复制代码
// 程序启动时注册(.NET 6+ 的 Host 里)
builder.Services.AddGrpcClient<Greeter.GreeterClient>(o =>
{
    o.Address = new Uri("https://localhost:5001");
});

之后在窗体里直接注入 Greeter.GreeterClient 使用即可。

四、四种通信模式

gRPC 支持四种调用模式,这是它比 REST 强的地方。

4.1 一元调用(Unary):一问一答

最常见的场景,上面已演示。

4.2 服务端流(Server Streaming):一问多答

适合"实时推送":比如日志流、行情推送、大文件分块下载。

csharp 复制代码
using var call = client.SayHelloStream(new HelloRequest { Name = "张三" });

// 逐条读取服务端推送的消息
await foreach (var reply in call.ResponseStream.ReadAllAsync())
{
    Console.WriteLine(reply.Message);
    // 想中途取消,可以 break
}

4.3 客户端流(Client Streaming):多问一答

适合"批量上传":比如批量导入数据、上传文件。

csharp 复制代码
using var call = client.Upload();

// 逐条写入
foreach (var item in dataList)
{
    await call.RequestStream.WriteAsync(item);
}

// 通知服务端"发送完毕",然后接收最终响应
await call.RequestStream.CompleteAsync();
var response = await call.ResponseAsync;

4.4 双向流(Bidirectional Streaming):边发边收

适合聊天、实时协作等复杂交互,两端都可随时发和收。

csharp 复制代码
using var call = client.Chat();

// 一个任务负责收
var readTask = Task.Run(async () =>
{
    await foreach (var msg in call.ResponseStream.ReadAllAsync())
        Console.WriteLine("收到: " + msg.Text);
});

// 一个任务负责发
foreach (var text in messagesToSend)
{
    await call.RequestStream.WriteAsync(new ChatMsg { Text = text });
}
await call.RequestStream.CompleteAsync();

await readTask;

五、认证、超时与取消

生产环境三件套,缺一不可。

5.1 认证:传 Token

Metadata 携带认证信息(如 JWT):

csharp 复制代码
var headers = new Metadata
{
    { "Authorization", "Bearer " + accessToken }
};

var reply = await client.SayHelloAsync(
    new HelloRequest { Name = "张三" }, headers);

5.2 超时与取消:Deadline + CancellationToken

gRPC 默认没有超时,网络抖动会让调用一直挂起,必须设置超时:

csharp 复制代码
// 方式一:CancellationTokenSource 定时取消
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var reply = await client.SayHelloAsync(
    new HelloRequest { Name = "张三" },
    cancellationToken: cts.Token);

// 方式二:deadline(绝对时间点)
var reply2 = await client.SayHelloAsync(
    new HelloRequest { Name = "张三" },
    deadline: DateTime.UtcNow.AddSeconds(5));

5.3 错误处理

gRPC 异常是 RpcException,通过 StatusCode 区分:

csharp 复制代码
try
{
    var reply = await client.SayHelloAsync(req);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
{
    MessageBox.Show("请求超时,请重试");
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.Unauthenticated)
{
    MessageBox.Show("登录已过期");
}

六、进阶:拦截器与重试

拦截器(Interceptor) 可以统一处理日志、注入 Token、统计耗时,避免在每个调用点重复写:

csharp 复制代码
public class LoggingInterceptor : Interceptor
{
    public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
        TRequest request, ClientInterceptorContext<TRequest, TResponse> context,
        AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
    {
        var call = continuation(request, context);
        // 拿到响应后记录日志
        var task = call.ResponseAsync.ContinueWith(t =>
        {
            Log.Info($"{context.Method} 调用完成");
        });
        return call;
    }
}

// 使用拦截器
var invoker = channel.Intercept(new LoggingInterceptor());
var client = new Greeter.GreeterClient(invoker);

七、与 WPF/WinForms 的 UI 结合

gRPC 调用是异步的,桌面端的关键是:别卡 UI 线程,await 后安全更新界面

csharp 复制代码
// WinForms:async void 事件处理器
private async void btnCall_Click(object sender, EventArgs e)
{
    btnCall.Enabled = false;   // 防重复点击
    try
    {
        var reply = await _client.SayHelloAsync(
            new HelloRequest { Name = textBox1.Text },
            deadline: DateTime.UtcNow.AddSeconds(5));

        // await 之后自动回到 UI 线程,可直接更新控件
        label1.Text = reply.Message;
    }
    catch (RpcException ex)
    {
        MessageBox.Show("调用失败:" + ex.StatusCode);
    }
    finally
    {
        btnCall.Enabled = true;
    }
}

要点:

  1. async/awaitawait 之后天然回到 UI 线程,不需要 Invoke (WPF 的 SynchronizationContext 会处理好)。
  2. 服务端流式推送的 await foreach 里更新 UI 同样安全,因为每次迭代都回到 UI 上下文。
  3. 页面/窗体关闭时,记得 CancellationTokenSource 取消进行中的流,避免资源泄漏。

总结

WPF/WinForms 客户端用 gRPC 通信,掌握这 7 点就够用了:

  1. .proto 文件做强类型契约,前后端共享同一份。
  2. Grpc.Tools 编译时自动生成客户端
  3. GrpcChannel 是重资源,全局复用,用 DI 管理生命周期。
  4. 按场景选四种通信模式:一元、服务端流、客户端流、双向流。
  5. 必须设超时/取消 ,用 Metadata 传 Token 认证,RpcException 做错误处理。
  6. 拦截器统一处理日志、Token 注入。
  7. 结合桌面 UI 用 async/awaitawait 后安全更新界面,退出前取消流。

gRPC + Protobuf 带来的强类型、高性能、流式能力,会让你的桌面客户端和后端协作更顺畅。如果你还在纠结"REST 还是 gRPC",下一篇我可以专门写两者的选型对比与迁移实践。


本文为原创,转载注明出处。觉得有用就点赞、收藏、关注,感谢支持!

相关推荐
一见已难忘1 天前
基于 Harmony 6.0 应用的宠物寄养预约系统实现
wpf·宠物
fl1768311 天前
基于C#WPF实现的内存加速球清理类似360加速球内存清理
开发语言·c#·wpf
李高钢1 天前
【WPF】高级 UI 与性能优化实战:从卡顿到丝滑
ui·性能优化·wpf
Chris _data2 天前
WPF 上位机开发学习笔记 - 第四天
笔记·学习·wpf
李高钢2 天前
除了 IDE,C#/WPF 开发者每天离不开的 10 个工具
ide·c#·wpf
李高钢2 天前
用 JetBrains Rider 写 WPF 的 15 个高效技巧:从 VS 转过来也值得看
wpf
公子小六3 天前
基于.NET的Windows窗体编程之WinForms音频控件
windows·microsoft·c#·.net·音视频·winforms
李高钢3 天前
【WPF】MVVM 与数据绑定实战:从原理到完整项目
wpf
李高钢3 天前
【WPF】从 Avalon 到 .NET 10:WPF 平台演进史与特性全览
.net·wpf