C# 實(shí)現(xiàn)Scoket心跳機(jī)制的方法
TCP網(wǎng)絡(luò)長(zhǎng)連接
手機(jī)能夠使用聯(lián)網(wǎng)功能是因?yàn)槭謾C(jī)底層實(shí)現(xiàn)了TCP/IP協(xié)議,可以使手機(jī)終端通過(guò)無(wú)線網(wǎng)絡(luò)建立TCP連接。TCP協(xié)議可以對(duì)上層網(wǎng)絡(luò)提供接口,使上層網(wǎng)絡(luò)數(shù)據(jù)的傳輸建立在“無(wú)差別”的網(wǎng)絡(luò)之上。
建立起一個(gè)TCP連接需要經(jīng)過(guò)“三次握手”:
第一次握手:客戶(hù)端發(fā)送syn包(syn=j)到服務(wù)器,并進(jìn)入SYN_SEND狀態(tài),等待服務(wù)器確認(rèn);
第二次握手:服務(wù)器收到syn包,必須確認(rèn)客戶(hù)的SYN(ack=j+1),同時(shí)自己也發(fā)送一個(gè)SYN包(syn=k),即SYN+ACK包,此時(shí)服務(wù)器進(jìn)入SYN_RECV狀態(tài);
第三次握手:客戶(hù)端收到服務(wù)器的SYN+ACK包,向服務(wù)器發(fā)送確認(rèn)包ACK(ack=k+1),此包發(fā)送完畢,客戶(hù)端和服務(wù)器進(jìn)入ESTABLISHED狀態(tài),完成三次握手。
握手過(guò)程中傳送的包里不包含數(shù)據(jù),三次握手完畢后,客戶(hù)端與服務(wù)器才正式開(kāi)始傳送數(shù)據(jù)。理想狀態(tài)下,TCP連接一旦建立,在通信雙方中的任何一方主動(dòng)關(guān)閉連接之前,TCP 連接都將被一直保持下去。斷開(kāi)連接時(shí)服務(wù)器和客戶(hù)端均可以主動(dòng)發(fā)起斷開(kāi)TCP連接的請(qǐng)求,斷開(kāi)過(guò)程需要經(jīng)過(guò)“四次握手”(過(guò)程就不細(xì)寫(xiě)了,就是服務(wù)器和客戶(hù)端交互,最終確定斷開(kāi))
什么是心跳
剛才說(shuō)到長(zhǎng)連接建立連接后,理想狀態(tài)下是不會(huì)斷開(kāi)的,但是由于網(wǎng)絡(luò)問(wèn)題,可能導(dǎo)致一方斷開(kāi)后,另一方仍然在發(fā)送數(shù)據(jù),或者有些客戶(hù)端長(zhǎng)時(shí)間不發(fā)送消息,服務(wù)器還維持這他的客戶(hù)端不必要的引用,增加了服務(wù)器的負(fù)荷。因此我們引入了心跳機(jī)制。
心跳包之所以叫心跳包是因?yàn)椋核裥奶粯用扛艄潭〞r(shí)間發(fā)一次,以此來(lái)告訴服務(wù)器,這個(gè)客戶(hù)端還活著。事實(shí)上這是為了保持長(zhǎng)連接,至于這個(gè)包的內(nèi)容,是沒(méi)有什么特別規(guī)定的,不過(guò)一般都是很小的包,或者只包含包頭的一個(gè)空包。
總的來(lái)說(shuō),心跳包主要也就是用于長(zhǎng)連接的?;詈蛿嗑€處理。一般的應(yīng)用下,判定時(shí)間在30-40秒比較不錯(cuò)。如果實(shí)在要求高,那就在6-9秒。
怎么發(fā)送心跳?
心跳包的發(fā)送,通常有兩種技術(shù)
方法1:應(yīng)用層自己實(shí)現(xiàn)的心跳包
由應(yīng)用程序自己發(fā)送心跳包來(lái)檢測(cè)連接是否正常,大致的方法是:服務(wù)器在一個(gè) Timer事件中定時(shí) 向客戶(hù)端發(fā)送一個(gè)短小精悍的數(shù)據(jù)包,然后啟動(dòng)一個(gè)低級(jí)別的線程,在該線程中不斷檢測(cè)客戶(hù)端的回應(yīng), 如果在一定時(shí)間內(nèi)沒(méi)有收到客戶(hù)端的回應(yīng),即認(rèn)為客戶(hù)端已經(jīng)掉線;同樣,如果客戶(hù)端在一定時(shí)間內(nèi)沒(méi) 有收到服務(wù)器的心跳包,則認(rèn)為連接不可用。
方法2:TCP的KeepAlive?;顧C(jī)制
因?yàn)橐紤]到一個(gè)服務(wù)器通常會(huì)連接多個(gè)客戶(hù)端,因此由用戶(hù)在應(yīng)用層自己實(shí)現(xiàn)心跳包,代碼較多 且稍顯復(fù)雜,而利用TCP/IP協(xié)議層為內(nèi)置的KeepAlive功能來(lái)實(shí)現(xiàn)心跳功能則簡(jiǎn)單得多。 不論是服務(wù)端還是客戶(hù)端,一方開(kāi)啟KeepAlive功能后,就會(huì)自動(dòng)在規(guī)定時(shí)間內(nèi)向?qū)Ψ桨l(fā)送心跳包, 而另一方在收到心跳包后就會(huì)自動(dòng)回復(fù),以告訴對(duì)方我仍然在線。 因?yàn)殚_(kāi)啟KeepAlive功能需要消耗額外的寬帶和流量,所以TCP協(xié)議層默認(rèn)并不開(kāi)啟KeepAlive功 能,盡管這微不足道,但在按流量計(jì)費(fèi)的環(huán)境下增加了費(fèi)用,另一方面,KeepAlive設(shè)置不合理時(shí)可能會(huì) 因?yàn)槎虝旱木W(wǎng)絡(luò)波動(dòng)而斷開(kāi)健康的TCP連接。并且,默認(rèn)的KeepAlive超時(shí)需要7,200,000 MilliSeconds, 即2小時(shí),探測(cè)次數(shù)為5次。對(duì)于很多服務(wù)端應(yīng)用程序來(lái)說(shuō),2小時(shí)的空閑時(shí)間太長(zhǎng)。因此,我們需要手工開(kāi)啟KeepAlive功能并設(shè)置合理的KeepAlive參數(shù)。
心跳檢測(cè)步驟:
1客戶(hù)端每隔一個(gè)時(shí)間間隔發(fā)生一個(gè)探測(cè)包給服務(wù)器
2客戶(hù)端發(fā)包時(shí)啟動(dòng)一個(gè)超時(shí)定時(shí)器
3服務(wù)器端接收到檢測(cè)包,應(yīng)該回應(yīng)一個(gè)包
4如果客戶(hù)機(jī)收到服務(wù)器的應(yīng)答包,則說(shuō)明服務(wù)器正常,刪除超時(shí)定時(shí)器
5如果客戶(hù)端的超時(shí)定時(shí)器超時(shí),依然沒(méi)有收到應(yīng)答包,則說(shuō)明服務(wù)器掛了
C#實(shí)現(xiàn)的一個(gè)簡(jiǎn)單的心跳
using System;
using System.Collections.Generic;
using System.Threading;
namespace ConsoleApplication1
{
// 客戶(hù)端離線委托
public delegate void ClientOfflineHandler(ClientInfo client);
// 客戶(hù)端上線委托
public delegate void ClientOnlineHandler(ClientInfo client);
public class Program
{
/// <summary>
/// 客戶(hù)端離線提示
/// </summary>
/// <param name="clientInfo"></param>
private static void ClientOffline(ClientInfo clientInfo)
{
Console.WriteLine(String.Format("客戶(hù)端{(lán)0}離線,離線時(shí)間:\t{1}", clientInfo.ClientID, clientInfo.LastHeartbeatTime));
}
/// <summary>
/// 客戶(hù)端上線提示
/// </summary>
/// <param name="clientInfo"></param>
private static void ClientOnline(ClientInfo clientInfo)
{
Console.WriteLine(String.Format("客戶(hù)端{(lán)0}上線,上線時(shí)間:\t{1}", clientInfo.ClientID, clientInfo.LastHeartbeatTime));
}
static void Main()
{
// 服務(wù)端
Server server = new Server();
// 服務(wù)端離線事件
server.OnClientOffline += ClientOffline;
// 服務(wù)器上線事件
server.OnClientOnline += ClientOnline;
// 開(kāi)啟服務(wù)器
server.Start();
// 模擬100個(gè)客戶(hù)端
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);
// 開(kāi)啟客戶(hù)端
client.Start();
}
System.Threading.Thread.Sleep(1000);
while (true)
{
Console.WriteLine("請(qǐng)輸入要離線的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))
{
// 客戶(hù)端離線
client.Offline = true;
}
}
else
{
return;
}
}
}
}
}
/// <summary>
/// 服務(wù)端
/// </summary>
public class Server
{
public event ClientOfflineHandler OnClientOffline;
public event ClientOnlineHandler OnClientOnline;
private Dictionary<Int32, ClientInfo> _DicClient;
/// <summary>
/// 構(gòu)造函數(shù)
/// </summary>
public Server()
{
_DicClient = new Dictionary<Int32, ClientInfo>(100);
}
/// <summary>
/// 開(kāi)啟服務(wù)端
/// </summary>
public void Start()
{
// 開(kāi)啟掃描離線線程
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];
// 如果已經(jīng)離線則不用管
if (!clientInfo.State)
{
continue;
}
// 判斷最后心跳時(shí)間是否大于3秒
TimeSpan sp = System.DateTime.Now - clientInfo.LastHeartbeatTime;
if (sp.Seconds >= 3)
{
// 離線,觸發(fā)離線事件
if (OnClientOffline != null)
{
OnClientOffline(clientInfo);
}
// 修改狀態(tài)
clientInfo.State = false;
}
}
}
}
}
/// <summary>
/// 接收心跳包
/// </summary>
/// <param name="clientID">客戶(hù)端ID</param>
public void ReceiveHeartbeat(Int32 clientID)
{
lock (_DicClient)
{
ClientInfo clientInfo;
if (_DicClient.TryGetValue(clientID, out clientInfo))
{
// 如果客戶(hù)端已經(jīng)上線,則更新最后心跳時(shí)間
clientInfo.LastHeartbeatTime = System.DateTime.Now;
}
else
{
// 客戶(hù)端不存在,則認(rèn)為是新上線的
clientInfo = new ClientInfo();
clientInfo.ClientID = clientID;
clientInfo.LastHeartbeatTime = System.DateTime.Now;
clientInfo.State = true;
_DicClient.Add(clientID, clientInfo);
// 觸發(fā)上線事件
if (OnClientOnline != null)
{
OnClientOnline(clientInfo);
}
}
}
}
}
/// <summary>
/// 客戶(hù)端
/// </summary>
public class Client
{
public Server Server;
public Int32 ClientID;
public Boolean Offline;
/// <summary>
/// 構(gòu)造函數(shù)
/// </summary>
/// <param name="clientID"></param>
/// <param name="server"></param>
public Client(Int32 clientID, Server server)
{
ClientID = clientID;
Server = server;
Offline = false;
}
/// <summary>
/// 開(kāi)啟客戶(hù)端
/// </summary>
public void Start()
{
// 開(kāi)啟心跳線程
Thread t = new Thread(new ThreadStart(Heartbeat));
t.IsBackground = true;
t.Start();
}
/// <summary>
/// 向服務(wù)器發(fā)送心跳包
/// </summary>
private void Heartbeat()
{
while (!Offline)
{
// 向服務(wù)端發(fā)送心跳包
Server.ReceiveHeartbeat(ClientID);
System.Threading.Thread.Sleep(1000);
}
}
}
/// <summary>
/// 客戶(hù)端信息
/// </summary>
public class ClientInfo
{
// 客戶(hù)端ID
public Int32 ClientID;
// 最后心跳時(shí)間
public DateTime LastHeartbeatTime;
// 狀態(tài)
public Boolean State;
}
}
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
RegexOptions.IgnoreCase正則表達(dá)式替換,忽略大小寫(xiě)
RegexOptions.IgnoreCase正則表達(dá)式替換,忽略大小寫(xiě),需要的朋友可以參考一下2013-03-03
C#?二進(jìn)制序列化和反序列化的具體實(shí)現(xiàn)
本文主要介紹了C#?二進(jìn)制序列化和反序列化的具體實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06
C#中DataTable和List互轉(zhuǎn)的示例代碼
很多場(chǎng)景下,我們需要將List轉(zhuǎn)換成為DataTable,本文主要介紹了C#中DataTable和List互轉(zhuǎn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04
WinForm實(shí)現(xiàn)仿視頻播放器左下角滾動(dòng)新聞效果的方法
這篇文章主要介紹了WinForm實(shí)現(xiàn)仿視頻播放器左下角滾動(dòng)新聞效果的方法,涉及WinForm窗口滾動(dòng)字幕設(shè)置的實(shí)現(xiàn)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-08-08
C#正則表達(dá)式獲取下拉菜單(select)的相關(guān)屬性值
這篇文章主要介紹了C#正則表達(dá)式獲取下拉菜單(select)的相關(guān)屬性值,比如可以獲得name屬性的值、value值、指定值,需要的朋友可以參考下2014-07-07

