拌合楼软件开发(23)监测客户端在线情况并联动企业微信提醒客户端离线和恢复
需求分析:
项目过程中,客户端通过将车牌重量等数据post到服务端进行处理后生成相应的单据,如果没有网络那么就会出现异常情况,往往只有到客户过车发现系统不能用找过来,分析原因才发现是客户端处断网了。这不仅增加了很多不必要的沟通成本,同时也让客户对系统的稳定性产生一些质疑。因此决定开发一个检测客户端是否离线的功能,如果离线,那么服务端主动推送企业微信群消息给服务人员群中,这样服务跟进人员可以主动与客户沟通。
实现的路径:
客户端每5分钟发送一个心跳包给到服务端,服务端记录下来心跳包到达的时间,服务端定时扫描客户端心跳包发送的时间,如果服务端记录的心跳包时间与当前时间差在30分中以上,那么表明客户端已经掉线,把客户端标记为离线,并发送消息,消息发送提醒10次。等到接收到客户端的心跳包时候,先判断客户端是否被标记为离线了,那么取消离线标识,并发送消息提醒恢复了。
一、需要搭建一个服务端接收客户端心跳包
实现心跳包接收的机制很多,一般的思路是服务端监听端口,客户端定时连接改端口,并发送约定的心跳包数据来证明自己存活。作者偷个懒,就用.net 写了个心跳处理的api接口来做这个事情。
二、服务端心跳包处理
1. 心跳包的数据内容:
客户端请求API接口,就一个clientId来标识自己的身份就可以了,服务器端就以接口接收到数据的时间来作为心跳包的时间,clientId 为免重复,用了guid,用什么格式这个读者按照自己的业务需求来定了。
2. 心跳包接收处理:
作者用到了redis来做数据的缓存了,mysql的数据库中表记录clientId信息,每次客户端心跳请求,那么先查询该clientId是否在表中有记录,如果没有记录表明是非法数据集,直接丢弃了,如果有记录再做后续的处理。
[HttpGet]public async Task<IActionResult> Get(string clientId){if (string.IsNullOrEmpty(clientId)){_logger.LogWarning("没有收到心跳包");return BadRequest("没有收到心跳包");}//var cacheData = await _redis.GetStringAsync(clientId);var client = GetClientList.ClientList.Where(u => u.clientId == clientId).FirstOrDefault();if (client == null){_logger.LogWarning($"心跳包数据不正确:{clientId}");return BadRequest("心跳包数据不正确");}var times = await _redis.GetStringAsync(clientId + "-offline");if (!string.IsNullOrEmpty(times)){await _redis.RemoveAsync(clientId + "-offline");SendOnlineNotificationAsync(client);}await _redis.SetStringAsync(clientId, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));return Ok(clientId);}
需要传递的数据很少,所以直接用GET就行了。
(1)先判断是否有传递clientId的参数,如果没有丢弃。
(2)判断clientId的参数是否在mysql库中,如果没有,丢弃
(3)查询redis中是否有key为 clientId +“-offline” 的数据,如果有那么表明客户端是已经离线的,那么移除该键值,并推送该客户端已经恢复上线的通知。
(4)记录下来一组值,key为clientId, 值为当前时间
3. 服务端定时轮询的处理
如何实现定时任务,不是本文的重点,主要讲实现的需求的逻辑。
public async Task Execute(IJobExecutionContext context){_logger.LogInformation("开始检测客户端的连接状态");foreach (var client in GetClientList.ClientList){var cacheData = await _redis.GetStringAsync(client.clientId);if (!string.IsNullOrEmpty(cacheData)){//如果缓存不是空DateTime lastAccessTime = DateTime.Parse(cacheData);if ((DateTime.Now - lastAccessTime).TotalMinutes > int.Parse(_config.GetSection("lostConnectTime").Value)){//如果缓存时间超过5分钟_logger.LogWarning($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} 客户端{client.clientId}, {client.clientName}已经断开连接,最后一次链接时间{cacheData}");string times = await _redis.GetStringAsync(client.clientId + "-offline");if (!string.IsNullOrEmpty(times)){await _redis.SetStringAsync(client.clientId + "-offline", (int.Parse(times) + 1).ToString());//只推送10次离线提醒if (int.Parse(times) <= int.Parse(_config.GetSection("notifyTimes").Value)){SendOfflineNotificationAsync(client, cacheData);}}else{await _redis.SetStringAsync(client.clientId + "-offline", "1");SendOfflineNotificationAsync(client, cacheData);}}}}
(1) Mysql表中记录有所有的客户端clientId信息,那么for循环查询所有redis中所有客户端的记录信息。
(2)查询redis中是否包含clientId为key的值,如果没有表明系统可以能刚启动,如果有
(3)该key中记录的是上一次心跳包达到服务端时间,如果与当前时间比较超过阈值,那么就标记客户端是离线。
(4)查询redis中是否存在 clientId+ “-offline” 为key的值, 如果不存在表明为第一次掉线,那么设置该键的值为1,并推送消息。
(5)如果存在,先查询存储的值,如果值大于设定提醒此次,那么就不发消息,如果没有那么推送消息,并将该值+1,存储回去。
三、客户端发送数据
客户端发送数据很简单,使用HttpClient发送数据。
string baseUrl = "https://xxxxx/api/heartData"; string clientI clientId = "your_client_id_here";// 创建HttpClient实例using (HttpClient client = new HttpClient()){try{// 构造带参数的URLstring requestUrl = $"{baseUrl}?clientId={Uri.EscapeDataString(clientId)}";// 发送GET请求HttpResponseMessage response = await client.GetAsync(requestUrl);// 检查响应状态if (response.IsSuccessStatusCode){// 读取响应内容string responseBody = await response.Content.ReadAsStringAsync();Console.WriteLine("Response: " + responseBody);}else{Console.WriteLine($"Error: {response.StatusCode}");}}catch (Exception ex){Console.WriteLine($"Exception: {ex.Message}");}
到此整个功能基本上就实现了。