【问题标题】:State machine and network socket - how to handle race conditions状态机和网络套接字 - 如何处理竞争条件
【发布时间】:2021-04-02 13:53:17
【问题描述】:

我在我的 C# 网络项目中使用 Stateless,主要是因为它是添加功能的好方法,例如套接字连接后的线级授权、重新连接延迟等。

话虽如此,我自己也遇到了一些竞争条件和死锁 - 就以下状态的最佳处理方式寻求建议:

enum State { Stopped, Disconnected, Connecting, Connected, Resetting }
enum Trigger { Start, Stop, Connect, SetConnectComplete, Reset, SetResetComplete }

class StateMachine : StateMachine<State, Trigger>
{
    public StateMachine(Action OnDisconnected, Action OnConnecting, Action OnConnected, Action OnResetting) : base(State.Stopped)
    {
        this.Configure(State.Stopped)
            .Permit(Trigger.Start, State.Disconnected);

        this.Configure(State.Disconnected)
            .OnEntry(OnDisconnected)
            .Permit(Trigger.Connect, State.Connecting);

        this.Configure(State.Connecting)
            .OnEntry(OnConnecting)
            .Permit(Trigger.SetConnectComplete, State.Connected)
            .Permit(Trigger.Reset, State.Resetting);

        this.Configure(State.Connected)
            .OnEntry(OnConnected)
            .Permit(Trigger.Reset, State.Resetting);

        this.Configure(State.Resetting)
            .OnEntry(OnResetting)
            .Permit(Trigger.SetResetComplete, State.Disconnected);
    }
}

这个功能是套接字将自动重新连接,并在连接时启动接收循环。如果发生套接字错误,它应该返回以释放资源,然后循环返回以重新启动。

但是,当我处理对象时,连接的套接字中止,这也释放了资源,并且它尝试等待自己。

我相信这与等待自身的线程有关,所以我的设计/状态结构从根本上肯定是关闭的,并且很欣赏可以完全避免死锁的更好结构的指针。

public class ManagedWebSocket : IDisposable
{
    readonly StateMachine stateMachine;
    Task backgroundReaderTask;

    private ClientWebSocket webSocket;
    private readonly ITargetBlock<byte[]> target;
    private readonly ILogger<ManagedWebSocket> logger;
    private CancellationTokenSource cancellationTokenSource;
    bool isDisposing;

    public ManagedWebSocket(string uri, ITargetBlock<byte[]> target, ILogger<ManagedWebSocket> logger)
    {
        this.stateMachine = new StateMachine(OnDisconnected, OnConnecting, OnConnected, OnResetting);
        this.target = target;
        this.logger = logger;
    }

    private void OnConnecting()
    {
        this.backgroundReaderTask = Task.Run(async () =>
        {
            this.cancellationTokenSource = new CancellationTokenSource();
            this.webSocket = new ClientWebSocket();
            webSocket.Options.KeepAliveInterval = KeepAliveInterval;

            try
            {
                await this.webSocket.ConnectAsync(this.uri, cancellationTokenSource.Token);
            }
            catch(WebSocketException ex)
            {
                this.logger.LogError(ex.Message, ex);
                await this.stateMachine.FireAsync(Trigger.Reset);
            }

            this.stateMachine.Fire(Trigger.SetConnectComplete);
        });
    }
    
    private void OnDisconnected()
    {
        if (isDisposing == false)
            this.stateMachine.Fire(Trigger.Connect);
    }

    private void OnResetting()
    {
        FreeResources();
        this.stateMachine.Fire(Trigger.SetResetComplete);
    }

    private void OnConnected()
    {
        this.backgroundReaderTask = Task.Run( async () => {
            try
            {
                // returns when the internal frame loop completes with websocket close, or by throwing an exception
                await this.webSocket.ReceiveFramesLoopAsync(target.SendAsync, 2048, this.cancellationTokenSource.Token);
            }
            catch (Exception ex)
            {
                this.logger.LogError(ex.Message, ex);
            }

            await this.stateMachine.FireAsync(Trigger.Reset);
        });
    }

    public async Task SendAsync(byte[] data, WebSocketMessageType webSocketMessageType)
    {
        if (this.stateMachine.State != State.Connected)
            throw new Exception($"{nameof(ManagedWebSocket)} is not yet connected.");

        try
        {
            await webSocket
                    .SendAsChunksAsync(data, webSocketMessageType, 2048, this.cancellationTokenSource.Token)
                    .ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            this.logger.LogError(ex, ex.Message);
            await this.stateMachine.FireAsync(Trigger.Reset);
        }
    }

    public void Start()
    {
        this.stateMachine.Fire(Trigger.Start);
    }    

    public void FreeResources()
    {
        this.logger.LogDebug($"{nameof(ManagedWebSocket.FreeResources)}");
        this.cancellationTokenSource?.Cancel();
        this.backgroundReaderTask?.Wait();
        this.cancellationTokenSource?.Dispose();
        this.backgroundReaderTask?.Dispose();
    }

    public void Dispose()
    {
        if (isDisposing)
            return;

        isDisposing = true;
        FreeResources();
    }
}

【问题讨论】:

  • 你为什么使用 websocket?您是建立 HTTP 连接还是 TCP 连接?一个 weboscket 你只有一个请求和一个响应。 KeepAlive 没有意义。
  • 是的,这是按照github.com/polygon-io/client-cs/blob/master/websocket_example/… 设计的,我需要订阅、验证订阅等,因此需要查看状态机
  • 一个 websocket 是 HTTP 并且使用 TCP 作为传输层。在网络托管库中,您无法从 websocket 访问 TCP(您正在使用的状态)。一个 websocket 你得到一个请求和一个响应。因此,您要么得到响应,要么在没有得到响应时检查超时。如果你想要 TCP 状态,那么你需要一个使用 TCP 连接实现 HTTP 协议的库。
  • 感谢.gbr 回复 jdweng,websocket 绝对是一个数据流,根据我发送的链接,每秒传输数万条单独的消息。似乎我们的术语可能会跨越电线,我使用底层 ClientWebSocket 并且它以流的形式发送数据。其他库(例如链接中的库)使用 similair .net 类。
  • 您在使用 Netstat 时是否收到“等待时间”错误? TCP 连接只能在连接的一端关闭。自 1970 年代以来,由于 RFC 规范的问题,TCP 存在一个已知的竞争条件。当 TCP 连接的两端同时关闭时,就会出现竞争条件。 TCP 每个命令都会收到一个 ACK​​。当连接的两端同时关闭时,一端没有收到 ACK。在 Windows 中,您使用 Netstat 获得时间等待状态。这意味着连接正在等待对 Close 命令的 ACK。

标签: c# state-machine


【解决方案1】:

我猜死锁是由在OnResetting() 中调用FreeResources(); 引起的,因为FreeResources(); 正在等待backgroundReaderTask,但在backgroundReaderTask 中你正在通过await this.stateMachine.FireAsync(Trigger.Reset); 等待OnResetting()

作为某种解决方法,您可以省略“await”关键字来触发重置,因为它无论如何都会释放整个对象。

另请注意,如果先前在 OnConnecting() 中引发异常,似乎没有理由调用 this.stateMachine.Fire(Trigger.SetConnectComplete); - 只需将其移至 try 块。

此外,作为某种最佳实践和旁注,请尝试遵循recommended dispose pattern

【讨论】:

  • @jdweng 在问题的 cmets 中:WebSocket 是通过 HTTP 启动的,但它本身是 TCP 之上的协议。它在 RFC 6455 中进行了标准化。在 WebSocket 的上下文中指定保持活动间隔确实非常有意义,因为当至少在“保持活动”期间没有通信时,WebSocket 连接被假定为“死”。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-02-23
  • 2019-06-08
  • 1970-01-01
相关资源
最近更新 更多