C# 实现Scoket心跳机制的方法
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; } }
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: Thinkphp无限级分类代码
下一篇: 微信小程序实现图片预加载组件