| | |
| | | using System; |
| | | using System.Collections.Generic; |
| | | using System.Linq; |
| | | using System.Net; |
| | | using System.Net.Sockets; |
| | | using System.Threading; |
| | | using System.Threading.Tasks; |
| | | using System.Text; |
| | | using System.Text.Json; |
| | | using System.IO; |
| | | using WIDESEAWCS_Core.LogHelper; |
| | | |
| | | namespace WIDESEAWCS_Tasks.SocketServer |
| | | { |
| | | public partial class TcpSocketServer |
| | | { |
| | | public Task StartAsync(CancellationToken cancellationToken) |
| | | /// <summary> |
| | | /// 异步启动 TCP Socket 服务器 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 创建 TCP 监听器并开始接受客户端连接。 |
| | | /// 如果服务器已在运行或被禁用,直接返回。 |
| | | /// 启动后启动接受循环和客户端监控任务。 |
| | | /// </remarks> |
| | | /// <param name="cancellationToken">取消令牌</param> |
| | | /// <returns>启动任务</returns> |
| | | public async Task StartAsync(CancellationToken cancellationToken) |
| | | { |
| | | if (IsRunning || !_options.Enabled) |
| | | { |
| | | return Task.CompletedTask; |
| | | return; |
| | | } |
| | | |
| | | // 解析监听地址 |
| | | IPAddress ipAddress = IPAddress.Any; |
| | | if (IPAddress.TryParse(_options.IpAddress, out IPAddress? parsedAddress)) |
| | | { |
| | | ipAddress = parsedAddress; |
| | | } |
| | | |
| | | await Task.Delay(5000); |
| | | |
| | | // 创建监听器 |
| | | _listener = new TcpListener(ipAddress, _options.Port); |
| | | _listener.Start(_options.Backlog); |
| | | _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); |
| | | IsRunning = true; |
| | | |
| | | // 启动接受客户端连接循环 |
| | | _ = AcceptLoopAsync(_cts.Token); |
| | | |
| | | // 启动客户端监控任务(检查空闲超时) |
| | | _monitorTask = Task.Run(() => MonitorClientsAsync(_cts.Token)); |
| | | |
| | | return Task.CompletedTask; |
| | | return; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 异步停止 TCP Socket 服务器 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 停止接受新连接,等待所有客户端任务完成。 |
| | | /// </remarks> |
| | | /// <param name="cancellationToken">取消令牌</param> |
| | | /// <returns>停止任务</returns> |
| | | public async Task StopAsync(CancellationToken cancellationToken) |
| | | { |
| | | if (!IsRunning) |
| | |
| | | return; |
| | | } |
| | | |
| | | // 发送取消信号 |
| | | _cts?.Cancel(); |
| | | |
| | | // 停止监听 |
| | | _listener?.Stop(); |
| | | |
| | | // 等待所有客户端任务完成 |
| | | Task[] tasks; |
| | | lock (_syncRoot) |
| | | { |
| | |
| | | IsRunning = false; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 异步接受客户端连接的主循环 |
| | | /// </summary> |
| | | /// <summary> |
| | | /// 异步接受客户端连接的主循环 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 在后台线程中持续接受新的客户端连接。 |
| | | /// 当有新连接时,将其添加到客户端字典并启动消息处理任务。 |
| | | /// </remarks> |
| | | /// <param name="cancellationToken">取消令牌</param> |
| | | private async Task AcceptLoopAsync(CancellationToken cancellationToken) |
| | | { |
| | | while (!cancellationToken.IsCancellationRequested) |
| | |
| | | TcpClient? client = null; |
| | | try |
| | | { |
| | | // 等待客户端连接 |
| | | client = await _listener!.AcceptTcpClientAsync().WaitAsync(cancellationToken); |
| | | QuartzLogger.Info($"客户端连接:{client.Client.RemoteEndPoint.ToString()}"); |
| | | } |
| | | catch (OperationCanceledException) |
| | | { |
| | | break; |
| | | } |
| | | catch (ObjectDisposedException) |
| | | { |
| | | break; |
| | | } |
| | | catch (OperationCanceledException) { break; } |
| | | catch (ObjectDisposedException) { break; } |
| | | catch |
| | | { |
| | | if (cancellationToken.IsCancellationRequested) |
| | | { |
| | | break; |
| | | } |
| | | if (cancellationToken.IsCancellationRequested) break; |
| | | } |
| | | |
| | | if (client == null) |
| | | { |
| | | continue; |
| | | } |
| | | if (client == null) continue; |
| | | |
| | | // 生成客户端 ID(使用远程端点地址) |
| | | string clientId = GetClientId(client); |
| | | |
| | | // 添加到客户端字典 |
| | | lock (_syncRoot) |
| | | { |
| | | _clients[clientId] = client; |
| | |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 移除客户端连接 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 关闭客户端连接并清理相关资源: |
| | | /// - 关闭 TcpClient |
| | | /// - 释放信号量 |
| | | /// - 移除活跃时间和编码记录 |
| | | /// - 移除设备绑定 |
| | | /// </remarks> |
| | | /// <param name="clientId">要移除的客户端唯一标识</param> |
| | | private void RemoveClient(string clientId) |
| | | { |
| | | lock (_syncRoot) |
| | | { |
| | | // 关闭并移除客户端连接 |
| | | if (_clients.TryGetValue(clientId, out var client)) |
| | | { |
| | | try { client.Close(); } catch { } |
| | | _clients.Remove(clientId); |
| | | } |
| | | |
| | | // 释放信号量 |
| | | if (_clientLocks.TryGetValue(clientId, out var sem)) |
| | | { |
| | | _clientLocks.Remove(clientId); |
| | | sem.Dispose(); |
| | | } |
| | | |
| | | // 移除活跃时间记录 |
| | | _clientLastActive.Remove(clientId); |
| | | |
| | | // 移除编码记录 |
| | | _clientEncodings.Remove(clientId); |
| | | |
| | | // 移除设备绑定 |
| | | var deviceIds = _deviceBindings.Where(kv => kv.Value == clientId).Select(kv => kv.Key).ToList(); |
| | | foreach (var deviceId in deviceIds) |
| | | { |
| | |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 异步监控客户端空闲超时 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 定期检查所有客户端的最后活跃时间, |
| | | /// 如果超过空闲超时时间,断开该客户端连接。 |
| | | /// </remarks> |
| | | /// <param name="cancellationToken">取消令牌</param> |
| | | private async Task MonitorClientsAsync(CancellationToken cancellationToken) |
| | | { |
| | | while (!cancellationToken.IsCancellationRequested) |
| | |
| | | { |
| | | foreach (var kv in _clientLastActive) |
| | | { |
| | | // 检查是否超过空闲超时 |
| | | if (_options.IdleTimeoutSeconds > 0 && DateTime.Now - kv.Value > TimeSpan.FromSeconds(_options.IdleTimeoutSeconds)) |
| | | { |
| | | toRemove.Add(kv.Key); |
| | |
| | | } |
| | | } |
| | | |
| | | // 断开超时的客户端 |
| | | foreach (var cid in toRemove) |
| | | { |
| | | RemoveClient(cid); |
| | | Log($"[{DateTime.Now}] TcpSocketServer disconnect idle client {cid}"); |
| | | } |
| | | } |
| | | catch |
| | | { |
| | | } |
| | | catch { } |
| | | |
| | | // 每秒检查一次 |
| | | try { await Task.Delay(1000, cancellationToken); } catch { } |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取客户端唯一标识 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 使用客户端的远程端点地址作为标识。 |
| | | /// 如果远程端点不可用,生成随机 GUID。 |
| | | /// </remarks> |
| | | /// <param name="client">TCP 客户端</param> |
| | | /// <returns>客户端标识字符串</returns> |
| | | public static string GetClientId(TcpClient client) |
| | | { |
| | | return client.Client.RemoteEndPoint?.ToString() ?? Guid.NewGuid().ToString(); |