ABP VNext + gRPC 双向流:实时数据推送与订阅场景实现

🚀 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 单次请求-响应
Server Streaming 服务端流
客户端
Client Streaming 客户端流
服务端
Bidirectional 双向流
服务端
调用类型请求数响应数典型场景
Unary11简单 RPC
Server Streaming1N日志推送、文件下载
Client StreamingN1批量上传
Bidirectional StreamingNN实时聊天、行情订阅

🔄 双向流时序图

Client Server WriteAsync(ChatMessage) %% 客户端发送消息 WriteAsync(ChatMessage) %% 服务端响应 循环读写,直到客户端调用 CompleteAsync() RequestStream.CompleteAsync() 终止 ResponseStream Client Server

🚀 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>

📂 参考资源


📦 示例源码

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kookoos

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值