HubClient.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. using System.Buffers;
  2. using System.Net.WebSockets;
  3. using System.Text;
  4. using System.Text.Json;
  5. using Serilog;
  6. using YZWater.Core.Models;
  7. namespace YZWater.Core.Services;
  8. /// <summary>
  9. /// 中控客户端 - 连接到服务端,接收数据,发送命令,自动重连
  10. /// </summary>
  11. public class HubClient : IDisposable
  12. {
  13. private ClientWebSocket? _ws;
  14. private CancellationTokenSource? _cts;
  15. private Task? _connectionTask;
  16. private bool _disposed;
  17. private volatile bool _isConnected;
  18. private volatile bool _shouldReconnect = true;
  19. /// <summary>
  20. /// 服务端地址
  21. /// </summary>
  22. public string ServerUrl { get; set; } = "ws://localhost:8765";
  23. /// <summary>
  24. /// 认证令牌
  25. /// </summary>
  26. public string Token { get; set; } = string.Empty;
  27. /// <summary>
  28. /// 是否忽略证书验证(仅用于自签名证书的测试环境)
  29. /// </summary>
  30. public bool IgnoreCertificateErrors { get; set; } = false;
  31. /// <summary>
  32. /// 是否已连接
  33. /// </summary>
  34. public bool IsConnected => _isConnected;
  35. /// <summary>
  36. /// 是否有操作锁
  37. /// </summary>
  38. public bool HasLock { get; private set; }
  39. /// <summary>
  40. /// 当前操作员
  41. /// </summary>
  42. public string? CurrentOperator { get; private set; }
  43. public event Action<PlcDataModel>? DataReceived;
  44. public event Action<AlarmRecord>? AlarmReceived;
  45. public event Action<bool, string?>? LockStatusChanged;
  46. public event Action<bool>? ConnectionChanged;
  47. public event Action<int>? ClientCountChanged;
  48. public event Action<bool, string>? CommandResult;
  49. /// <summary>
  50. /// 连接到服务端(带自动重连)
  51. /// </summary>
  52. public async Task ConnectAsync()
  53. {
  54. if (_isConnected) return;
  55. _cts = new CancellationTokenSource();
  56. _shouldReconnect = true;
  57. await ConnectInternalAsync();
  58. // 启动接收 + 重连循环
  59. _connectionTask = Task.Run(() => ConnectionLoopAsync(_cts.Token));
  60. }
  61. /// <summary>
  62. /// 断开连接
  63. /// </summary>
  64. public async Task DisconnectAsync()
  65. {
  66. _shouldReconnect = false;
  67. _cts?.Cancel();
  68. if (_ws?.State == WebSocketState.Open)
  69. {
  70. try
  71. {
  72. await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "客户端断开", CancellationToken.None);
  73. }
  74. catch { /* 断开清理中,忽略关闭异常 */ }
  75. }
  76. _isConnected = false;
  77. HasLock = false;
  78. CurrentOperator = null;
  79. ConnectionChanged?.Invoke(false);
  80. Log.Information("已断开中控服务端连接");
  81. }
  82. /// <summary>
  83. /// 连接 + 接收 + 自动重连主循环
  84. /// </summary>
  85. private async Task ConnectionLoopAsync(CancellationToken ct)
  86. {
  87. var reconnectDelay = 1000;
  88. const int maxDelay = 30000;
  89. while (!ct.IsCancellationRequested && _shouldReconnect)
  90. {
  91. try
  92. {
  93. if (!_isConnected)
  94. {
  95. await ConnectInternalAsync();
  96. reconnectDelay = 1000; // 连接成功,重置延迟
  97. }
  98. // 进入接收循环
  99. await ReceiveLoopAsync(ct);
  100. }
  101. catch (OperationCanceledException)
  102. {
  103. break;
  104. }
  105. catch (Exception ex)
  106. {
  107. Log.Warning(ex, "连接异常");
  108. }
  109. // 连接断开,自动重连
  110. if (!ct.IsCancellationRequested && _shouldReconnect)
  111. {
  112. _isConnected = false;
  113. HasLock = false;
  114. ConnectionChanged?.Invoke(false);
  115. Log.Information("将在 {Delay}ms 后重连...", reconnectDelay);
  116. try
  117. {
  118. await Task.Delay(reconnectDelay, ct);
  119. }
  120. catch (OperationCanceledException)
  121. {
  122. break;
  123. }
  124. reconnectDelay = Math.Min(reconnectDelay * 2, maxDelay);
  125. }
  126. }
  127. }
  128. /// <summary>
  129. /// 内部连接方法
  130. /// </summary>
  131. private async Task ConnectInternalAsync()
  132. {
  133. _ws?.Dispose();
  134. _ws = new ClientWebSocket();
  135. // 如果启用忽略证书验证(自签名证书场景)
  136. if (IgnoreCertificateErrors && ServerUrl.StartsWith("wss://"))
  137. {
  138. _ws.Options.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true;
  139. Log.Warning("已忽略 TLS 证书验证(仅用于测试环境)");
  140. }
  141. try
  142. {
  143. await _ws.ConnectAsync(new Uri(ServerUrl), _cts?.Token ?? CancellationToken.None);
  144. // 发送认证令牌
  145. if (!string.IsNullOrEmpty(Token))
  146. {
  147. await SendMessageAsync(HubMessage.Auth(Token));
  148. // 等待认证结果
  149. var authResult = await ReceiveOneMessageAsync(_cts?.Token ?? CancellationToken.None);
  150. if (authResult?.Type != "auth_result" || authResult.GetPayload<bool>() != true)
  151. {
  152. Log.Warning("中控服务端认证失败");
  153. _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "认证失败", CancellationToken.None).Wait(1000);
  154. throw new Exception("认证失败");
  155. }
  156. Log.Information("中控服务端认证成功");
  157. }
  158. _isConnected = true;
  159. ConnectionChanged?.Invoke(true);
  160. Log.Information("已连接到中控服务端: {Url}", ServerUrl);
  161. }
  162. catch (Exception ex)
  163. {
  164. Log.Warning(ex, "连接中控服务端失败: {Url}", ServerUrl);
  165. _isConnected = false;
  166. throw;
  167. }
  168. }
  169. /// <summary>
  170. /// 请求操作锁
  171. /// </summary>
  172. public async Task RequestLockAsync(string operatorName)
  173. {
  174. await SendMessageAsync(new HubMessage
  175. {
  176. Type = "command",
  177. Payload = JsonSerializer.SerializeToElement(new CommandPayload
  178. {
  179. Action = "request_lock",
  180. Operator = operatorName
  181. })
  182. });
  183. }
  184. /// <summary>
  185. /// 释放操作锁
  186. /// </summary>
  187. public async Task ReleaseLockAsync()
  188. {
  189. await SendMessageAsync(new HubMessage
  190. {
  191. Type = "command",
  192. Payload = JsonSerializer.SerializeToElement(new CommandPayload
  193. {
  194. Action = "release_lock",
  195. Operator = CurrentOperator ?? ""
  196. })
  197. });
  198. }
  199. /// <summary>
  200. /// 发送控制命令
  201. /// </summary>
  202. public async Task SendCommandAsync(string action, string? target = null, object? value = null, string? operatorName = null)
  203. {
  204. await SendMessageAsync(new HubMessage
  205. {
  206. Type = "command",
  207. Payload = JsonSerializer.SerializeToElement(new CommandPayload
  208. {
  209. Action = action,
  210. Target = target,
  211. Value = value,
  212. Operator = operatorName ?? CurrentOperator ?? "unknown"
  213. })
  214. });
  215. }
  216. /// <summary>
  217. /// 发送消息
  218. /// </summary>
  219. private async Task SendMessageAsync(HubMessage message)
  220. {
  221. if (_ws?.State != WebSocketState.Open)
  222. {
  223. Log.Warning("未连接到服务端,无法发送消息");
  224. return;
  225. }
  226. var bytes = Encoding.UTF8.GetBytes(message.ToJson());
  227. await _ws.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None);
  228. }
  229. /// <summary>
  230. /// 接收单条消息(用于认证等握手场景)
  231. /// </summary>
  232. private async Task<HubMessage?> ReceiveOneMessageAsync(CancellationToken ct)
  233. {
  234. if (_ws?.State != WebSocketState.Open) return null;
  235. var buffer = new byte[65536];
  236. var messageBuffer = new MemoryStream();
  237. WebSocketReceiveResult result;
  238. do
  239. {
  240. result = await _ws.ReceiveAsync(new ArraySegment<byte>(buffer), ct);
  241. if (result.MessageType == WebSocketMessageType.Close) return null;
  242. messageBuffer.Write(buffer, 0, result.Count);
  243. }
  244. while (!result.EndOfMessage);
  245. if (result.MessageType != WebSocketMessageType.Text) return null;
  246. var json = Encoding.UTF8.GetString(messageBuffer.ToArray());
  247. return HubMessage.FromJson(json);
  248. }
  249. /// <summary>
  250. /// 接收消息循环(带分片处理,使用 ArrayPool 复用缓冲区)
  251. /// </summary>
  252. private async Task ReceiveLoopAsync(CancellationToken ct)
  253. {
  254. var buffer = ArrayPool<byte>.Shared.Rent(65536);
  255. var messageBuffer = new MemoryStream();
  256. try
  257. {
  258. while (_ws?.State == WebSocketState.Open && !ct.IsCancellationRequested)
  259. {
  260. messageBuffer.SetLength(0); // 复用 MemoryStream
  261. WebSocketReceiveResult result;
  262. do
  263. {
  264. result = await _ws.ReceiveAsync(new ArraySegment<byte>(buffer), ct);
  265. if (result.MessageType == WebSocketMessageType.Close)
  266. {
  267. return; // 触发重连
  268. }
  269. messageBuffer.Write(buffer, 0, result.Count);
  270. }
  271. while (!result.EndOfMessage);
  272. if (result.MessageType == WebSocketMessageType.Text)
  273. {
  274. var json = Encoding.UTF8.GetString(messageBuffer.ToArray());
  275. var message = HubMessage.FromJson(json);
  276. if (message != null)
  277. {
  278. HandleMessage(message);
  279. }
  280. }
  281. }
  282. }
  283. finally
  284. {
  285. ArrayPool<byte>.Shared.Return(buffer);
  286. messageBuffer.Dispose();
  287. }
  288. }
  289. /// <summary>
  290. /// 处理接收到的消息
  291. /// </summary>
  292. private void HandleMessage(HubMessage message)
  293. {
  294. switch (message.Type)
  295. {
  296. case "data":
  297. var data = message.GetPayload<PlcDataModel>();
  298. if (data != null) DataReceived?.Invoke(data);
  299. break;
  300. case "alarm":
  301. var alarm = message.GetPayload<AlarmRecord>();
  302. if (alarm != null) AlarmReceived?.Invoke(alarm);
  303. break;
  304. case "lock_result":
  305. var lockResult = message.GetPayload<LockResultPayload>();
  306. if (lockResult != null)
  307. {
  308. HasLock = lockResult.Success;
  309. CurrentOperator = lockResult.CurrentOperator;
  310. LockStatusChanged?.Invoke(lockResult.Success, lockResult.CurrentOperator);
  311. }
  312. break;
  313. case "lock_status":
  314. var lockStatus = message.GetPayload<LockStatusPayload>();
  315. if (lockStatus != null)
  316. {
  317. LockStatusChanged?.Invoke(lockStatus.IsLocked, lockStatus.CurrentOperator);
  318. }
  319. break;
  320. case "command_result":
  321. var cmdResult = message.GetPayload<CommandResultPayload>();
  322. if (cmdResult != null)
  323. {
  324. CommandResult?.Invoke(cmdResult.Success, cmdResult.Message);
  325. }
  326. break;
  327. case "client_count":
  328. var count = message.Payload?.GetInt32();
  329. if (count.HasValue) ClientCountChanged?.Invoke(count.Value);
  330. break;
  331. }
  332. }
  333. public void Dispose()
  334. {
  335. if (_disposed) return;
  336. _disposed = true;
  337. Log.Information("HubClient 正在释放... _shouldReconnect={Reconnect}, _connectionTask={HasTask}",
  338. _shouldReconnect, _connectionTask != null);
  339. _shouldReconnect = false;
  340. _cts?.Cancel();
  341. if (_ws?.State == WebSocketState.Open)
  342. {
  343. try
  344. {
  345. _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "关闭", CancellationToken.None).Wait(1000);
  346. }
  347. catch { /* 断开清理中,忽略关闭异常 */ }
  348. }
  349. _ws?.Dispose();
  350. try
  351. {
  352. var completed = _connectionTask?.Wait(TimeSpan.FromSeconds(2));
  353. if (completed == true)
  354. Log.Information("HubClient 连接任务已完成");
  355. else
  356. Log.Warning("HubClient 连接任务在2秒内未完成");
  357. }
  358. catch (AggregateException ex) when (ex.InnerExceptions.All(e => e is OperationCanceledException))
  359. {
  360. // 取消是正常退出路径。
  361. Log.Information("HubClient 连接任务已取消(正常)");
  362. }
  363. _connectionTask = null;
  364. _cts?.Dispose();
  365. Log.Information("HubClient 已释放");
  366. }
  367. }