socket心跳超時檢測,快速處理新思路(適用於超大量TCP連線情況下)
假設一種情景:TCP伺服器有1萬個客戶端連線,如果客戶端5秒鐘不發資料,則要斷開。服務端如何檢測客戶端是否超時?這看起來是一個非常簡單的問題,其實不然!
最簡單的處理方法是:啟動一個執行緒,每隔一段時間,檢查每個連線是否超時。每次處理需要1萬次檢查。計算量太大!檢查的時間間隔不能太小,否則大大增加計算量;如果間隔時間太大,超時誤差會增大。
本文提出一種新穎的處理方法,就是針對這個看似簡單而不易解決的問題!(以下用socket表示一個客戶端連線)
1 記憶體佈局圖
假設socket3有新的資料到達,需要更新socket3所在的時間軸,處理邏輯如下:
2 處理過程分析:
基本的處理思路就是增加時間軸概念。將socket按最後更新時間排序。因為時間是連續的,不可能將時間分割太細。首先將時間離散,比如屬於同一秒內的更新,被認為是屬於同一個時間點。離散的時間間隔稱為時間刻度,該刻度值可以根據具體情況調整。刻度值越小,超時計算越精確;但是計算量增大。如果時間刻度為10毫秒,則一秒的時間長度被劃分為100份。所以需要對更新時間做規整,程式碼如下:
DateTime CreateNow() { DateTime now = DateTime.Now; int m = 0; if(now.Millisecond != 0) { if(_minimumScaleOfMillisecond == 1000) { now = now.AddSeconds(1); //尾數加1,確保超時值大於 給定的值 } else { //如果now.Millisecond為16毫秒,精確度為10毫秒。則轉換後為20毫秒 m = now.Millisecond - now.Millisecond % _minimumScaleOfMillisecond + _minimumScaleOfMillisecond; if(m>=1000) { m -= 1000; now = now.AddSeconds(1); } } } return new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second,m); }
屬於同一個時間刻度的socket,被放入在一個雜湊表中(見圖中Group)。存放socket的類如下:
class SameTimeKeyGroup<T> { DateTime _timeStamp; public DateTime TimeStamp => _timeStamp; public SameTimeKeyGroup(DateTime time) { _timeStamp = time; } public HashSet<T> KeyGroup { get; set; } = new HashSet<T>(); public bool ContainKey(T key) { return KeyGroup.Contains(key); } internal void AddKey(T key) { KeyGroup.Add(key); } internal bool RemoveKey(T key) { return KeyGroup.Remove(key); } }
定義一個List表示時間軸:
List<SameTimeKeyGroup<T>> _listTimeScale = new List<SameTimeKeyGroup<T>>();
在_listTimeScale 前端的時間較舊,所以連結串列前端就是有可能超時的socket。
當有socket需要更新時,需要快速知道socket所在的group。這樣才能將socket從舊的group移走,再新增到新的group中。需要新增一個連結串列:
Dictionary<T, SameTimeKeyGroup<T>> _socketToSameTimeKeyGroup = new Dictionary<T, SameTimeKeyGroup<T>>();
2.1 當socket有新的資料到達時,處理步驟:
- 查詢socket的上一個群組。如果該群組對應的時刻和當前時刻相同(時間都已經離散,才有可能相同),無需更新時間軸。
- 從舊的群組刪除,增加到新的群組。
public void UpdateTime(T key) { DateTime now = CreateNow(); //是否已存在,從上一個時間群組刪除 if (_socketToSameTimeKeyGroup.ContainsKey(key)) { SameTimeKeyGroup<T> group = _socketToSameTimeKeyGroup[key]; if (group.ContainKey(key)) { if (group.TimeStamp == now) //同一時間更新,無需移動 { return; } else { group.RemoveKey(key); _socketToSameTimeKeyGroup.Remove(key); } } } //從超時組 刪除 _timeoutSocketGroup.Remove(key); //加入到新組 SameTimeKeyGroup<T> groupFromScaleList = GetOrCreateSocketGroup(now, out bool newCreate); groupFromScaleList.AddKey(key); _socketToSameTimeKeyGroup.Add(key, groupFromScaleList); if (newCreate) { AdjustTimeout(); } }
2.2 獲取超時的socket
時間軸從舊到新,對比群組的時間與超時時刻。就是連結串列_listTimeScale,從0開始查詢。
/// <summary> ///timeLimit 值為超時時刻限制 ///比如DateTime.Now.AddMilliseconds(-1000);表示 返回一秒鐘以前的資料 /// </summary> /// <param name="timeLimit">該時間以前的socket會被返回</param> /// <returns></returns> public List<T> GetTimeoutValue(DateTime timeLimit, bool remove = true) { if((DateTime.Now - timeLimit) > _maxSpan ) { Debug.Write("GetTimeoutSocket timeLimit 引數有誤!"); } //從超時組 讀取 List<T> result = new List<T>(); foreach(T key in _timeoutSocketGroup) { _timeoutSocketGroup.Add(key); } if(remove) { _timeoutSocketGroup.Clear(); } while (_listTimeScale.Count > 0) { //時間軸從舊到新,查詢對比 SameTimeKeyGroup<T> group = _listTimeScale[0]; if(timeLimit >= group.TimeStamp) { foreach (T key in group.KeyGroup) { result.Add(key); if (remove) { _socketToSameTimeKeyGroup.Remove(key); } } if(remove) { _listTimeScale.RemoveAt(0); } } else { break; } } return result; }
3 使用舉例
//建立變數。最大超時時間為600秒,時間刻度為1秒 TimeSpanManage<Socket> _deviceActiveManage = TimeSpanManage<Socket>.Create(TimeSpan.FromSeconds(600), 1000); //當有資料到達時,呼叫更新函式 _deviceActiveManage.UpdateTime(socket); //需要線上程或定時器中,每隔一段時間呼叫,找出超時的socket //找出超時時間超過600秒的socket。 foreach (Socket socket in _deviceActiveManage.GetTimeoutValue(DateTime.Now.AddSeconds(-600))) { socket.Close(); }
4 完整程式碼
1 /// <summary> 2 /// 超時時間 時間間隔處理 3 /// </summary> 4 class TimeSpanManage<T> 5 { 6 TimeSpan _maxSpan; 7 int _minimumScaleOfMillisecond; 8 int _scaleCount; 9 10 List<SameTimeKeyGroup<T>> _listTimeScale = new List<SameTimeKeyGroup<T>>(); 11 private TimeSpanManage() 12 { 13 } 14 15 /// <summary> 16 /// 17 /// </summary> 18 /// <param name="maxSpan">最大時間時間</param> 19 /// <param name="minimumScaleOfMillisecond">最小刻度(毫秒)</param> 20 /// <returns></returns> 21 public static TimeSpanManage<T> Create(TimeSpan maxSpan, int minimumScaleOfMillisecond) 22 { 23 if (minimumScaleOfMillisecond <= 0) 24 throw new Exception("minimumScaleOfMillisecond 小於0"); 25 if (minimumScaleOfMillisecond > 1000) 26 throw new Exception("minimumScaleOfMillisecond 不能大於1000"); 27 28 if (maxSpan.TotalMilliseconds <= 0) 29 throw new Exception("maxSpan.TotalMilliseconds 小於0"); 30 31 TimeSpanManage<T> result = new TimeSpanManage<T>(); 32 result._maxSpan = maxSpan; 33 result._minimumScaleOfMillisecond = minimumScaleOfMillisecond; 34 35 result._scaleCount = (int)(maxSpan.TotalMilliseconds / minimumScaleOfMillisecond); 36 result._scaleCount++; 37 return result; 38 } 39 40 Dictionary<T, SameTimeKeyGroup<T>> _socketToSameTimeKeyGroup = new Dictionary<T, SameTimeKeyGroup<T>>(); 41 public void UpdateTime(T key) 42 { 43 DateTime now = CreateNow(); 44 //是否已存在,從上一個時間群組刪除 45 if (_socketToSameTimeKeyGroup.ContainsKey(key)) 46 { 47 SameTimeKeyGroup<T> group = _socketToSameTimeKeyGroup[key]; 48 if (group.ContainKey(key)) 49 { 50 if (group.TimeStamp == now) //同一時間更新,無需移動 51 { 52 return; 53 } 54 else 55 { 56 group.RemoveKey(key); 57 _socketToSameTimeKeyGroup.Remove(key); 58 } 59 } 60 } 61 62 //從超時組 刪除 63 _timeoutSocketGroup.Remove(key); 64 65 //加入到新組 66 SameTimeKeyGroup<T> groupFromScaleList = GetOrCreateSocketGroup(now, out bool newCreate); 67 groupFromScaleList.AddKey(key); 68 69 _socketToSameTimeKeyGroup.Add(key, groupFromScaleList); 70 71 if (newCreate) 72 { 73 AdjustTimeout(); 74 } 75 } 76 77 public bool RemoveSocket(T key) 78 { 79 bool result = false; 80 if (_socketToSameTimeKeyGroup.ContainsKey(key)) 81 { 82 SameTimeKeyGroup<T> group = _socketToSameTimeKeyGroup[key]; 83 result = group.RemoveKey(key); 84 85 _socketToSameTimeKeyGroup.Remove(key); 86 } 87 88 //從超時組 刪除 89 bool result2 = _timeoutSocketGroup.Remove(key); 90 return result || result2; 91 } 92 93 /// <summary> 94 ///timeLimit 值為超時時刻限制 95 ///比如DateTime.Now.AddMilliseconds(-1000);表示 返回一秒鐘以前的資料 96 /// </summary> 97 /// <param name="timeLimit">該時間以前的socket會被返回</param> 98 /// <returns></returns> 99 public List<T> GetTimeoutValue(DateTime timeLimit, bool remove = true) 100 { 101 if((DateTime.Now - timeLimit) > _maxSpan ) 102 { 103 Debug.Write("GetTimeoutSocket timeLimit 引數有誤!"); 104 } 105 106 //從超時組 讀取 107 List<T> result = new List<T>(); 108 foreach(T key in _timeoutSocketGroup) 109 { 110 _timeoutSocketGroup.Add(key); 111 } 112 113 if(remove) 114 { 115 _timeoutSocketGroup.Clear(); 116 } 117 118 while (_listTimeScale.Count > 0) 119 { 120 //時間軸從舊到新,查詢對比 121 SameTimeKeyGroup<T> group = _listTimeScale[0]; 122 if(timeLimit >= group.TimeStamp) 123 { 124 foreach (T key in group.KeyGroup) 125 { 126 result.Add(key); 127 if (remove) 128 { 129 _socketToSameTimeKeyGroup.Remove(key); 130 } 131 } 132 133 if(remove) 134 { 135 _listTimeScale.RemoveAt(0); 136 } 137 } 138 else 139 { 140 break; 141 } 142 } 143 144 return result; 145 } 146 147 HashSet<T> _timeoutSocketGroup = new HashSet<T>(); 148 private void AdjustTimeout() 149 { 150 while (_listTimeScale.Count > _scaleCount) 151 { 152 SameTimeKeyGroup<T> group = _listTimeScale[0]; 153 foreach (T key in group.KeyGroup) 154 { 155 _timeoutSocketGroup.Add(key); 156 } 157 158 _listTimeScale.RemoveAt(0); 159 } 160 } 161 162 private SameTimeKeyGroup<T> GetOrCreateSocketGroup(DateTime now, out bool newCreate) 163 { 164 if (_listTimeScale.Count == 0) 165 { 166 newCreate = true; 167 SameTimeKeyGroup<T> result = new SameTimeKeyGroup<T>(now); 168 _listTimeScale.Add(result); 169 return result; 170 } 171 else 172 { 173 SameTimeKeyGroup<T> lastGroup = _listTimeScale[_listTimeScale.Count - 1]; 174 if (lastGroup.TimeStamp == now) 175 { 176 newCreate = false; 177 return lastGroup; 178 } 179 180 newCreate = true; 181 SameTimeKeyGroup<T> result = new SameTimeKeyGroup<T>(now); 182 _listTimeScale.Add(result); 183 return result; 184 } 185 } 186 187 DateTime CreateNow() 188 { 189 DateTime now = DateTime.Now; 190 int m = 0; 191 if(now.Millisecond != 0) 192 { 193 if(_minimumScaleOfMillisecond == 1000) 194 { 195 now = now.AddSeconds(1); //尾數加1,確保超時值大於 給定的值 196 } 197 else 198 { 199 //如果now.Millisecond為16毫秒,精確度為10毫秒。則轉換後為20毫秒 200 m = now.Millisecond - now.Millisecond % _minimumScaleOfMillisecond + _minimumScaleOfMillisecond; 201 if(m>=1000) 202 { 203 m -= 1000; 204 now = now.AddSeconds(1); 205 } 206 } 207 } 208 return new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second,m); 209 } 210 } 211 212 class SameTimeKeyGroup<T> 213 { 214 DateTime _timeStamp; 215 public DateTime TimeStamp => _timeStamp; 216 public SameTimeKeyGroup(DateTime time) 217 { 218 _timeStamp = time; 219 } 220 public HashSet<T> KeyGroup { get; set; } = new HashSet<T>(); 221 222 public bool ContainKey(T key) 223 { 224 return KeyGroup.Contains(key); 225 } 226 227 internal void AddKey(T key) 228 { 229 KeyGroup.Add(key); 230 } 231 internal bool RemoveKey(T key) 232 { 233 return KeyGroup.Remove(key); 234 } 235 }