C# 实现Scoket心跳机制的方法

 更新时间:2020年6月25日 10:35  点击:1288

TCP网络长连接

手机能够使用联网功能是因为手机底层实现了TCP/IP协议,可以使手机终端通过无线网络建立TCP连接。TCP协议可以对上层网络提供接口,使上层网络数据的传输建立在“无差别”的网络之上。

建立起一个TCP连接需要经过“三次握手”:
第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。断开连接时服务器和客户端均可以主动发起断开TCP连接的请求,断开过程需要经过“四次握手”(过程就不细写了,就是服务器和客户端交互,最终确定断开)

什么是心跳

刚才说到长连接建立连接后,理想状态下是不会断开的,但是由于网络问题,可能导致一方断开后,另一方仍然在发送数据,或者有些客户端长时间不发送消息,服务器还维持这他的客户端不必要的引用,增加了服务器的负荷。因此我们引入了心跳机制。

心跳包之所以叫心跳包是因为:它像心跳一样每隔固定时间发一次,以此来告诉服务器,这个客户端还活着。事实上这是为了保持长连接,至于这个包的内容,是没有什么特别规定的,不过一般都是很小的包,或者只包含包头的一个空包。

总的来说,心跳包主要也就是用于长连接的保活和断线处理。一般的应用下,判定时间在30-40秒比较不错。如果实在要求高,那就在6-9秒。

怎么发送心跳?

心跳包的发送,通常有两种技术

方法1:应用层自己实现的心跳包 

由应用程序自己发送心跳包来检测连接是否正常,大致的方法是:服务器在一个 Timer事件中定时 向客户端发送一个短小精悍的数据包,然后启动一个低级别的线程,在该线程中不断检测客户端的回应, 如果在一定时间内没有收到客户端的回应,即认为客户端已经掉线;同样,如果客户端在一定时间内没 有收到服务器的心跳包,则认为连接不可用。

方法2:TCP的KeepAlive保活机制

因为要考虑到一个服务器通常会连接多个客户端,因此由用户在应用层自己实现心跳包,代码较多 且稍显复杂,而利用TCP/IP协议层为内置的KeepAlive功能来实现心跳功能则简单得多。 不论是服务端还是客户端,一方开启KeepAlive功能后,就会自动在规定时间内向对方发送心跳包, 而另一方在收到心跳包后就会自动回复,以告诉对方我仍然在线。 因为开启KeepAlive功能需要消耗额外的宽带和流量,所以TCP协议层默认并不开启KeepAlive功 能,尽管这微不足道,但在按流量计费的环境下增加了费用,另一方面,KeepAlive设置不合理时可能会 因为短暂的网络波动而断开健康的TCP连接。并且,默认的KeepAlive超时需要7,200,000 MilliSeconds, 即2小时,探测次数为5次。对于很多服务端应用程序来说,2小时的空闲时间太长。因此,我们需要手工开启KeepAlive功能并设置合理的KeepAlive参数。

心跳检测步骤:

1客户端每隔一个时间间隔发生一个探测包给服务器
2客户端发包时启动一个超时定时器
3服务器端接收到检测包,应该回应一个包
4如果客户机收到服务器的应答包,则说明服务器正常,删除超时定时器
5如果客户端的超时定时器超时,依然没有收到应答包,则说明服务器挂了

C#实现的一个简单的心跳

using System;
using System.Collections.Generic;
using System.Threading;
 
namespace ConsoleApplication1
{
  // 客户端离线委托
  public delegate void ClientOfflineHandler(ClientInfo client);
 
  // 客户端上线委托
  public delegate void ClientOnlineHandler(ClientInfo client);
 
  public class Program
  {
    /// <summary>
    /// 客户端离线提示
    /// </summary>
    /// <param name="clientInfo"></param>
    private static void ClientOffline(ClientInfo clientInfo)
    {
      Console.WriteLine(String.Format("客户端{0}离线,离线时间:\t{1}", clientInfo.ClientID, clientInfo.LastHeartbeatTime));
    }
 
    /// <summary>
    /// 客户端上线提示
    /// </summary>
    /// <param name="clientInfo"></param>
    private static void ClientOnline(ClientInfo clientInfo)
    {
      Console.WriteLine(String.Format("客户端{0}上线,上线时间:\t{1}", clientInfo.ClientID, clientInfo.LastHeartbeatTime));
    }
 
    static void Main()
    {
      // 服务端
      Server server = new Server();
 
      // 服务端离线事件
      server.OnClientOffline += ClientOffline;
 
      // 服务器上线事件
      server.OnClientOnline += ClientOnline;
 
      // 开启服务器
      server.Start();
 
      // 模拟100个客户端
      Dictionary<Int32, Client> dicClient = new Dictionary<Int32, Client>();
      for (Int32 i = 0; i < 100; i++)
      {
        // 这里传入server只是为了方便而已
        Client client = new Client(i + 1, server);
        dicClient.Add(i + 1, client);
 
        // 开启客户端
        client.Start();
      }
 
      System.Threading.Thread.Sleep(1000);
 
      while (true)
      {
        Console.WriteLine("请输入要离线的ClientID,输入0则退出程序:");
        String clientID = Console.ReadLine();
        if (!String.IsNullOrEmpty(clientID))
        {
          Int32 iClientID = 0;
          Int32.TryParse(clientID, out iClientID);
          if (iClientID > 0)
          {
            Client client;
            if (dicClient.TryGetValue(iClientID, out client))
            {
              // 客户端离线
              client.Offline = true;
            }
          }
          else
          {
            return;
          }
        }
      }
    }
  }
 
  /// <summary>
  /// 服务端
  /// </summary>
  public class Server
  {
    public event ClientOfflineHandler OnClientOffline;
    public event ClientOnlineHandler OnClientOnline;
 
    private Dictionary<Int32, ClientInfo> _DicClient;
 
    /// <summary>
    /// 构造函数
    /// </summary>
    public Server()
    {
      _DicClient = new Dictionary<Int32, ClientInfo>(100);      
    }
 
    /// <summary>
    /// 开启服务端
    /// </summary>
    public void Start()
    {
      // 开启扫描离线线程
      Thread t = new Thread(new ThreadStart(ScanOffline));
      t.IsBackground = true;
      t.Start();
    }
 
    /// <summary>
    /// 扫描离线
    /// </summary>
    private void ScanOffline()
    {
      while (true)
      {
        // 一秒扫描一次
        System.Threading.Thread.Sleep(1000);
 
        lock (_DicClient)
        {
          foreach (Int32 clientID in _DicClient.Keys)
          {
            ClientInfo clientInfo = _DicClient[clientID];
 
            // 如果已经离线则不用管
            if (!clientInfo.State)
            {
              continue;
            }
 
            // 判断最后心跳时间是否大于3秒
            TimeSpan sp = System.DateTime.Now - clientInfo.LastHeartbeatTime;
            if (sp.Seconds >= 3)
            {
              // 离线,触发离线事件
              if (OnClientOffline != null)
              {
                OnClientOffline(clientInfo);
              }
 
              // 修改状态
              clientInfo.State = false;
            }
          }
        }
      }
    }
 
    /// <summary>
    /// 接收心跳包
    /// </summary>
    /// <param name="clientID">客户端ID</param>
    public void ReceiveHeartbeat(Int32 clientID)
    {
      lock (_DicClient)
      {
        ClientInfo clientInfo;
        if (_DicClient.TryGetValue(clientID, out clientInfo))
        {
          // 如果客户端已经上线,则更新最后心跳时间
          clientInfo.LastHeartbeatTime = System.DateTime.Now;
        }
        else
        {
          // 客户端不存在,则认为是新上线的
          clientInfo = new ClientInfo();
          clientInfo.ClientID = clientID;
          clientInfo.LastHeartbeatTime = System.DateTime.Now;
          clientInfo.State = true;
 
          _DicClient.Add(clientID, clientInfo);
 
          // 触发上线事件
          if (OnClientOnline != null)
          {
            OnClientOnline(clientInfo);
          }
        }
      }
    }
  }
 
  /// <summary>
  /// 客户端
  /// </summary>
  public class Client
  {
    public Server Server;
    public Int32 ClientID;
    public Boolean Offline;
 
    /// <summary>
    /// 构造函数
    /// </summary>
    /// <param name="clientID"></param>
    /// <param name="server"></param>
    public Client(Int32 clientID, Server server)
    {
      ClientID = clientID;
      Server = server;
      Offline = false;
    }
 
    /// <summary>
    /// 开启客户端
    /// </summary>
    public void Start()
    {
      // 开启心跳线程
      Thread t = new Thread(new ThreadStart(Heartbeat));
      t.IsBackground = true;
      t.Start();
    }
 
    /// <summary>
    /// 向服务器发送心跳包
    /// </summary>
    private void Heartbeat()
    {
      while (!Offline)
      {
        // 向服务端发送心跳包
        Server.ReceiveHeartbeat(ClientID);
         
        System.Threading.Thread.Sleep(1000);
      }
    }
  }
 
  /// <summary>
  /// 客户端信息
  /// </summary>
  public class ClientInfo
  {
    // 客户端ID
    public Int32 ClientID;
 
    // 最后心跳时间
    public DateTime LastHeartbeatTime;
 
    // 状态
    public Boolean State;
  }
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持猪先飞。

[!--infotagslink--]

相关文章

  • C#实现简单的登录界面

    我们在使用C#做项目的时候,基本上都需要制作登录界面,那么今天我们就来一步步看看,如果简单的实现登录界面呢,本文给出2个例子,由简入难,希望大家能够喜欢。...2020-06-25
  • 浅谈C# 字段和属性

    这篇文章主要介绍了C# 字段和属性的的相关资料,文中示例代码非常详细,供大家参考和学习,感兴趣的朋友可以了解下...2020-11-03
  • C#中截取字符串的的基本方法详解

    这篇文章主要介绍了C#中截取字符串的的基本方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-11-03
  • C#实现简单的Http请求实例

    这篇文章主要介绍了C#实现简单的Http请求的方法,以实例形式较为详细的分析了C#实现Http请求的具体方法,需要的朋友可以参考下...2020-06-25
  • C#连接SQL数据库和查询数据功能的操作技巧

    本文给大家分享C#连接SQL数据库和查询数据功能的操作技巧,本文通过图文并茂的形式给大家介绍的非常详细,需要的朋友参考下吧...2021-05-17
  • C#中new的几种用法详解

    本文主要介绍了C#中new的几种用法,具有很好的参考价值,下面跟着小编一起来看下吧...2020-06-25
  • 使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序)

    这篇文章主要介绍了使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25
  • C#开发Windows窗体应用程序的简单操作步骤

    这篇文章主要介绍了C#开发Windows窗体应用程序的简单操作步骤,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-04-12
  • C#从数据库读取图片并保存的两种方法

    这篇文章主要介绍了C#从数据库读取图片并保存的方法,帮助大家更好的理解和使用c#,感兴趣的朋友可以了解下...2021-01-16
  • C#和JavaScript实现交互的方法

    最近做一个小项目不可避免的需要前端脚本与后台进行交互。由于是在asp.net中实现,故问题演化成asp.net中jiavascript与后台c#如何进行交互。...2020-06-25
  • C++调用C#的DLL程序实现方法

    本文通过例子,讲述了C++调用C#的DLL程序的方法,作出了以下总结,下面就让我们一起来学习吧。...2020-06-25
  • 轻松学习C#的基础入门

    轻松学习C#的基础入门,了解C#最基本的知识点,C#是一种简洁的,类型安全的一种完全面向对象的开发语言,是Microsoft专门基于.NET Framework平台开发的而量身定做的高级程序设计语言,需要的朋友可以参考下...2020-06-25
  • C#变量命名规则小结

    本文主要介绍了C#变量命名规则小结,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-09-09
  • C#绘制曲线图的方法

    这篇文章主要介绍了C#绘制曲线图的方法,以完整实例形式较为详细的分析了C#进行曲线绘制的具体步骤与相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下...2020-06-25
  • C# 中如何取绝对值函数

    本文主要介绍了C# 中取绝对值的函数。具有很好的参考价值。下面跟着小编一起来看下吧...2020-06-25
  • c#自带缓存使用方法 c#移除清理缓存

    这篇文章主要介绍了c#自带缓存使用方法,包括获取数据缓存、设置数据缓存、移除指定数据缓存等方法,需要的朋友可以参考下...2020-06-25
  • c#中(&&,||)与(&,|)的区别详解

    这篇文章主要介绍了c#中(&&,||)与(&,|)的区别详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-06-25
  • 经典实例讲解C#递归算法

    这篇文章主要用实例讲解C#递归算法的概念以及用法,文中代码非常详细,帮助大家更好的参考和学习,感兴趣的朋友可以了解下...2020-06-25
  • C#学习笔记- 随机函数Random()的用法详解

    下面小编就为大家带来一篇C#学习笔记- 随机函数Random()的用法详解。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25
  • C#中list用法实例

    这篇文章主要介绍了C#中list用法,结合实例形式分析了C#中list排序、运算、转换等常见操作技巧,具有一定参考借鉴价值,需要的朋友可以参考下...2020-06-25