RPC远程过程调用,二进制封装,提供高吞吐低延迟的高性能RPC框架
大石头 authored at 2022-08-10 13:26:19
6.14 KiB
NewLife.Remoting
using System.Text;
using NewLife.Log;
using NewLife.Remoting.Models;
using NewLife.Remoting.Services;
using NewLife.Serialization;

namespace NewLife.Remoting.Extensions.Services;

/// <summary>SSE 命令会话。通过 Server-Sent Events 实现服务端向客户端的单向命令推送</summary>
/// <remarks>
/// SSE 是 WebSocket 的轻量降级通道,适用于:
/// <list type="bullet">
/// <item>客户端环境不支持 WebSocket(某些代理/防火墙)</item>
/// <item>仅需服务端→客户端单向推送的场景</item>
/// <item>作为 WebSocket 的备用通道,提高整体可靠性</item>
/// </list>
/// 
/// <para>SSE 协议格式:</para>
/// <code>
/// event: command
/// data: {"Id":1,"Command":"Restart",...}
///
/// </code>
/// 每条消息以空行(\n\n)分隔,支持多行 data。
/// 
/// <para>客户端通过 HTTP GET /Device/NotifySSE 建立连接,需携带设备令牌。</para>
/// </remarks>
public class SseCommandSession : CommandSession
{
    #region 属性
    private readonly HttpResponse _response;
    private readonly Stream _body;

    /// <summary>服务提供者。用于获取 JSON 序列化器</summary>
    public IServiceProvider? ServiceProvider { get; set; }

    /// <summary>取消令牌源</summary>
    private CancellationTokenSource? _source;

    /// <summary>是否活动中。根据响应流是否可写判断</summary>
    public override Boolean Active => !_response.HttpContext.RequestAborted.IsCancellationRequested;

    /// <summary>心跳间隔(秒)。默认 30 秒,发送 SSE 注释防止代理超时</summary>
    public Int32 HeartbeatInterval { get; set; } = 30;
    #endregion

    #region 构造
    /// <summary>实例化 SSE 命令会话</summary>
    /// <param name="response">HTTP 响应</param>
    /// <param name="deviceCode">设备编码</param>
    /// <param name="serviceProvider">服务提供者</param>
    public SseCommandSession(HttpResponse response, String deviceCode, IServiceProvider serviceProvider)
    {
        _response = response ?? throw new ArgumentNullException(nameof(response));
        _body = response.Body;
        Code = deviceCode;
        ServiceProvider = serviceProvider;
    }

    /// <summary>销毁资源</summary>
    /// <param name="disposing"></param>
    protected override void Dispose(Boolean disposing)
    {
        base.Dispose(disposing);

        try
        {
            _source?.Cancel();
            _source.TryDispose();
        }
        catch { }
    }
    #endregion

    #region 命令处理
    /// <summary>通过 SSE 向客户端发送命令</summary>
    /// <param name="command">命令模型</param>
    /// <param name="message">原始命令消息的 JSON 字符串</param>
    /// <param name="cancellationToken">取消令牌</param>
    /// <returns></returns>
    public override async Task HandleAsync(CommandModel command, String? message, CancellationToken cancellationToken)
    {
        if (!Active) return;

        // 优先使用原始消息,避免重复序列化
        if (message == null && command != null)
        {
            var jsonHost = ServiceProvider?.GetService<IJsonHost>();
            message = jsonHost != null ? jsonHost.Write(command) : command.ToJson();
        }

        if (message.IsNullOrEmpty()) return;

        // SSE 格式: "event: command\ndata: {json}\n\n"
        var sseData = $"event: command\ndata: {message}\n\n";
        var bytes = Encoding.UTF8.GetBytes(sseData);

        try
        {
            await _body.WriteAsync(bytes, cancellationToken).ConfigureAwait(false);
            await _body.FlushAsync(cancellationToken).ConfigureAwait(false);
        }
        catch (Exception ex) when (ex is not OperationCanceledException)
        {
            Log?.WriteLog("SSE发送命令失败", false, ex.Message);
        }
    }
    #endregion

    #region SSE 等待
    /// <summary>初始化 SSE 响应头并保持连接</summary>
    /// <param name="span">链路追踪埋点</param>
    /// <param name="cancellationToken">取消令牌</param>
    /// <returns></returns>
    public virtual async Task WaitAsync(ISpan? span, CancellationToken cancellationToken)
    {
        // 设置 SSE 响应头
        _response.StatusCode = 200;
        _response.ContentType = "text/event-stream";
        _response.Headers.Append("Cache-Control", "no-cache");
        _response.Headers.Append("Connection", "keep-alive");
        _response.Headers.Append("X-Accel-Buffering", "no");  // 禁用 nginx 缓冲

        // 通知上线
        SetOnline?.Invoke(true);

        // 结束创建埋点
        span?.TryDispose();

        // 创建取消令牌
        using var source = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _response.HttpContext.RequestAborted);
        _source = source;

        try
        {
            // 发送初始连接事件
            var connected = $"event: connected\ndata: {{\"code\":\"{Code}\"}}\n\n";
            await _body.WriteAsync(Encoding.UTF8.GetBytes(connected), source.Token).ConfigureAwait(false);
            await _body.FlushAsync(source.Token).ConfigureAwait(false);

            // 保持连接,定期发送心跳
            Log?.WriteLog("SSE连接", true, $"Code={Code}");

            while (!source.IsCancellationRequested)
            {
                try
                {
                    await Task.Delay(HeartbeatInterval * 1000, source.Token).ConfigureAwait(false);

                    // 发送 SSE 注释作为心跳,防止代理超时
                    var heartbeat = ": heartbeat\n\n";
                    await _body.WriteAsync(Encoding.UTF8.GetBytes(heartbeat), source.Token).ConfigureAwait(false);
                    await _body.FlushAsync(source.Token).ConfigureAwait(false);
                }
                catch (OperationCanceledException) { break; }
                catch (Exception ex)
                {
                    Log?.WriteLog("SSE心跳异常", false, ex.Message);
                    break;
                }
            }
        }
        finally
        {
            _source = null;
        }

        Log?.WriteLog("SSE断开", true, $"Code={Code}");

        // 通知下线
        SetOnline?.Invoke(false);
    }
    #endregion
}