wanshenmean
2026-03-26 8e42d0c1b7ae36cff2e7c69999117911a4b6f300
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using WIDESEAWCS_QuartzJob;
 
namespace WIDESEAWCS_Tasks.SocketServer
{
    /// <summary>
    /// 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 object();
 
        /// <summary>
        /// TCP 监听器
        /// </summary>
        private TcpListener? _listener;
 
        /// <summary>
        /// 取消令牌源
        /// </summary>
        /// <remarks>
        /// 用于请求停止服务器的运行。
        /// </remarks>
        public CancellationTokenSource? _cts;
 
        /// <summary>
        /// 客户端任务列表
        /// </summary>
        /// <remarks>
        /// 记录所有活跃客户端的处理任务。
        /// </remarks>
        public readonly List<Task> _clientTasks = new();
 
        /// <summary>
        /// 客户端连接字典
        /// </summary>
        /// <remarks>
        /// Key: 客户端 ID(通常是 IP:Port)
        /// Value: TcpClient 连接对象
        /// </remarks>
        public readonly Dictionary<string, TcpClient> _clients = new();
 
        /// <summary>
        /// 设备绑定字典
        /// </summary>
        /// <remarks>
        /// Key: 设备 ID
        /// Value: 客户端 ID
        /// 用于通过设备 ID 找到对应的客户端连接。
        /// </remarks>
        public readonly Dictionary<string, string> _deviceBindings = new();
 
        /// <summary>
        /// 客户端锁字典
        /// </summary>
        /// <remarks>
        /// 每个客户端一个 SemaphoreSlim,确保同一客户端的发送操作互斥。
        /// </remarks>
        public readonly Dictionary<string, SemaphoreSlim> _clientLocks = new();
 
        /// <summary>
        /// 客户端编码字典
        /// </summary>
        /// <remarks>
        /// 记录每个客户端使用的字符编码。
        /// 支持自动检测:UTF-8 或 GBK。
        /// </remarks>
        public readonly Dictionary<string, Encoding> _clientEncodings = new();
 
        /// <summary>
        /// 客户端最后活跃时间字典
        /// </summary>
        /// <remarks>
        /// 记录每个客户端最后一次活动的时间。
        /// 用于空闲超时检测。
        /// </remarks>
        public readonly Dictionary<string, DateTime> _clientLastActive = new();
 
        /// <summary>
        /// 默认文本编码
        /// </summary>
        public readonly Encoding _textEncoding;
 
        /// <summary>
        /// 自动检测的 GBK 编码
        /// </summary>
        public readonly Encoding? _autoDetectedGb2312;
 
        /// <summary>
        /// 日志文件路径
        /// </summary>
        private readonly string _logFile;
 
        /// <summary>
        /// 客户端监控任务
        /// </summary>
        private Task? _monitorTask;
 
        /// <summary>
        /// 服务器是否正在运行
        /// </summary>
        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");
        }
 
        /// <summary>
        /// 记录日志
        /// </summary>
        /// <remarks>
        /// 将消息输出到控制台并写入日志文件。
        /// </remarks>
        /// <param name="message">日志消息</param>
        private void Log(string message)
        {
            Console.WriteLine(message);
            try { File.AppendAllText(_logFile, message + Environment.NewLine); } catch { }
        }
    }
}