1. 程式人生 > >C#之實現Scoket心跳機制

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;
        }
    }
---------------------
作者:PassionY
來源:CSDN
原文:https://blog.csdn.net/yupu56/article/details/72356700
版權宣告:本文為博主原創文章,轉載請附上博文連結!