实现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
相关产品推荐
相关产品推荐

