XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEToolBox

【WPF】XFE工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Net.WebSockets;
using System.Text;
using System.Threading.Channels;

namespace XFEToolBox.Server.Realtime;

internal sealed class ChatRealtimeConnection : IAsyncDisposable
{
    private const int InboundCapacity = 128;
    private const int MaximumPendingSends = 128;
    private readonly Channel<string> _inbound = Channel.CreateBounded<string>(new BoundedChannelOptions(InboundCapacity)
    {
        AllowSynchronousContinuations = false,
        FullMode = BoundedChannelFullMode.Wait,
        SingleReader = true,
        SingleWriter = false
    });
    private readonly SemaphoreSlim _sendLock = new(1, 1);
    private readonly CancellationTokenSource _lifetime = new();
    private readonly Queue<string> _recentEventOrder = new();
    private readonly HashSet<string> _recentEventIds = new(StringComparer.Ordinal);
    private long _lastSeenTimestamp;
    private long _rateWindow;
    private int _framesInWindow;
    private int _pendingSends;
    private int _closing;

    public ChatRealtimeConnection(WebSocket socket, ChatRealtimeTicketClaims claims, TimeProvider timeProvider)
    {
        Socket = socket;
        Claims = claims;
        TimeProvider = timeProvider;
        ConnectionId = Guid.NewGuid().ToString("N");
        Touch();
    }

    public string ConnectionId { get; }

    public WebSocket Socket { get; }

    public ChatRealtimeTicketClaims Claims { get; }

    public TimeProvider TimeProvider { get; }

    public DateTimeOffset LastSeenUtc => new(Interlocked.Read(ref _lastSeenTimestamp), TimeSpan.Zero);

    public CancellationToken LifetimeToken => _lifetime.Token;

    public void Touch() => Interlocked.Exchange(ref _lastSeenTimestamp, TimeProvider.GetUtcNow().UtcTicks);

    public bool TryConsumeFrameQuota(int maximumFramesPerTenSeconds)
    {
        var window = TimeProvider.GetUtcNow().ToUnixTimeSeconds() / 10;
        var observed = Interlocked.Read(ref _rateWindow);
        if (observed != window && Interlocked.CompareExchange(ref _rateWindow, window, observed) == observed)
            Interlocked.Exchange(ref _framesInWindow, 0);
        return Interlocked.Increment(ref _framesInWindow) <= maximumFramesPerTenSeconds;
    }

    public bool TryQueue(string message) =>
        Volatile.Read(ref _closing) == 0 && _inbound.Writer.TryWrite(message);

    public bool TryRememberEventId(string eventId)
    {
        // The inbound loop is single-reader, so this small bounded LRU needs no lock.
        if (!_recentEventIds.Add(eventId))
            return false;
        _recentEventOrder.Enqueue(eventId);
        while (_recentEventOrder.Count > 512)
            _recentEventIds.Remove(_recentEventOrder.Dequeue());
        return true;
    }

    public Task RunInboundLoopAsync(Func<ChatRealtimeConnection, string, CancellationToken, Task> handler) =>
        Task.Run(async () =>
        {
            try
            {
                await foreach (var message in _inbound.Reader.ReadAllAsync(_lifetime.Token))
                    await handler(this, message, _lifetime.Token);
            }
            catch (OperationCanceledException) when (_lifetime.IsCancellationRequested)
            {
            }
        });

    public async Task<bool> SendTextAsync(string message, CancellationToken cancellationToken = default)
    {
        if (Volatile.Read(ref _closing) != 0 || Socket.State != WebSocketState.Open)
            return false;
        if (Interlocked.Increment(ref _pendingSends) > MaximumPendingSends)
        {
            Interlocked.Decrement(ref _pendingSends);
            return false;
        }

        try
        {
            using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _lifetime.Token);
            timeout.CancelAfter(TimeSpan.FromSeconds(5));
            await _sendLock.WaitAsync(timeout.Token);
            try
            {
                if (Socket.State != WebSocketState.Open)
                    return false;
                var bytes = Encoding.UTF8.GetBytes(message);
                await Socket.SendAsync(bytes, WebSocketMessageType.Text, true, timeout.Token);
                return true;
            }
            finally
            {
                _sendLock.Release();
            }
        }
        catch (Exception exception) when (exception is OperationCanceledException or WebSocketException or ObjectDisposedException)
        {
            return false;
        }
        finally
        {
            Interlocked.Decrement(ref _pendingSends);
        }
    }

    public async Task CloseAsync(WebSocketCloseStatus status, string description)
    {
        if (Interlocked.Exchange(ref _closing, 1) != 0)
            return;

        _inbound.Writer.TryComplete();
        _lifetime.Cancel();
        try
        {
            using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(2));
            if (Socket.State is WebSocketState.Open or WebSocketState.CloseReceived)
                await Socket.CloseOutputAsync(status, description, timeout.Token);
        }
        catch (Exception exception) when (exception is OperationCanceledException or WebSocketException or ObjectDisposedException)
        {
            Socket.Abort();
        }
    }

    public async ValueTask DisposeAsync()
    {
        await CloseAsync(WebSocketCloseStatus.NormalClosure, "connection disposed");
        _sendLock.Dispose();
        _lifetime.Dispose();
    }
}