Repository: dotnetcore/BootstrapBlazor (2025-07-08T04:09:22Z)

Issue:
feat(ITcpSocketClient): add concurrency connect logic
### Is there an existing issue for this?

- [x] I have searched the existing issues

### Is your feature request related to a problem? Please describe the problem.

add concurrency connectlogic

### Describe the solution you'd like

_No response_

### Additional context

_No response_


Discussion:
@ArgoZhang Thank you for contacting us. We will give feedback later.



Patch:
diff --git a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs
index c15e4d5ad3f..ff086bd9f06 100644
--- a/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs
+++ b/src/BootstrapBlazor/Services/TcpSocket/TcpSocketClientBase.cs
@@ -65,6 +65,8 @@ public abstract class TcpSocketClientBase(SocketClientOptions options) : ITcpSoc
     private CancellationTokenSource? _receiveCancellationTokenSource;
     private CancellationTokenSource? _autoConnectTokenSource;
 
+    private readonly SemaphoreSlim _semaphoreSlim = new(1, 1);
+
     /// <summary>
     /// <inheritdoc/>
     /// </summary>
@@ -78,41 +80,64 @@ public async ValueTask<bool> ConnectAsync(IPEndPoint endPoint, CancellationToken
             return true;
         }
 
+        var connectionToken = GenerateConnectionToken(token);
+        try
+        {
+            await _semaphoreSlim.WaitAsync(connectionToken).ConfigureAwait(false);
+        }
+        catch (OperationCanceledException)
+        {
+            // 如果信号量等待被取消，则直接返回 IsConnected
+            // 不管是超时还是被取消，都不需要重连，肯定有其他线程在连接中
+            return IsConnected;
+        }
+
+        if (IsConnected)
+        {
+            _semaphoreSlim.Release();
+            return true;
+        }
+
+        var reconnect = true;
         var ret = false;
         SocketClientProvider = ServiceProvider?.GetRequiredService<ISocketClientProvider>()
             ?? throw new InvalidOperationException("SocketClientProvider is not registered in the service provider.");
 
         try
         {
-            ret = await ConnectCoreAsync(SocketClientProvider, endPoint, token);
-            if (ret == false)
-            {
-                Reconnect();
-            }
+            ret = await ConnectCoreAsync(SocketClientProvider, endPoint, connectionToken);
         }
         catch (OperationCanceledException ex)
         {
             if (token.IsCancellationRequested)
             {
                 Log(LogLevel.Warning, ex, $"TCP Socket connect operation was canceled from {LocalEndPoint} to {endPoint}");
+                reconnect = false;
             }
             else
             {
                 Log(LogLevel.Warning, ex, $"TCP Socket connect operation timed out from {LocalEndPoint} to {endPoint}");
-                Reconnect();
             }
         }
         catch (Exception ex)
         {
             Log(LogLevel.Error, ex, $"TCP Socket connection failed from {LocalEndPoint} to {endPoint}");
+        }
+
+        // 释放信号量
+        _semaphoreSlim.Release();
+
+        if (!ret && reconnect)
+        {
             Reconnect();
         }
+
         return ret;
     }
 
     private void Reconnect()
     {
-        if (options.IsAutoReconnect)
+        if (options.IsAutoReconnect && _remoteEndPoint != null)
         {
             Task.Run(async () =>
             {
@@ -120,7 +145,7 @@ private void Reconnect()
                 {
                     _autoConnectTokenSource ??= new();
                     await Task.Delay(options.ReconnectInterval, _autoConnectTokenSource.Token).ConfigureAwait(false);
-                    HandleDisconnection();
+                    await ConnectAsync(_remoteEndPoint, _autoConnectTokenSource.Token).ConfigureAwait(false);
                 }
                 catch { }
             }, CancellationToken.None).ConfigureAwait(false);
@@ -138,14 +163,7 @@ private async ValueTask<bool> ConnectCoreAsync(ISocketClientProvider provider, I
         _localEndPoint = Options.LocalEndPoint;
         _remoteEndPoint = endPoint;
 
-        var connectionToken = token;
-        if (Options.ConnectTimeout > 0)
-        {
-            // 设置连接超时时间
-            var connectTokenSource = new CancellationTokenSource(options.ConnectTimeout);
-            connectionToken = CancellationTokenSource.CreateLinkedTokenSource(token, connectTokenSource.Token).Token;
-        }
-        var ret = await provider.ConnectAsync(endPoint, connectionToken);
+        var ret = await provider.ConnectAsync(endPoint, token);
 
         if (ret)
         {
@@ -159,13 +177,16 @@ private async ValueTask<bool> ConnectCoreAsync(ISocketClientProvider provider, I
         return ret;
     }
 
-    private void HandleDisconnection()
+    private CancellationToken GenerateConnectionToken(CancellationToken token)
     {
-        if (options.IsAutoReconnect && _remoteEndPoint != null)
+        var connectionToken = token;
+        if (Options.ConnectTimeout > 0)
         {
-            _autoConnectTokenSource ??= new();
-            _ = Task.Run(() => ConnectAsync(_remoteEndPoint, _autoConnectTokenSource.Token)).ConfigureAwait(false);
+            // 设置连接超时时间
+            var connectTokenSource = new CancellationTokenSource(options.ConnectTimeout);
+            connectionToken = CancellationTokenSource.CreateLinkedTokenSource(token, connectTokenSource.Token).Token;
         }
+        return connectionToken;
     }
 
     /// <summary>
@@ -182,6 +203,7 @@ public virtual async ValueTask<bool> SendAsync(ReadOnlyMemory<byte> data, Cancel
         }
 
         var ret = false;
+        var reconnect = true;
         try
         {
             var sendToken = token;
@@ -192,15 +214,12 @@ public virtual async ValueTask<bool> SendAsync(ReadOnlyMemory<byte> data, Cancel
                 sendToken = CancellationTokenSource.CreateLinkedTokenSource(token, sendTokenSource.Token).Token;
             }
             ret = await SocketClientProvider.SendAsync(data, sendToken);
-            if (ret == false)
-            {
-                HandleDisconnection();
-            }
         }
         catch (OperationCanceledException ex)
         {
             if (token.IsCancellationRequested)
             {
+                reconnect = false;
                 Log(LogLevel.Warning, ex, $"TCP Socket send operation was canceled from {_localEndPoint} to {_remoteEndPoint}");
             }
             else
@@ -211,14 +230,18 @@ public virtual async ValueTask<bool> SendAsync(ReadOnlyMemory<byte> data, Cancel
         catch (Exception ex)
         {
             Log(LogLevel.Error, ex, $"TCP Socket send failed from {_localEndPoint} to {_remoteEndPoint}");
-
-            HandleDisconnection();
         }
 
         if (options.EnableLog)
         {
             Log(LogLevel.Information, null, $"Sending data from {_localEndPoint} to {_remoteEndPoint}, Data Length: {data.Length} Data Content: {BitConverter.ToString(data.ToArray())} Result: {ret}");
         }
+
+        if (!ret && reconnect)
+        {
+            // 如果发送失败并且需要重连则尝试重连
+            Reconnect();
+        }
         return ret;
     }
 
@@ -244,7 +267,7 @@ public virtual async ValueTask<Memory<byte>> ReceiveAsync(CancellationToken toke
         var len = await ReceiveCoreAsync(SocketClientProvider, buffer, token);
         if (len == 0)
         {
-            HandleDisconnection();
+            Reconnect();
         }
         return buffer[..len];
     }
@@ -270,11 +293,12 @@ private async ValueTask AutoReceiveAsync()
             }
         }
 
-        HandleDisconnection();
+        Reconnect();
     }
 
     private async ValueTask<int> ReceiveCoreAsync(ISocketClientProvider client, Memory<byte> buffer, CancellationToken token)
     {
+        var reconnect = true;
         var len = 0;
         try
         {
@@ -292,8 +316,6 @@ private async ValueTask<int> ReceiveCoreAsync(ISocketClientProvider client, Memo
                 // 远端主机关闭链路
                 Log(LogLevel.Information, null, $"TCP Socket {_localEndPoint} received 0 data closed by {_remoteEndPoint}");
                 buffer = Memory<byte>.Empty;
-
-                HandleDisconnection();
             }
             else
             {
@@ -311,6 +333,7 @@ private async ValueTask<int> ReceiveCoreAsync(ISocketClientProvider client, Memo
             if (token.IsCancellationRequested)
             {
                 Log(LogLevel.Warning, ex, $"TCP Socket receive operation canceled from {_localEndPoint} to {_remoteEndPoint}");
+                reconnect = false;
             }
             else
             {
@@ -320,14 +343,18 @@ private async ValueTask<int> ReceiveCoreAsync(ISocketClientProvider client, Memo
         catch (Exception ex)
         {
             Log(LogLevel.Error, ex, $"TCP Socket receive failed from {_localEndPoint} to {_remoteEndPoint}");
-
-            HandleDisconnection();
         }
 
         if (options.EnableLog)
         {
             Log(LogLevel.Information, null, $"Receiving data from {_localEndPoint} to {_remoteEndPoint}, Data Length: {len} Data Content: {BitConverter.ToString(buffer.ToArray())}");
         }
+
+        if (len == 0 && reconnect)
+        {
+            // 如果接收数据长度为 0 并且需要重连则尝试重连
+            Reconnect();
+        }
         return len;
     }
 


Test patch:
diff --git a/test/UnitTest/Services/TcpSocketFactoryTest.cs b/test/UnitTest/Services/TcpSocketFactoryTest.cs
index bca1825a6c8..f439c7193df 100644
--- a/test/UnitTest/Services/TcpSocketFactoryTest.cs
+++ b/test/UnitTest/Services/TcpSocketFactoryTest.cs
@@ -69,6 +69,18 @@ public async Task ConnectAsync_Cancel()
         var cst = new CancellationTokenSource();
         cst.Cancel();
         var connect = await client.ConnectAsync("localhost", 9999, cst.Token);
+
+        // 由于信号量被取消，所以连接会失败
+        Assert.False(connect);
+
+        // 测试真正的连接被取消逻辑
+        cst = new CancellationTokenSource();
+        _ = Task.Run(async () =>
+        {
+            await Task.Delay(50);
+            cst.Cancel();
+        });
+        connect = await client.ConnectAsync("localhost", 9999, cst.Token);
         Assert.False(connect);
     }
 
@@ -102,6 +114,29 @@ public async Task ConnectAsync_Error()
         methodInfo.Invoke(client, [LogLevel.Error, null!, "Test error log"]);
     }
 
+    [Fact]
+    public async Task ConnectAsync_Lock()
+    {
+        // 测试并发锁问题
+        var provider = new MockAutoReconnectLockSocketProvider();
+        var client = CreateClient(builder =>
+        {
+            builder.AddTransient<ISocketClientProvider>(p => provider);
+        });
+
+        // 开 5 个线程同时连接
+        _ = Task.Run(async () =>
+        {
+            // 延时 150 保证有一个连接失败
+            await Task.Delay(150);
+            provider.SetConnected(true);
+        });
+        var results = await Task.WhenAll(Enumerable.Range(1, 5).Select(i => client.ConnectAsync("localhost", 0).AsTask()));
+        // 期望结果是 1个 false 4个 true
+        Assert.Equal(1, results.Count(r => !r));
+        Assert.Equal(4, results.Count(r => r));
+    }
+
     [Fact]
     public async Task Send_Timeout()
     {
@@ -432,6 +467,7 @@ public async Task AutoReconnect_Cancel()
         });
 
         await client.ConnectAsync("localhost", 0);
+        await Task.Delay(100);
         await client.DisposeAsync();
     }
 
@@ -912,6 +948,41 @@ public async ValueTask<bool> SendAsync(ReadOnlyMemory<byte> data, CancellationTo
         }
     }
 
+    class MockAutoReconnectLockSocketProvider : ISocketClientProvider
+    {
+        public bool IsConnected { get; private set; }
+
+        public IPEndPoint LocalEndPoint { get; set; } = new IPEndPoint(IPAddress.Loopback, 0);
+
+        public async ValueTask<bool> ConnectAsync(IPEndPoint endPoint, CancellationToken token = default)
+        {
+            await Task.Delay(100, token);
+            return IsConnected;
+        }
+
+        public ValueTask<bool> SendAsync(ReadOnlyMemory<byte> data, CancellationToken token = default)
+        {
+            return ValueTask.FromResult(true);
+        }
+
+        public ValueTask<int> ReceiveAsync(Memory<byte> buffer, CancellationToken token = default)
+        {
+            byte[] data = [1, 2, 3, 4, 5];
+            data.CopyTo(buffer);
+            return ValueTask.FromResult(5);
+        }
+
+        public ValueTask CloseAsync()
+        {
+            return ValueTask.CompletedTask;
+        }
+
+        public void SetConnected(bool state)
+        {
+            IsConnected = state;
+        }
+    }
+
     class MockAutoReconnectSocketProvider : ISocketClientProvider
     {
         public bool IsConnected { get; private set; }