閱讀此博客時,能夠參考之前的博客<<UNIX網絡編程——socket的keep-alive>>和<<UNIX網絡編程——套接字選項(心跳檢測、綁定地址複用)>>。編程
下面是關於回送客戶和服務器程序開發一些簡單的心搏函數。這些函數能夠發現對端主機或到對端的通訊路徑的過早失效。
在給出這些函數以前咱們必須提出一些警告。首先,有人會想到使用TCP的保持存活特性(SO_KEEPALIVE套接字選項)來提供這種功能,然而TCP得在鏈接已經閒置2小時以後才發送一個保持存活探測段。意識到這一點之後,他們的下一個問題是如何把保持存活參數改成一個小得多的值(每每是在秒鐘的量級),以便更快的檢測到失效。儘管縮短TCP的保持存活定時器參數在許多系統上確實可行,可是這些參數一般是按照內核而不是按照每一個套接字維護的,所以改動他們將影響全部開啓該選項的套接字。另外保持存活選項的用意毫不是這個目的(搞頻率的輪詢)。
其次,兩個端系統之間短暫的鏈接性丟失並不是老是壞事。TCP一開始就設計成可以對付臨時斷連,而源自Berkeley的TCP實現將重傳8-10分鐘才放棄某個鏈接。較新的IP路由協議可以發現連接的失效,而且有可能在短期內(譬如在秒鐘量級上)啓用候選的路徑。所以應用程序開發人員必須審查想要引入心搏機制的具體應用,確實在沒有聽到對端應答的持續時間超過5-10S以後終止相應鏈接是件好事仍是壞事。有些應用系統須要這種功能,不過大多數卻並不須要。
咱們將使用TCP的緊急模式週期地輪詢對端;在下面的講解中咱們假設每1S輪詢一次,若持續5S沒有聽到對端應答則認爲對端已再也不存活,不過這些值能夠由應用程序改動。服務器
在這個例子中,客戶每隔1S向服務器發送一個帶外字節,服務器取該字節將致使它向客戶發送回一個帶外字節。每端都須要知道對端是否不復存在或者再也不可達。客戶和服務器每1S遞增他們的cnt變量一次,每收到一個帶外字節又把該變量重置爲0。若是該計數器達到5(也就是說本進程已有5S沒有收到來自對端的帶外字節),那就認定鏈接失效。當有帶外字節到達時,客戶和服務器都是用SIGURG信號得以通知。咱們在該圖中間指出:數據,回送數據和帶外字節都經過單個TCP鏈接交換。網絡
以下是咱們的heatbeat_cli函數設置客戶的心搏特性,其中第二個參數是以秒爲單位的輪詢頻率,第三個參數是放棄當前鏈接以前應該經歷的持續無響應輪詢次數。socket
#include "unp.h" /* 給heartbeat_cli的參數的拷貝: 套接口描述字(信號處理程序需用它來發送和接收帶外數據),SIGALRM的頻率,在客戶認爲服務器或鏈接死掉以前沒有來子服務器的響應的SIGALRM的總數,總量nprobes記錄從最近一次服務器應答以來的SIGALRM的數目 */ static int servfd; static int nsec; /* #seconds between each alarm */ static int maxnprobes; /* #probes w/no response before quit */ static int nprobes; /* #probes since last server response */ static void sig_urg(int), sig_alrm(int); void heartbeat_cli(int servfd_arg, int nsec_arg, int maxnprobes_arg) {/* heartbeat_cli函數檢查而且保存參數,給SIGURG和SIGALRM創建信號處理函數,將套接口的屬主設爲進程ID,alarm調度一個SIGALRM */ servfd = servfd_arg; /* set globals for signal handlers */ if( (nsec = nsec_arg) < 1) nsec = 1; if( (maxnprobes = maxnprobes_arg) < nsec) maxnprobes = nsec; nprobes = 0; Signal(SIGURG, sig_urg); Fcntl(servfd, F_SETOWN, getpid() ); Signal(SIGALRM, sig_alrm); alarm(nesc); } static void sig_urg(int signo) {/* 當一個帶外通知到來時,就會產生這個信號。咱們試圖去讀帶外字節,但若是尚未到(EWOULDBLOCK)也沒有關係。因爲系統不是在線接收帶外數據,所以不會干擾客戶讀取它的普通數據。既然服務器仍然存活,nprobes就重置爲0 */ int n; char c; if( ( n = recv(servfd, &c, 1, MSG_OOB) ) < 0 ) { if(errno != EWOULDBLOCK) err_sys("recv error"); } nprobes = 0; /* reset counter */ return; /* may interrupt client code */ } static void sig_alrm(int signo) {/* 這個信號以有規律間隔產生。計數器nprobes增1, 若是達到了maxnprobes,咱們認爲服務器或者崩潰或者不可達。在這個例子中,咱們結束客戶進程,儘管其餘的設計也可使用:能夠發送給主循環一個信號,或者做爲另一個參數給heartbeat_cli提供一個客戶函數,當服務器看來死掉時調用它 */ if( ++nprobes > maxnprobes) { fprintf(stderr, "server is unreachable \n"); exit(0); } Send(servfd, "1", 1, MSG_OOB); alarm(nsec); return; /* may interrupt client code */ }
全局變量
3-6 前3個變量是heartbeat_cli函數參數的副本:套接字描述符(信號處理函數用它來發送和接收帶外數據),SIGALRM的頻率,在客戶認爲服務器或鏈接不復存活以前處理的無服務器響應的SIGALRM總數。變量nprobes計量從收到來自服務器的最後一個應答以來處理的SIGALRM數目。函數
heartbeat_cli函數
8-20 heartbeat_cli函數檢查並保存參數,給SIGURG和SIGALRM創建信號處理函數,並把套接字的屬主設置爲本進程ID。執行alarm以調度第一個SIGALRM.ui
SIGURG處理函數
21-32 本信號在某個帶外通知到達時產生。咱們嘗試讀入相應的帶外字節,不過若是它尚未到達(EWOULDBLOCK),那也沒有關係。注意,咱們不採用在線接收帶外數據方式,由於這種方式會干擾客戶讀取它的正常數據。既然服務器仍然存活着,咱們把nprobes重置爲0.spa
SIGALRM處理函數
33-43 本信號以恆定的間隔產生。遞增計數器nprobes,若是達到maxnprobes,咱們就認定服務器主機或者已經崩潰,或者再也不可達。咱們這裏是直接結束客戶進程。做爲帶外數據發送一個含有字符1的字節(該值沒有任何隱含意義),再執行alarm調度下一個SIGALRM。.net
下面是服務器程序的心搏函數。設計
#include "unp.h" static int servfd; static int nsec; /* #seconds between each alarm */ static int maxnalarms; /* #alarms w/no client probe before quit */ static int nprobes; /* #alarms since last client probe */ static void sig_urg(int), sig_alrm(int); void heartbeat_serv(int servfd_arg, int nsec_arg, int maxnalarms_arg) { servfd = servfd_arg; /* set globals for signal handlers */ if( (nsec = nsec_arg) < 1 ) nsec = 1; if( (maxnalarms = maxnalarms_arg) < nsec) maxnalarms = nsec; Signal(SIGURG, sig_urg); Fcntl(servfd, F_SETOWN, getpid()); Signal(SIGALRM, sig_alrm); alarm(nsec); } static void sig_urg(int signo) { /* 當一個帶外通知收到時, 服務器試圖讀入它。就像客戶同樣,若是帶外字節沒有到達沒有什麼關係。帶外字節被做爲帶外數據返回給客戶。注意,若是recv返回EWOULDBLOCK錯誤,那麼自動變量c碰巧是什麼就送給客戶什麼。因爲咱們不用帶外字節的值,因此這沒有關係。重要的是發送1字節的帶外數據,而無論該字節是什麼。因爲剛收到通知,客戶仍存活,因此重置nprobes爲0 */ int n; char c; if( (n = recv(servfd, &c, 1, MSG_OOB)) < 0) { if(errno != EWOULDBLOCK) err_sys("recv error"); } Send(servfd, &c, 1, MSG_OOB); /* echo back out-of-hand byte */ nprobes = 0; /* reset counter */ return; /* may interrupt server code */ } static void sig_alrm(int signo) { /* nprobes增1, 若是它到達了調用者指定的值maxnalarms,服務器進程將被終止。不然調度一下SIGALRM */ if( ++nprobes > maxnalarms) { printf("no probes from client\n"); exit(0); } alarm(nsec); return; /* may interrupt server code */ }
heartbeat_serv函數
7-18 聲明變量,函數heartbeat_serv幾乎與客戶的心搏初始化函數同樣。code
SIGURG處理函數
19-31 服務器收到一個帶外通知後就嘗試讀入相應的帶外字節。就像客戶同樣,若是該帶外字節尚未到達,那也沒有聲明關係。服務器把讀入的帶外字節做爲帶外數據回送給客戶。注意,若是recv返回EWOULDBLOCK錯誤,那麼自動變量C碰巧是什麼就回送什麼。既然咱們不把帶外字節的值用於任何目的,這麼處置就不會有問題。重要的是發送1字節的帶外數據自己,而不是該字節究竟是什麼。既然剛收到客戶仍然存活着的通知,咱們把nprobes重置爲0.
SIGALRM處理函數32-41 遞增nprobes,若是達到由調用者指定的maxnalarms值,那就終止服務器進程,不然調度下一個SIGALRM。