拌合楼软件开发(23)监测客户端在线情况并联动企业微信提醒客户端离线和恢复

需求分析:

项目过程中,客户端通过将车牌重量等数据post到服务端进行处理后生成相应的单据,如果没有网络那么就会出现异常情况,往往只有到客户过车发现系统不能用找过来,分析原因才发现是客户端处断网了。这不仅增加了很多不必要的沟通成本,同时也让客户对系统的稳定性产生一些质疑。因此决定开发一个检测客户端是否离线的功能,如果离线,那么服务端主动推送企业微信群消息给服务人员群中,这样服务跟进人员可以主动与客户沟通。

实现的路径:

客户端每5分钟发送一个心跳包给到服务端,服务端记录下来心跳包到达的时间,服务端定时扫描客户端心跳包发送的时间,如果服务端记录的心跳包时间与当前时间差在30分中以上,那么表明客户端已经掉线,把客户端标记为离线,并发送消息,消息发送提醒10次。等到接收到客户端的心跳包时候,先判断客户端是否被标记为离线了,那么取消离线标识,并发送消息提醒恢复了。

一、需要搭建一个服务端接收客户端心跳包

实现心跳包接收的机制很多,一般的思路是服务端监听端口,客户端定时连接改端口,并发送约定的心跳包数据来证明自己存活。作者偷个懒,就用.net 写了个心跳处理的api接口来做这个事情。

二、服务端心跳包处理

1. 心跳包的数据内容:

客户端请求API接口,就一个clientId来标识自己的身份就可以了,服务器端就以接口接收到数据的时间来作为心跳包的时间,clientId 为免重复,用了guid,用什么格式这个读者按照自己的业务需求来定了。

2. 心跳包接收处理:

作者用到了redis来做数据的缓存了,mysql的数据库中表记录clientId信息,每次客户端心跳请求,那么先查询该clientId是否在表中有记录,如果没有记录表明是非法数据集,直接丢弃了,如果有记录再做后续的处理。

cs 复制代码
   [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. 服务端定时轮询的处理

如何实现定时任务,不是本文的重点,主要讲实现的需求的逻辑。

cs 复制代码
 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发送数据。

cs 复制代码
    string baseUrl = "https://xxxxx/api/heartData";        
    string clientI    clientId  = "your_client_id_here";
        // 创建HttpClient实例
        using (HttpClient client = new HttpClient())
        {
            try
            {
                // 构造带参数的URL
                string 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}");
            }

到此整个功能基本上就实现了。