You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

实现WebSocket客户端认证方法的非重叠调用

WebSocket客户端会话冲突问题解决方案

我正在开发一个WebSocket客户端包装器,对接的第三方服务器会管理会话,且不支持并发会话。若连续两次调用AuthenticateAsync方法,由于会话重叠(第一个会话在第二个启动时仍短暂处于活跃状态),两个会话都会被终止。

基础实现代码

public async Task AuthenticateAsync()
{
    Console.WriteLine("Starting authentication...");
    await Task.Delay(3000); // 模拟调用服务器API的IO网络操作
    Console.WriteLine("Authentication completed.");
}

public async Task GetAccountInformationAsync()
{
    // TODO: 检查是否已登录
}

修复方案

  • 方案一:当AuthenticateAsync被连续调用时,第二次调用需等待第一次完成,确保第一次失败时第二次自动重试。已知用TaskCompletionSource可轻松实现,但希望通过Rx.NET完成。
  • 方案二:使用状态设计模式控制登录流程,系统分为LoggedOff、LoggingIn、LoggedIn三种状态。调用AuthenticateAsync时检查当前状态,若处于LoggingIn或LoggedIn则不启动新登录,避免并发登录。

连续调用预期结果

连续调用5次AuthenticateAsync应得到如下结果:

  • 登录成功
  • 正在登录(快速失败,而非等待前次调用完成)
  • 正在登录
  • 正在登录
  • 正在登录

方案二实现代码

以下是第二种方案的实现代码,注意登录操作完成后不会立即将状态设为LoggedIn,需在收到服务器响应后手动调用SetLoggedInState/SetLoggedOffState来设置最终状态:

public enum LogonState
{
    LoggedOff,
    LoggingIn,
    LoggedIn
}

public class StateTransitionResult
{
    public bool Success { get; }
    public string Message { get; }

    public StateTransitionResult(bool success, string message)
    {
        Success = success;
        Message = message;
    }

    public static StateTransitionResult Ok() => new(true, string.Empty);
    public static StateTransitionResult Fail(string message) => new(false, message);
}

public class LogonStateManager
{
    private readonly BehaviorSubject<LogonState> _loginStateSubject = new(LogonState.LoggedOff);
    private readonly SemaphoreSlim _logonLock = new(1, 1); // 同一时间仅允许一个调用者

    public IObservable<LogonState> CurrentLogonState => _loginStateSubject.AsObservable();

    public async Task<StateTransitionResult> TryLogonAsync(Func<Task> logonAction)
    {
        await _logonLock.WaitAsync();

        try
        {
            if (!CanTransitionTo(LogonState.LoggingIn))
            {
                return StateTransitionResult.Fail(GetMessageForLogonState(_loginStateSubject.Value));
            }

            _loginStateSubject.OnNext(LogonState.LoggingIn);
            await logonAction();
            // 此时仍处于"LoggingIn"状态,最终状态由调用方决定
            return StateTransitionResult.Ok();

        }
        finally
        {
            _logonLock.Release();
        }
    }

    public StateTransitionResult SetLoggedOffState()
    {
        if (!CanTransitionTo(LogonState.LoggedOff))
        {
            return StateTransitionResult.Fail("无法从当前状态切换到LoggedOff。");
        }

        _loginStateSubject.OnNext(LogonState.LoggedOff);
        return StateTransitionResult.Ok();

    }

    public StateTransitionResult SetLoggedInState()
    {
        if (!CanTransitionTo(LogonState.LoggedIn))
        {
            return StateTransitionResult.Fail("无法从当前状态切换到LoggedIn。");
        }

        _loginStateSubject.OnNext(LogonState.LoggedIn);
        return StateTransitionResult.Ok();

    }

    private bool CanTransitionTo(LogonState targetState)
    {
        var currentState = _loginStateSubject.Value;

        switch (currentState)
        {
            case LogonState.LoggedOff:
                return targetState == LogonState.LoggingIn;

            case LogonState.LoggingIn:
                return targetState == LogonState.LoggedIn || targetState == LogonState.LoggedOff;

            case LogonState.LoggedIn:
                return targetState == LogonState.LoggedOff;

            default:
                return false;
        }
    }

    private string GetMessageForLogonState(LogonState state) =>
        state switch
        {
            LogonState.LoggingIn => "系统正在登录过程中。",
            LogonState.LoggedIn => "系统已登录。",
            LogonState.LoggedOff => "系统当前处于未登录状态。",
            _ => "未知登录状态。"
        };
}

public class LogonStateManagerTests
{
    [Fact]
    public async Task DefaultLogonStateIsLoggedOff()
    {
        // 准备
        var sut = new LogonStateManager();

        // 执行
        var state = await sut.CurrentLogonState.FirstAsync();

        // 断言
        state.Should().Be(LogonState.LoggedOff);
    }

    [Fact]
    public async Task OnlyOneLogonCallSucceedsWhenConcurrentlyInvoked()
    {
        // 准备
        var sut = new LogonStateManager();
        var task1 = sut.TryLogonAsync(async () => await Task.Delay(100));
        var task2 = sut.TryLogonAsync(async () => await Task.Delay(100));

        // 执行
        var results = await Task.WhenAll(task1, task2);

        // 断言
        results.Should().ContainSingle(r => r.Success).And.ContainSingle(r => !r.Success);
    }

    [Fact]
    public async Task LogonRemainsInLoggingInStateUntilExplicitStateChange()
    {
        // 准备
        var sut = new LogonStateManager();

        // 执行 & 断言:检查首次登录尝试及之后的状态
        var firstLogonResult = await sut.TryLogonAsync(() => Task.CompletedTask);
        firstLogonResult.Success.Should().BeTrue("首次登录尝试应成功");

        var intermediateState = await sut.CurrentLogonState.FirstAsync();
        intermediateState.Should().Be(LogonState.LoggingIn, "调用TryLogonAsync后应保持LoggingIn状态");

        // 执行 & 断言:设置登录状态并检查
        var setStateResult = sut.SetLoggedInState();
        setStateResult.Success.Should().BeTrue("切换到已登录状态应成功");

        var loggedInState = await sut.CurrentLogonState.FirstAsync();
        loggedInState.Should().Be(LogonState.LoggedIn, "调用SetLoggedInState后应处于LoggedIn状态");

        // 执行 & 断言:已登录状态下再次尝试登录
        var secondLogonResult = await sut.TryLogonAsync(() => Task.CompletedTask);
        secondLogonResult.Success.Should().BeFalse("已登录时再次尝试登录应失败");
    }
}

内容的提问来源于stack exchange,提问作者nop

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 17:35:17