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#实现的一个简单的心跳
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251using 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; } }
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持靠谱客。
最后
以上就是潇洒棉花糖最近收集整理的关于C# 实现Scoket心跳机制的方法的全部内容,更多相关C#内容请搜索靠谱客的其他文章。
发表评论 取消回复