| | |
| | | namespace WIDESEAWCS_Tasks.SocketServer |
| | | { |
| | | /// <summary> |
| | | /// TCP Socket服务端(基于行协议,按换行符分割消息) |
| | | /// TCP Socket 服务器 - 核心类 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 核心职责: |
| | | /// 1. 接受客户端 TCP 连接 |
| | | /// 2. 管理客户端连接状态 |
| | | /// 3. 接收和发送消息 |
| | | /// 4. 处理设备注册 |
| | | /// 5. 消息帧解析(支持头尾标识) |
| | | /// |
| | | /// 服务器使用以下数据结构管理客户端: |
| | | /// - _clients: 客户端 ID 到 TcpClient 的映射 |
| | | /// - _clientLocks: 客户端 ID 到信号量的映射(保证每个客户端的发送互斥) |
| | | /// - _deviceBindings: 设备 ID 到客户端 ID 的映射 |
| | | /// - _clientEncodings: 客户端 ID 到编码的映射(支持自动编码检测) |
| | | /// - _clientLastActive: 客户端 ID 到最后活跃时间的映射 |
| | | /// </remarks> |
| | | public partial class TcpSocketServer : IDisposable |
| | | { |
| | | /// <summary> |
| | | /// 服务器配置选项 |
| | | /// </summary> |
| | | private readonly SocketServerOptions _options; |
| | | |
| | | /// <summary> |
| | | /// 提供一个可用于同步对包含实例的访问的对象。 |
| | | /// 同步根对象,用于线程同步 |
| | | /// </summary> |
| | | /// <remarks>在对实例实现线程安全操作时,可将此对象用作锁定目标。此模式通常用于避免死锁并确保一致的同步。</remarks> |
| | | public readonly object _syncRoot = new(); |
| | | /// <remarks> |
| | | /// 在多线程访问共享数据结构时使用此对象进行同步。 |
| | | /// 采用保守策略,确保线程安全。 |
| | | /// </remarks> |
| | | public readonly object _syncRoot = new object(); |
| | | |
| | | /// <summary> |
| | | /// TCP 监听器 |
| | | /// </summary> |
| | | private TcpListener? _listener; |
| | | |
| | | /// <summary> |
| | | /// 表示用于发出进行中操作的取消请求的取消令牌源。 |
| | | /// 取消令牌源 |
| | | /// </summary> |
| | | /// <remarks>如果当前没有活动的取消机制,此字段可能为null。使用此令牌源取消支持取消的任务或操作。</remarks> |
| | | /// <remarks> |
| | | /// 用于请求停止服务器的运行。 |
| | | /// </remarks> |
| | | public CancellationTokenSource? _cts; |
| | | |
| | | /// <summary> |
| | | /// 提供表示活动客户端操作的任务列表。 |
| | | /// 客户端任务列表 |
| | | /// </summary> |
| | | /// <remarks>此字段用于内部跟踪异步客户端活动。它是只读的,不应在包含类外部直接修改。</remarks> |
| | | /// <remarks> |
| | | /// 记录所有活跃客户端的处理任务。 |
| | | /// </remarks> |
| | | public readonly List<Task> _clientTasks = new(); |
| | | |
| | | /// <summary> |
| | | /// 提供从客户端标识符到其关联的TCP客户端连接的映射。 |
| | | /// 客户端连接字典 |
| | | /// </summary> |
| | | /// <remarks>此字典允许通过唯一字符串标识符访问活动的TCP客户端。在多线程场景中,对集合的修改应小心进行以避免并发问题。</remarks> |
| | | /// <remarks> |
| | | /// Key: 客户端 ID(通常是 IP:Port) |
| | | /// Value: TcpClient 连接对象 |
| | | /// </remarks> |
| | | public readonly Dictionary<string, TcpClient> _clients = new(); |
| | | |
| | | /// <summary> |
| | | /// 提供从设备标识符到其对应绑定值的映射。 |
| | | /// 设备绑定字典 |
| | | /// </summary> |
| | | /// <remarks>此字段是只读的,用于包含类内部使用。应通过指定的方法或属性对字典进行修改以确保一致性。</remarks> |
| | | /// <remarks> |
| | | /// Key: 设备 ID |
| | | /// Value: 客户端 ID |
| | | /// 用于通过设备 ID 找到对应的客户端连接。 |
| | | /// </remarks> |
| | | public readonly Dictionary<string, string> _deviceBindings = new(); |
| | | |
| | | /// <summary> |
| | | /// 提供从客户端标识符到其关联锁的映射,用于同步对客户端特定资源的访问。 |
| | | /// 客户端锁字典 |
| | | /// </summary> |
| | | /// <remarks>字典中的每个条目将一个唯一的客户端ID与一个<see cref="SemaphoreSlim"/>实例关联,实现每个客户端的线程安全操作。此集合用于内部协调并发访问,不应直接修改。</remarks> |
| | | /// <remarks> |
| | | /// 每个客户端一个 SemaphoreSlim,确保同一客户端的发送操作互斥。 |
| | | /// </remarks> |
| | | public readonly Dictionary<string, SemaphoreSlim> _clientLocks = new(); |
| | | |
| | | /// <summary> |
| | | /// 提供从客户端标识符到其关联文本编码的映射。 |
| | | /// 客户端编码字典 |
| | | /// </summary> |
| | | /// <remarks>此字典用于内部跟踪已连接客户端的编码偏好。键表示客户端标识符,值指定用于文本操作的对应<see cref="System.Text.Encoding"/>。</remarks> |
| | | /// <remarks> |
| | | /// 记录每个客户端使用的字符编码。 |
| | | /// 支持自动检测:UTF-8 或 GBK。 |
| | | /// </remarks> |
| | | public readonly Dictionary<string, Encoding> _clientEncodings = new(); |
| | | |
| | | /// <summary> |
| | | /// 存储每个客户端最后活动的时间戳,以客户端标识符为键。 |
| | | /// 客户端最后活跃时间字典 |
| | | /// </summary> |
| | | /// <remarks>此字段用于内部跟踪客户端活动。字典将客户端标识符映射到对应的最后活动时间(UTC)。直接修改此集合可能影响客户端会话管理逻辑。</remarks> |
| | | /// <remarks> |
| | | /// 记录每个客户端最后一次活动的时间。 |
| | | /// 用于空闲超时检测。 |
| | | /// </remarks> |
| | | public readonly Dictionary<string, DateTime> _clientLastActive = new(); |
| | | |
| | | /// <summary> |
| | | /// 指定包含类型中字符数据使用的文本编码。 |
| | | /// 默认文本编码 |
| | | /// </summary> |
| | | /// <remarks>使用此字段确定处理字符数据时如何编码或解码文本。编码影响字节如何被解释为字符,反之亦然。常见的编码包括UTF8、ASCII和Unicode。</remarks> |
| | | public readonly Encoding _textEncoding; |
| | | |
| | | /// <summary> |
| | | /// 表示自动检测到的GB2312编码(如果可用)。 |
| | | /// 自动检测的 GBK 编码 |
| | | /// </summary> |
| | | /// <remarks>通常从输入数据确定编码时设置此字段。如果检测失败或未执行检测,值可能为null。</remarks> |
| | | public readonly Encoding? _autoDetectedGb2312; |
| | | |
| | | /// <summary> |
| | | /// 日志文件路径 |
| | | /// </summary> |
| | | private readonly string _logFile; |
| | | |
| | | /// <summary> |
| | | /// 客户端监控任务 |
| | | /// </summary> |
| | | private Task? _monitorTask; |
| | | |
| | | /// <summary> |
| | | /// 使用指定的服务器选项初始化 TcpSocketServer 类的新实例。 |
| | | /// 服务器是否正在运行 |
| | | /// </summary> |
| | | /// <remarks>如果启用了 AutoDetectEncoding 选项,服务器将默认使用 UTF-8 编码, |
| | | /// 并尝试支持 GBK 编码进行自动检测。如果编码检测失败或提供了无效的编码名称, |
| | | /// 将回退使用 UTF-8 编码。日志文件路径由 LogFilePath 选项决定, |
| | | /// 如果未指定,则默认为应用程序基目录下的 'socketserver.log' 文件。</remarks> |
| | | /// <param name="options">套接字服务器的配置选项。不能为 null。提供编码设置、日志文件路径和自动检测行为等配置。</param> |
| | | public bool IsRunning { get; private set; } |
| | | |
| | | /// <summary> |
| | | /// 消息接收事件 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 当服务器接收到消息时触发。 |
| | | /// 参数:消息内容、是否 JSON 格式、TCP 客户端、机器人状态 |
| | | /// </remarks> |
| | | public event Func<string, bool, TcpClient, RobotSocketState, Task<string?>>? MessageReceived; |
| | | |
| | | /// <summary> |
| | | /// 机器人连接断开事件 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 当机器人客户端断开连接时触发。 |
| | | /// 参数:客户端 ID |
| | | /// </remarks> |
| | | public event Func<string, Task<string?>>? RobotReceived; |
| | | |
| | | /// <summary> |
| | | /// 构造函数 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 使用指定的配置选项初始化 TcpSocketServer 实例。 |
| | | /// 配置项包括:端口、字符编码、自动编码检测、日志文件路径等。 |
| | | /// </remarks> |
| | | /// <param name="options">Socket 服务器配置选项</param> |
| | | public TcpSocketServer(IOptions<SocketServerOptions> options) |
| | | { |
| | | _options = options.Value; |
| | | |
| | | // 配置字符编码 |
| | | if (_options.AutoDetectEncoding) |
| | | { |
| | | // 自动检测编码模式:默认 UTF-8,也支持 GBK |
| | | _textEncoding = Encoding.UTF8; |
| | | try { _autoDetectedGb2312 = Encoding.GetEncoding("GBK"); } catch { _autoDetectedGb2312 = null; } |
| | | } |
| | | else |
| | | { |
| | | // 指定编码模式 |
| | | try { _textEncoding = Encoding.GetEncoding(_options.EncodingName ?? "utf-8"); } |
| | | catch { _textEncoding = Encoding.UTF8; } |
| | | _autoDetectedGb2312 = null; |
| | | } |
| | | |
| | | // 配置日志文件路径 |
| | | _logFile = Path.Combine(AppContext.BaseDirectory ?? ".", _options.LogFilePath ?? "socketserver.log"); |
| | | Log($"[{DateTime.Now}] TcpSocketServer starting"); |
| | | } |
| | | |
| | | public bool IsRunning { get; private set; } |
| | | |
| | | public event Func<string, bool, TcpClient, RobotSocketState, Task<string?>>? MessageReceived; |
| | | |
| | | public event Func<string, Task<string?>>? RobotReceived; |
| | | |
| | | /// <summary> |
| | | /// 记录日志 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 将消息输出到控制台并写入日志文件。 |
| | | /// </remarks> |
| | | /// <param name="message">日志消息</param> |
| | | private void Log(string message) |
| | | { |
| | | Console.WriteLine(message); |