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 性能的关键:
- 不要每次调用都
new GrpcChannel,否则连接池失效、性能骤降。 - 在应用生命周期内全局复用一个 Channel,用单例或依赖注入管理。
- 程序退出前正确
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;
}
}
要点:
- 用
async/await,await之后天然回到 UI 线程,不需要Invoke(WPF 的SynchronizationContext会处理好)。 - 服务端流式推送的
await foreach里更新 UI 同样安全,因为每次迭代都回到 UI 上下文。 - 页面/窗体关闭时,记得
CancellationTokenSource取消进行中的流,避免资源泄漏。
总结
WPF/WinForms 客户端用 gRPC 通信,掌握这 7 点就够用了:
- 用
.proto文件做强类型契约,前后端共享同一份。 Grpc.Tools编译时自动生成客户端。GrpcChannel是重资源,全局复用,用 DI 管理生命周期。- 按场景选四种通信模式:一元、服务端流、客户端流、双向流。
- 必须设超时/取消 ,用
Metadata传 Token 认证,RpcException做错误处理。 - 用拦截器统一处理日志、Token 注入。
- 结合桌面 UI 用
async/await,await后安全更新界面,退出前取消流。
gRPC + Protobuf 带来的强类型、高性能、流式能力,会让你的桌面客户端和后端协作更顺畅。如果你还在纠结"REST 还是 gRPC",下一篇我可以专门写两者的选型对比与迁移实践。
本文为原创,转载注明出处。觉得有用就点赞、收藏、关注,感谢支持!