什么是心跳包?
心跳包就是在客戶端和服務(wù)器間定時(shí)通知對(duì)方自己狀態(tài)的一個(gè)自己定義的命令字,按照一定的時(shí)間間隔發(fā)送,類似于心跳,所以叫做心跳包。
?
用來判斷對(duì)方(設(shè)備,進(jìn)程或其它網(wǎng)元)是否正常運(yùn)行,采用定時(shí)發(fā)送簡單的通訊包,如果在指定時(shí)間段內(nèi)未收到對(duì)方響應(yīng),則判斷對(duì)方已經(jīng)離線。用于檢測TCP的異常斷開。基本原因是服務(wù)器端不能有效的判斷客戶端是否在線,也就是說,服務(wù)器無法區(qū)分客戶端是長時(shí)間在空閑,還是已經(jīng)掉線的情況。所謂的心跳包就是客戶端定時(shí)發(fā)送簡單的信息給服務(wù)器端告訴它我還在而已。代碼就是每隔幾分鐘發(fā)送一個(gè)固定信息給服務(wù)端,服務(wù)端收到后回復(fù)一個(gè)固定信息如果服務(wù)端幾分鐘內(nèi)沒有收到客戶端信息則視客戶端斷開。
比如有些通信軟件長時(shí)間不使用,要想知道它的狀態(tài)是在線還是離線就需要心跳包,定時(shí)發(fā)包收包。發(fā)包方:可以是客戶也可以是服務(wù)端,看哪邊實(shí)現(xiàn)方便合理,一般是客戶端。服務(wù)器也可以定時(shí)發(fā)心跳下去。一般來說,出于效率的考慮,是由客戶端主動(dòng)向服務(wù)器端發(fā)包,而不是服務(wù)器向客戶端發(fā)。客戶端每隔一段時(shí)間發(fā)一個(gè)包,使用TCP的,用send發(fā),使用UDP的,用sendto發(fā),服務(wù)器收到后,就知道當(dāng)前客戶端還處于“活著”的狀態(tài),否則,如果隔一定時(shí)間未收到這樣的包,則服務(wù)器認(rèn)為客戶端已經(jīng)斷開,進(jìn)行相應(yīng)的客戶端斷開邏輯處理。
服務(wù)器實(shí)現(xiàn)心跳機(jī)制的兩種策略
大部分CS的應(yīng)用需要心跳機(jī)制。心跳機(jī)制一般在Server和Client都要實(shí)現(xiàn),兩者實(shí)現(xiàn)原理基本一樣。Client不關(guān)心性能,怎么做都行。
如果應(yīng)用是基于TCP的,可以簡單地通過SO_KEEPALIVE實(shí)現(xiàn)心跳。TCP在設(shè)置的KeepAlive定時(shí)器到達(dá)時(shí)向?qū)Χ税l(fā)一個(gè)檢測TCP segment,如果沒收到ACK或RST,嘗試幾次后,就認(rèn)為對(duì)端已經(jīng)不存在,最后通知應(yīng)用程序。這里有個(gè)缺點(diǎn)是,Server主動(dòng)發(fā)出檢測包,對(duì)性能有點(diǎn)影響。
應(yīng)用自己實(shí)現(xiàn)
Client啟動(dòng)一個(gè)定時(shí)器,不斷發(fā)心跳;
Server收到心跳后,給個(gè)回應(yīng);
Server啟動(dòng)一個(gè)定時(shí)器,判斷Client是否存在,判斷方法這里列兩種:時(shí)間差和簡單標(biāo)志。
1. 時(shí)間差策略
收到一個(gè)心跳后,記錄當(dāng)前時(shí)間(記為recvedTime)。
判斷定時(shí)器時(shí)間到達(dá),計(jì)算多久沒收到心跳的時(shí)間(T)=當(dāng)前時(shí)間 - recvedTime(上面記錄的時(shí)間)。如果T大于某個(gè)設(shè)定值,就可以認(rèn)為Client超時(shí)了。
2. 簡單標(biāo)志
收到一個(gè)心跳后,設(shè)置連接標(biāo)志為true;
判斷定時(shí)器時(shí)間到達(dá),查看所有的標(biāo)志,false的,認(rèn)為對(duì)端超時(shí)了;true的將其設(shè)成false。
上面這種方法比上面簡單一些,但檢測某個(gè)Client是否離線的誤差有點(diǎn)大。
您還有心跳嗎?超時(shí)機(jī)制分析
問題描述
在C/S模式中,有時(shí)我們會(huì)長時(shí)間保持一個(gè)連接,以避免頻繁地建立連接,但同時(shí),一般會(huì)有一個(gè)超時(shí)時(shí)間,在這個(gè)時(shí)間內(nèi)沒發(fā)起任何請(qǐng)求的連接會(huì)被斷開,以減少負(fù)載,節(jié)約資源。并且該機(jī)制一般都是在服務(wù)端實(shí)現(xiàn),因?yàn)閏lient強(qiáng)制關(guān)閉或意外斷開連接,server端在此刻是感知不到的,如果放到client端實(shí)現(xiàn),在上述情況下,該超時(shí)機(jī)制就失效了。本來這問題很普通,不太值得一提,但最近在項(xiàng)目中看到了該機(jī)制的一種糟糕的實(shí)現(xiàn),故在此深入分析一下。
問題分析及解決方案
服務(wù)端一般會(huì)保持很多個(gè)連接,所以,一般是創(chuàng)建一個(gè)定時(shí)器,定時(shí)檢查所有連接中哪些連接超時(shí)了。此外我們要做的是,當(dāng)收到客戶端發(fā)來的數(shù)據(jù)時(shí),怎么去刷新該連接的超時(shí)信息?
最近看到一種實(shí)現(xiàn)方式是這樣做的:
public class Connection {private long lastTime;public void refresh() {lastTime = System.currentTimeMillis();}public long getLastTime() {return lastTime;}//...... }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
在每次收到客戶端發(fā)來的數(shù)據(jù)時(shí),調(diào)用refresh方法。
然后在定時(shí)器里,用當(dāng)前時(shí)間跟每個(gè)連接的getLastTime()作比較,來判定超時(shí):
public class TimeoutTask extends TimerTask{public void run() {long now = System.currentTimeMillis();for(Connection c: connections){if(now - c.getLastTime()> TIMEOUT_THRESHOLD);//timeout, do something}} }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
看到這,可能不少讀者已經(jīng)看出問題來了,那就是內(nèi)存可見性問題,調(diào)用refresh方法的線程跟執(zhí)行定時(shí)器的線程肯定不是一個(gè)線程,那run方法中讀到的lastTime就可能是舊值,即可能將活躍的連接判定超時(shí),然后被干掉。
有讀者此時(shí)可能想到了這樣一個(gè)方法,將lastTime加個(gè)volatile修飾,是的,這樣確實(shí)解決了問題,不過,作為服務(wù)端,很多時(shí)候?qū)π阅苁怯幸蟮?#xff0c;下面來看下在我電腦上測出的一組數(shù)據(jù),測試代碼如下,供參考
public class PerformanceTest {private static long i;private volatile static long vt;private static final int TEST_SIZE = 10000000;public static void main(String[] args) {long time = System.nanoTime();for (int n = 0; n < TEST_SIZE; n++)vt = System.currentTimeMillis();System.out.println(-time + (time = System.nanoTime()));for (int n = 0; n < TEST_SIZE; n++)i = System.currentTimeMillis();System.out.println(-time + (time = System.nanoTime()));for (int n = 0; n < TEST_SIZE; n++)synchronized (PerformanceTest.class) {}System.out.println(-time + (time = System.nanoTime()));for (int n = 0; n < TEST_SIZE; n++)vt++;System.out.println(-time + (time = System.nanoTime()));for (int n = 0; n < TEST_SIZE; n++)vt = i;System.out.println(-time + (time = System.nanoTime()));for (int n = 0; n < TEST_SIZE; n++)i = vt;System.out.println(-time + (time = System.nanoTime()));for (int n = 0; n < TEST_SIZE; n++)i++;System.out.println(-time + (time = System.nanoTime()));for (int n = 0; n < TEST_SIZE; n++)i = n;System.out.println(-time + (time = System.nanoTime()));} }- 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
測試一千萬次,結(jié)果是(耗時(shí)單位:納秒,包含循環(huán)本身的時(shí)間):?
238932949 volatile寫+取系統(tǒng)時(shí)間?
144317590 普通寫+取系統(tǒng)時(shí)間?
135596135 空的同步塊(synchronized)?
80042382 volatile變量自增?
15875140 volatile寫?
6548994 volatile讀?
2722555 普通自增?
2949571 普通讀寫
從上面的數(shù)據(jù)看來,volatile寫+取系統(tǒng)時(shí)間的耗時(shí)是很高的,取系統(tǒng)時(shí)間的耗時(shí)也比較高,跟一次無競爭的同步差不多了,接下來分析下如何優(yōu)化該超時(shí)時(shí)機(jī)。
首先:同步問題是肯定得考慮的,因?yàn)橛锌缇€程的數(shù)據(jù)操作;另外,取系統(tǒng)時(shí)間的操作比較耗時(shí),能否不在每次刷新時(shí)都取時(shí)間?因?yàn)樗⑿抡{(diào)用在高負(fù)載的情況下很頻繁。如果不在刷新時(shí)取時(shí)間,那又該怎么去判定超時(shí)?
我想到的辦法是,在refresh方法里,僅設(shè)置一個(gè)volatile的boolean變量reset(這應(yīng)該是成本最小的了吧,因?yàn)橐幚硗絾栴},要么同步塊,要么volatile,而volatile讀在此處是沒什么意義的),對(duì)時(shí)間的掌控交給定時(shí)器來做,并為每個(gè)連接維護(hù)一個(gè)計(jì)數(shù)器,每次加一,如果reset被設(shè)置為true了,則計(jì)數(shù)器歸零,并將reset設(shè)為false(因?yàn)橛?jì)數(shù)器只由定時(shí)器維護(hù),所以不需要做同步處理,從上面的測試數(shù)據(jù)來看,普通變量的操作,時(shí)間成本是很低的),如果計(jì)數(shù)器超過某個(gè)值,則判定超時(shí)。 下面給出具體的代碼:
public class Connection {int count = 0;volatile boolean reset = false;public void refresh() {if (reset == false)reset = true;} }public class TimeoutTask extends TimerTask {public void run() {for (Connection c : connections) {if (c.reset) {c.reset = false;c.count = 0;} else if (++c.count >= TIMEOUT_COUNT);// timeout, do something}} }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
代碼中的TIMEOUT_COUNT 等于超時(shí)時(shí)間除以定時(shí)器的周期,周期大小既影響定時(shí)器的執(zhí)行頻率,也會(huì)影響實(shí)際超時(shí)時(shí)間的波動(dòng)范圍(這個(gè)波動(dòng),第一個(gè)方案也存在,也不太可能避免,并且也不需要多么精確)。
代碼很簡潔,下面來分析一下。
reset加上了volatile,所以保證了多線程操作的可見性,雖然有兩個(gè)線程都對(duì)變量有寫操作,但無論這兩個(gè)線程怎么穿插執(zhí)行,都不會(huì)影響其邏輯含義。
再說下refresh方法,為什么我在賦值語句上多加了個(gè)條件?這不是多了一次volatile讀操作嗎?我是這么考慮的,高負(fù)載下,refresh會(huì)被頻繁調(diào)用,意味著reset長時(shí)間為true,那么加上條件后,就不會(huì)執(zhí)行寫操作了,只有一次讀操作,從上面的測試數(shù)據(jù)來看,volatile變量的讀操作的性能是顯著優(yōu)于寫操作的。只不過在reset為false的時(shí)候,多了一次讀操作,但此情況在定時(shí)器的一個(gè)周期內(nèi)最多只會(huì)發(fā)一次,而且對(duì)高負(fù)載情況下的優(yōu)化顯然更有意義,所以我認(rèn)為加上條件還是值得的。
————————————-?
補(bǔ)充一下:一般情況下,也可用特定的心跳包來刷新,而不是每次收到消息都刷新,這樣一來,刷新頻率就很低了,也就沒必要太在乎性能開銷。
轉(zhuǎn)載自:?
java心跳是怎么回事兒啊??
服務(wù)器實(shí)現(xiàn)心跳機(jī)制的兩種策略?
您還有心跳嗎?超時(shí)機(jī)制分析
總結(jié)
- 上一篇: 微信支付开发,开通微信免充值代金券和微信
- 下一篇: 城市公共交通规划掌握内容