1. 程式人生 > >使用帶外資料設定 C/S 心跳機制

使用帶外資料設定 C/S 心跳機制

   首先,如果你還不瞭解什麼是帶外資料:點這裡

心跳機制的產生就是為了檢測出對端主機或到對端的通訊路徑是否過早失效。

注意:在使用心跳機制時,你應該考慮是不是符合你所處的情景,確定在對端應答的時間超過 5~10s 之後終止連線是件好事還是壞事。如果你的產品需要實時的知道對端的“生存狀態”,(要麼是為了需求,要麼是為了節省資源)那麼就是需要這種機制的。一般用於 長連線 。

   在這裡,我們使用TCP的帶外資料來完成心跳機制的實現(每秒鐘輪詢一次,若5秒沒有得到響應就認為對端已經“死亡”),實現如下所示 :

在這裡插入圖片描述

客戶端每隔1秒鐘向伺服器傳送一個帶外位元組,伺服器收到該型別的位元組然後再發送回一個帶外位元組。因為每一端都需要對端不復存在或者不再可達。需要指出的是:**資料,回送資料和帶外位元組都通過單個的連線交換的 。**程式碼實現如下,具體實現細節在程式碼中有註釋指出

  1. Recvline 函式
#include "../myhead.h"

static int recv_cnt = 0;
static char *recv_ptr = NULL;
static char recv_buf[MAXLINE];

static ssize_t my_recv(int fd, char *ptr, int flags)
{
	if (recv_cnt <= 0)
	{
	again:
		if ((recv_cnt = recv(fd, recv_buf, sizeof(recv_buf), flags)) < 0)
		{
			if (errno ==
EINTR) goto again; else return (-1); } else if (recv_cnt == 0) { return (0); } recv_ptr = recv_buf; } recv_cnt--; *ptr = *recv_ptr++; return (1); } ssize_t recvline(int fd, void *vptr, size_t maxlen, int flags) { ssize_t n, rc; char c, *ptr; ptr = vptr; for (n = 1; n <
maxlen; n++) { if ((rc = my_recv(fd, &c, flags)) == 1) { *ptr++ = c; if (c == '\n') break; } else if (rc == 0) { *ptr = 0; return (n - 1); } else return (-1); } *ptr = 0; return (n); } ssize_t Recvline(int fd, void *buf, size_t Maxlen, int flags) { //注意引數 Maxlen ssize_t n; if ((n = recvline(fd, buf, Maxlen, flags)) < 0) err_sys("recvline error "); return (n); }

recv_buf寫做我們的一個緩衝區,recv_ptr指向緩衝區下一個可被讀取的位元組,在my_recv函式中複製給引數ptr,表示讀取到一個位元組。recv_cnt表示緩衝區剩餘位元組的數量 ,如果<=0,就進行下一次的緩衝區讀取。如果在需要一個位元組一個位元組的處理資料時,比起一個位元組一個位元組的讀取,顯然這樣的方式是更有效率的 !!!

  1. 服務端主函式 serv_main.c
#include "../myhead.h"
#include "test.h"

void fun_serv(int connfd) //子程序執行函式
{
    ssize_t n;
    char line[MAXLINE];

    heartbeat_serv(connfd, 1, 5);

    for (;;)
    {
        if ((n = Recvline(connfd, line, MAXLINE, 0)) == 0)
        {
            printf("客 戶 端 關  閉 啦 !!!\n");
            Close(connfd);
            return ; 
        }
        Sendlen(connfd, line, n, 0);
    }
}

int main(int argc, char **argv)
{
    int listenfd, connfd;
    pid_t childpid;
    socklen_t clilen;
    struct sockaddr_in cliaddr, servaddr;

    listenfd = Socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT); //9877

    int opt = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (int *)&opt, sizeof(int));

    Bind(listenfd, (SA *)&servaddr, sizeof(servaddr));

    Listen(listenfd, LISTENQ);

    for (;;)
    {
        clilen = sizeof(cliaddr);
        if ((connfd = Accept(listenfd, (SA *)&cliaddr, &clilen)) < 0)
        {
            if (errno == EINTR)
                continue;
            else
                err_sys("accept error ");
        }

        if ((childpid = Fork()) == 0)
        {                    
            Close(listenfd); 

            printf(" 新的連線:connfd == %d \n", connfd);

            fun_serv(connfd); 

            exit(0);
        }
        Close(connfd); 
    }
}

在我們fork之後,父子程序都會有一個監聽套接字和一個連線套接字,另外,所有的套接字選項都會從監聽套接字傳承給連線套接字。因為我們只需要讓子程序去處理客戶端請求即可。所以我們在父程序中關閉連線套接字,讓他只去負責處理客戶連線。子程序中關閉監聽套接字,讓子程序去處理客戶請求。

  1. 服務端設定心跳包函式 heartbeat_serv.c
#include "../myhead.h"

static int servfd;
static int nsec;
static int maxnalarms;
static int nprobes;                      //統計 SIGALRM 數量
static void sig_urg(int), sig_alrm(int); // alarm 函式的使用是為了輪詢

void heartbeat_serv(int servfd_arg, int nsec_arg, int maxnalarms_arg) //fd  1 5
{
    servfd = servfd_arg;
    nsec = nsec_arg;
    maxnalarms = maxnalarms_arg;

    nprobes = 0;
    signal(SIGURG, sig_urg);
    Fcntl(servfd, F_SETOWN, getpid());

    signal(SIGALRM, sig_alrm);
    alarm(nsec);
}

static void sig_urg(int signo)
{
    printf("產生  SIGURG 訊號 \n");

    int n;
    char ch;

    if ((n = recv(servfd, &ch, 1, MSG_OOB)) < 0) //只要產生帶外資料,就說明客戶端主機是存活的
    {
        if (errno != EWOULDBLOCK)
            err_sys("recv error");
    }
    else if (n > 0)
    {
        printf("伺服器接收到帶外資料,說明客戶端主機是存活的\n");

        if (send(servfd, &ch, 1, MSG_OOB) > 0)
            printf("伺服器傳送了帶外資料\n");
        nprobes = 0;
    }
    return;
}
static void sig_alrm(int signo)
{
    if (++nprobes > maxnalarms)
    {
        fprintf(stderr, " 此客戶端 gg,伺服器子程序退出 \n");
        exit(0); 
    }
    alarm(nsec);
    return;
}

客戶端連線之後,由客戶端主動發起OOB資料心跳,然後在服務端產生SIGURG訊號,服務端處理該訊號和OOB資料,並回送。如果已經產生了SIGURG訊號,但是oob數還沒有到達,recv會返回EWOULDBLOCK錯誤。nprobes用來統計時間。

  1. 客戶端主函式 cli_main.c
#include "../myhead.h"
#include "test.h"

void fun_client(FILE *fp, int sockfd)
{
	int maxfdp1, stdineof = 0;
	fd_set rset;
	char recvline[MAXLINE], sendline[MAXLINE];
	int n;
	heartbeat_cli(sockfd, 1, 5); // 1.呼叫函式

	FD_ZERO(&rset);
	for (;;)
	{
		if (stdineof == 0)
			FD_SET(fileno(fp), &rset);
		FD_SET(sockfd, &rset);
		maxfdp1 = max(fileno(fp), sockfd) + 1;

		if ((n = select(maxfdp1, &rset, NULL, NULL, NULL)) < 0)
		{
			if (errno == EINTR) // 2.處理 select
				continue;
			else
				err_sys("select error");
		}

		if (FD_ISSET(sockfd, &rset))
		{
			if ((n = Recvline(sockfd, recvline, MAXLINE, 0)) == 0) //讀取一行,Readline 返回讀取到的位元組數
			{
				if (stdineof == 1)
					return;
				else
					err_quit("fun_client:server terminated permaturely ");
			}

			write(STDOUT_FILENO, recvline, n); //3.呼叫 write 函式,而不是 fputs,見`UNP`
		}
		if (FD_ISSET(fileno(fp), &rset))  //可以進行輸入了
		{
			if (Fgets(sendline, MAXLINE, fp) == NULL)
			{
				stdineof = 1;
				shutdown(sockfd, SHUT_WR);
				FD_CLR(fileno(fp), &rset); // 在檔案描述符集合中刪除一個檔案描述符。
				continue;
			}
			Sendlen(sockfd, sendline, strlen(sendline), 0);
		}
	}
}

int main(int argc, char **argv)
{
	int sockfd;
	struct sockaddr_in servaddr;

	if (argc != 2)
	{
		printf("usage: tcpcli <IPaddress>\n");
		return -1;
	}

	sockfd = Socket(AF_INET, SOCK_STREAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(SERV_PORT); // 9877
	inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

	connect(sockfd, (SA *)&servaddr, sizeof(servaddr));

	fun_client(stdin, sockfd);

	exit(0);
}

使用select 來監聽兩個套接字,一個是連線套接字,一個是標準輸入。stdeof=0,說明可以輸入。如果輸入結束,我們將stdeof設為1,關閉對sockfd的任何寫操作,刪除select對應位。EINTR:當慢系統呼叫被訊號打斷時,慢系統呼叫會返回它。對比於下面一種簡單的寫法,他為什麼能處理伺服器崩潰的情況?因為在這裡,當讀到EOF時,判斷了是否是正常結束(stdeof=1正常,否則伺服器過早終止server terminated permaturely

  1. 客戶端設定心跳包函式 heartbeat_cli.c

#include "../myhead.h"
#include "test.h"

static int servfd;
static int nsec;
static int maxnprobes;
static int nprobes;						 //統計產生訊號`SIGALRM`的數量
static void sig_urg(int), sig_alrm(int); // alarm 函式的使用是為了輪詢

void heartbeat_cli(int servfd_arg, int nsec_arg, int maxnprobes_arg) //fd  1 5
{
	//sleep(6);

	servfd = servfd_arg;
	nsec = nsec_arg;
	maxnprobes = maxnprobes_arg;

	nprobes = 0;
	signal(SIGURG, sig_urg);
	Fcntl(servfd, F_SETOWN, getpid());

	signal(SIGALRM, sig_alrm);
	alarm(nsec);
}

static void sig_urg(int signo)
{
	printf("產生  SIGURG 訊號 \n");

	int n;
	char ch;
	if ((n = recv(servfd, &ch, 1, MSG_OOB)) < 0) //只要產生帶外資料,就說明伺服器主機是存活的
	{
		if (errno != EWOULDBLOCK)
			err_sys("recv error");
	}
	else if (n > 0)
	{
		printf("客戶端接收到帶外資料,說明伺服器主機是存活的\n");
		nprobes = 0;
	}
	return;
}

static void sig_alrm(int signo)
{
	if (++nprobes > maxnprobes)
	{
		fprintf(stderr, "此伺服器gg,客戶端直接退出 \n");
		exit(0);
	}
	if( send(servfd, "1", 1, MSG_OOB) > 0 )
		printf("客戶端傳送了帶外資料\n");
	
	alarm(nsec);
	return;
}

測試:

  1. 是否正確執行 在這裡插入圖片描述

  2. 伺服器超時響應: 在這裡插入圖片描述

  3. 回射伺服器正常執行:

在這裡插入圖片描述

  1. 某一端崩潰:

在這裡插入圖片描述

具體原始碼見我的github原始碼

討論:1. 為什麼不選用TCP 保持存活特性(SO_KEEPLIVE)來提供這種功能?

   兩個原因:

  1. SO_KEEPALIVE選項預設是閒置2小時,傳送保持存活檢測段。可以改動時間,但是改動之後會影響所有的開啟該選項的套接字。
  2. SO_KEEPALIVE也不是用來高頻率的輪詢的 。

2. 為什麼在發現對端 gg之後,直接退出,而不是選擇關閉套接字再退出?

因為對方已經ggclose(fd)會給對方傳送一個SYN,自己身處於time_wait狀態 ,而這是完全沒有必要的 。

3. Recvline 函式有什麼缺點?靜態資料有什麼缺點?

使用靜態資料會使得recvline函式變得非執行緒安全了 。持續更新----------》

4. 像下面一樣寫,當伺服器崩潰時,客戶端會有什麼效果?

void fun_client00000(int connfd)
{
	char sendline[MAXLINE] = {0};
	char recvline[MAXLINE] = {0};
	int tt = 0;

	while (Fgets(sendline, MAXLINE, stdin) != NULL)
	{
		tt = Sendlen(connfd, sendline, strlen(sendline), 0);

		if ( Recvline(connfd, recvline, sizeof(recvline), 0) == 0)
		{
			printf("服 務 器 關 閉 連 接 退 出 \n");
			Close(connfd);
			return;
		}
		Fputs(recvline, stdout);
	}
}
int main(int argc, char **argv)
{
	int sockfd;
	struct sockaddr_in servaddr;

	if (argc != 2)
	{
		printf("usage: tcpcli <IPaddress>\n");
		return -1;
	}

	sockfd = Socket(AF_INET, SOCK_STREAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(SERV_PORT); // 9877
	inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

	connect(sockfd, (SA *)&servaddr, sizeof(servaddr));

	fun_client00000(sockfd);

	exit(0);
}

客戶端首先阻塞於Fgets,然後阻塞於Recvline,如果一旦服務端崩潰,客戶端的Recvline返回0,列印輸出資訊。但是,這裡遇到的一個問題就是“當伺服器崩潰時,只有按了回車鍵才會輸出列印資訊”,還沒有搞清楚這是為什麼? ~_~