UNIX網絡編程——客戶/服務器心搏函數

         閱讀此博客時,能夠參考之前的博客<<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。

相關文章
相關標籤/搜索