🚀 ABP VNext + gRPC 双向流:实时数据推送与订阅场景实现
📚 目录
📄 背景与动机
gRPC 提供三种流式调用模式(Server Streaming、Client Streaming、Bidirectional Streaming),在现代实时系统中至关重要。特别是在 ABP VNext 架构下,双向流(Bidirectional Streaming)可实现客户端与服务端的双向、持续通信,适用于日志推送、聊天系统、行情订阅等场景,兼具低延迟和高吞吐。
🧰 环境与依赖
-
.NET SDK:6.0+
-
ABP VNext:6.x+
-
NuGet 包:
Grpc.AspNetCore
-
appsettings.json 示例(开发环境 HTTP/2 明文):
{ "Kestrel": { "Endpoints": { "Grpc": { "Url": "http://localhost:5001", "Protocols": "Http2" } } } }
生产环境:强烈建议启用 HTTPS/TLS,并在 Kestrel 中配置证书,避免明文传输。
🔁 流式模型对比
调用类型 | 请求数 | 响应数 | 典型场景 |
---|---|---|---|
Unary | 1 | 1 | 简单 RPC |
Server Streaming | 1 | N | 日志推送、文件下载 |
Client Streaming | N | 1 | 批量上传 |
Bidirectional Streaming | N | N | 实时聊天、行情订阅 |
🔄 双向流时序图
🚀 ABP 集成 gRPC
// Program.cs 或在 Module.ConfigureServices 中
services.AddSingleton<AuthInterceptor>();
services.AddGrpc(options =>
{
options.EnableDetailedErrors = true; // 详细错误
options.MaxReceiveMessageSize = 4 * 1024 * 1024; // 接收大小限制
options.MaxSendMessageSize = 4 * 1024 * 1024; // 发送大小限制
options.ResponseCompressionAlgorithm = "gzip"; // Gzip 压缩
// 注入拦截器
options.Interceptors.Add(typeof(AuthInterceptor));
});
// 中间件管道
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
// 映射 gRPC 服务(ABP 内部会调用 endpoints.MapGrpcService<T>())
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<ChatService>();
});
说明:如果是在非 ABP 原生项目中,请使用:
app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapGrpcService<ChatService>(); });
确保 gRPC 服务对外可访问。
📄 Proto 文件
syntax = "proto3";
package realtime;
service ChatService {
// 双向流:客户端和服务端同时读写
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
message ChatMessage {
string user = 1;
string message = 2;
int64 timestamp = 3;
}
🎙️ 服务端实现:ChatService
using System.Threading.Channels;
using Grpc.Core;
using Microsoft.Extensions.Logging;
public class ChatService : ChatService.ChatServiceBase
{
private readonly ILogger<ChatService> _logger;
public ChatService(ILogger<ChatService> logger)
{
_logger = logger;
}
public override async Task Chat(
IAsyncStreamReader<ChatMessage> requestStream,
IServerStreamWriter<ChatMessage> responseStream,
ServerCallContext context)
{
var ct = context.CancellationToken;
// 使用 Bounded Channel 实现反压
var channel = Channel.CreateBounded<ChatMessage>(new BoundedChannelOptions(1000)
{
FullMode = BoundedChannelFullMode.Wait
});
// 生产者:读取客户端流并写入 Channel
var readerTask = Task.Run(async () =>
{
await foreach (var msg in requestStream.ReadAllAsync(ct))
{
await channel.Writer.WriteAsync(msg, ct);
}
channel.Writer.Complete();
}, ct);
// 消费者:从 Channel 读取并写入到 responseStream
var writerTask = Task.Run(async () =>
{
await foreach (var msg in channel.Reader.ReadAllAsync(ct))
{
var time = DateTimeOffset
.FromUnixTimeSeconds(msg.Timestamp)
.ToLocalTime()
.ToString("o"); // ISO8601 格式
_logger.LogInformation("[{Time}] {User}: {Message}", time, msg.User, msg.Message);
try
{
await responseStream.WriteAsync(msg, ct);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled)
{
_logger.LogWarning("Client cancelled the stream");
break;
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
{
_logger.LogWarning("Write timed out");
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to write message");
break;
}
}
}, ct);
await Task.WhenAll(readerTask, writerTask);
}
}
🔐 安全与拦截器
using Grpc.Core;
using Microsoft.Extensions.Logging;
public class AuthInterceptor : Interceptor
{
private bool ValidateToken(string token)
=> /* your logic */ true;
// 通用认证处理(无返回值)
private async Task HandleAuthAsync(ServerCallContext context, Func<Task> next)
{
var header = context.RequestHeaders
.FirstOrDefault(h => h.Key.Equals("authorization", StringComparison.OrdinalIgnoreCase))
?.Value;
var token = header?.Replace("Bearer ", "");
if (string.IsNullOrEmpty(token) || !ValidateToken(token))
{
throw new RpcException(new Status(StatusCode.Unauthenticated, "Invalid token"));
}
await next();
}
// 通用认证处理(有返回值)
private async Task<T> HandleAuthAsync<T>(ServerCallContext context, Func<Task<T>> next)
{
var header = context.RequestHeaders
.FirstOrDefault(h => h.Key.Equals("authorization", StringComparison.OrdinalIgnoreCase))
?.Value;
var token = header?.Replace("Bearer ", "");
if (string.IsNullOrEmpty(token) || !ValidateToken(token))
{
throw new RpcException(new Status(StatusCode.Unauthenticated, "Invalid token"));
}
return await next();
}
// Unary(单次请求-响应)
public override Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request, ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation) =>
HandleAuthAsync(context, () => base.UnaryServerHandler(request, context, continuation));
// Server Streaming(服务端流)
public override Task ServerStreamingServerHandler<TRequest, TResponse>(
TRequest request,
IServerStreamWriter<TResponse> responseStream,
ServerCallContext context,
ServerStreamingServerMethod<TRequest, TResponse> continuation) =>
HandleAuthAsync(context, () => base.ServerStreamingServerHandler(request, responseStream, context, continuation));
// Client Streaming(客户端流)
public override Task<TResponse> ClientStreamingServerHandler<TRequest, TResponse>(
IAsyncStreamReader<TRequest> requestStream,
ServerCallContext context,
ClientStreamingServerMethod<TRequest, TResponse> continuation) =>
HandleAuthAsync(context, () => base.ClientStreamingServerHandler(requestStream, context, continuation));
// Bidirectional Streaming(双向流)
public override Task DuplexStreamingServerHandler<TRequest, TResponse>(
IAsyncStreamReader<TRequest> requestStream,
IServerStreamWriter<TResponse> responseStream,
ServerCallContext context,
DuplexStreamingServerMethod<TRequest, TResponse> continuation) =>
HandleAuthAsync(context, () => base.DuplexStreamingServerHandler(requestStream, responseStream, context, continuation));
}
🖥️ 客户端实现(Console 示例)
using Grpc.Net.Client;
using Grpc.Core;
using var channel = GrpcChannel.ForAddress(
"http://localhost:5001", // 开发环境 HTTP/2 明文
new GrpcChannelOptions
{
MaxReceiveMessageSize = 4 * 1024 * 1024,
MaxSendMessageSize = 4 * 1024 * 1024,
});
var client = new ChatService.ChatServiceClient(channel);
using var call = client.Chat();
var send = Task.Run(async () =>
{
while (true)
{
var line = Console.ReadLine();
if (string.IsNullOrWhiteSpace(line))
{
await call.RequestStream.CompleteAsync(); // 完成上行流
return;
}
await call.RequestStream.WriteAsync(new ChatMessage
{
User = "User1",
Message = line,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
});
}
});
var receive = Task.Run(async () =>
{
try
{
await foreach (var reply in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"{reply.User}: {reply.Message}");
}
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled)
{
Console.WriteLine("Stream closed by server");
}
catch (Exception ex)
{
Console.WriteLine($"Error receiving: {ex.Message}");
}
});
await Task.WhenAll(send, receive);
提示:生产环境请使用
https://localhost:5001
并配置 TLS。
📊 性能优化与可观测性
-
消息大小限制 & 压缩
services.AddGrpc(options => { options.MaxReceiveMessageSize = 4 * 1024 * 1024; options.MaxSendMessageSize = 4 * 1024 * 1024; options.ResponseCompressionAlgorithm = "gzip"; });
-
反压策略:服务端使用 Bounded Channel,防止内存爆炸。
-
链路追踪:集成 OpenTelemetry,通过
ActivitySource
捕获分布式 TraceId。 -
自定义 Metrics:使用 Prometheus .NET 客户端,在读写异常、超时等关键处打点 Counter/Gauge。
🧩 模块化与 CI 自动生成 Proto
[DependsOn(typeof(AbpAspNetCoreModule))]
public class ChatGrpcModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
// 只要注册拦截器、AddGrpc 即可
}
}
<!--
在编译时自动生成 gRPC 存根代码(ChatMessage, ChatServiceBase 等),
但还需在模块或 Program.cs 中调用 AddGrpc 和 MapGrpcService 来注册并映射服务
-->
<ItemGroup>
<Protobuf Include="Protos\chat.proto" GrpcServices="Server" />
</ItemGroup>
📂 参考资源
📦 示例源码
- 仓库地址:
RealTimeChat_Demo