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

XFEExtension

【DLL】XFE各类拓展是一个C#的DLL库,旨在优化C#代码中常用语句的使用,并提供更简洁的访问方式,同时提供Xunit测试框架,快速搭建服务器/客户端,免费ChatGPTAPI接口,免费通讯服务器,XFE下载器,新增格式等

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

namespace XFEExtension.NetCore.CyberComm;

/// <summary>
/// WebSocket 发送结果。
/// </summary>
public enum CyberCommSendStatus
{
    Success,
    Closed,
    QueueFull,
    Timeout,
    Cancelled,
    Failed
}

/// <summary>
/// WebSocket 发送结果。
/// </summary>
public readonly record struct CyberCommSendResult(CyberCommSendStatus Status, Exception? Exception = null)
{
    public bool IsSuccess => Status == CyberCommSendStatus.Success;
}

internal sealed class CyberCommWebSocketPeer : IAsyncDisposable
{
    private readonly WebSocket _webSocket;
    private readonly Channel<OutboundMessage> _outbound;
    private readonly TimeSpan _sendTimeout;
    private readonly CancellationTokenSource _stopSource = new();
    private readonly Task _sendLoop;

    public CyberCommWebSocketPeer(WebSocket webSocket, CyberCommLimitOptions limits)
    {
        _webSocket = webSocket;
        _sendTimeout = limits.SendTimeout;
        _outbound = Channel.CreateBounded<OutboundMessage>(new BoundedChannelOptions(limits.OutboundQueueCapacity)
        {
            FullMode = BoundedChannelFullMode.Wait,
            SingleReader = true,
            SingleWriter = false
        });
        _sendLoop = SendLoopAsync(_stopSource.Token);
    }

    public async ValueTask<CyberCommSendResult> SendAsync(
        ReadOnlyMemory<byte> payload,
        WebSocketMessageType messageType,
        CancellationToken cancellationToken)
    {
        if (_webSocket.State is not (WebSocketState.Open or WebSocketState.CloseReceived))
            return new(CyberCommSendStatus.Closed);

        var completion = new TaskCompletionSource<CyberCommSendResult>(TaskCreationOptions.RunContinuationsAsynchronously);
        var message = OutboundMessage.Data(payload.ToArray(), messageType, completion);
        using var timeout = CreateTimeoutSource(cancellationToken);
        try
        {
            await _outbound.Writer.WriteAsync(message, timeout.Token).ConfigureAwait(false);
            return await completion.Task.WaitAsync(timeout.Token).ConfigureAwait(false);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            return new(CyberCommSendStatus.Cancelled);
        }
        catch (OperationCanceledException)
        {
            return new(CyberCommSendStatus.Timeout);
        }
        catch (ChannelClosedException)
        {
            return new(CyberCommSendStatus.Closed);
        }
    }

    public async ValueTask<CyberCommSendResult> CloseAsync(
        WebSocketCloseStatus closeStatus,
        string description,
        CancellationToken cancellationToken)
    {
        if (_webSocket.State is WebSocketState.Closed or WebSocketState.Aborted)
            return new(CyberCommSendStatus.Closed);

        var completion = new TaskCompletionSource<CyberCommSendResult>(TaskCreationOptions.RunContinuationsAsynchronously);
        using var timeout = CreateTimeoutSource(cancellationToken);
        try
        {
            await _outbound.Writer.WriteAsync(OutboundMessage.Close(closeStatus, description, completion), timeout.Token).ConfigureAwait(false);
            return await completion.Task.WaitAsync(timeout.Token).ConfigureAwait(false);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            return new(CyberCommSendStatus.Cancelled);
        }
        catch (OperationCanceledException)
        {
            return new(CyberCommSendStatus.Timeout);
        }
        catch (ChannelClosedException)
        {
            return new(CyberCommSendStatus.Closed);
        }
    }

    private async Task SendLoopAsync(CancellationToken cancellationToken)
    {
        Exception? terminalException = null;
        try
        {
            await foreach (var message in _outbound.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
            {
                using var sendSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
                if (_sendTimeout != Timeout.InfiniteTimeSpan) sendSource.CancelAfter(_sendTimeout);
                try
                {
                    if (message.IsClose)
                    {
                        if (_webSocket.State is WebSocketState.Open or WebSocketState.CloseReceived)
                            await _webSocket.CloseOutputAsync(message.CloseStatus, message.CloseDescription, sendSource.Token).ConfigureAwait(false);
                    }
                    else
                    {
                        await _webSocket.SendAsync(message.Payload, message.MessageType, true, sendSource.Token).ConfigureAwait(false);
                    }
                    message.Completion.TrySetResult(new(CyberCommSendStatus.Success));
                }
                catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
                {
                    message.Completion.TrySetResult(new(CyberCommSendStatus.Cancelled));
                    throw;
                }
                catch (OperationCanceledException)
                {
                    message.Completion.TrySetResult(new(CyberCommSendStatus.Timeout));
                    _webSocket.Abort();
                    break;
                }
                catch (Exception ex)
                {
                    terminalException = ex;
                    message.Completion.TrySetResult(new(CyberCommSendStatus.Failed, ex));
                    break;
                }
            }
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
        }
        finally
        {
            _outbound.Writer.TryComplete(terminalException);
            while (_outbound.Reader.TryRead(out var pending))
                pending.Completion.TrySetResult(new(terminalException is null ? CyberCommSendStatus.Closed : CyberCommSendStatus.Failed, terminalException));
        }
    }

    private CancellationTokenSource CreateTimeoutSource(CancellationToken cancellationToken)
    {
        var source = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _stopSource.Token);
        if (_sendTimeout != Timeout.InfiniteTimeSpan)
            source.CancelAfter(_sendTimeout);
        return source;
    }

    public async ValueTask DisposeAsync()
    {
        _outbound.Writer.TryComplete();
        _stopSource.Cancel();
        try { await _sendLoop.ConfigureAwait(false); } catch (OperationCanceledException) { }
        _stopSource.Dispose();
    }

    private sealed record OutboundMessage(
        ReadOnlyMemory<byte> Payload,
        WebSocketMessageType MessageType,
        bool IsClose,
        WebSocketCloseStatus CloseStatus,
        string CloseDescription,
        TaskCompletionSource<CyberCommSendResult> Completion)
    {
        public static OutboundMessage Data(byte[] payload, WebSocketMessageType messageType, TaskCompletionSource<CyberCommSendResult> completion)
            => new(payload, messageType, false, WebSocketCloseStatus.Empty, string.Empty, completion);

        public static OutboundMessage Close(WebSocketCloseStatus closeStatus, string description, TaskCompletionSource<CyberCommSendResult> completion)
            => new(ReadOnlyMemory<byte>.Empty, WebSocketMessageType.Close, true, closeStatus, description, completion);
    }
}