带外数据的编程实现
验证带外数据的发送与接收的各种方式。
利用带外数据原理设计并实现客户-服务器心搏函数。用于发现对端主机或到对端的通信路径的过早失效。
假设每1 秒钟轮询一次,若持续5 秒钟没有听到对端应答则认为对端已不再存活,这些值可以有应用程序改动。
1.客户-服务器心搏机制
每隔1 秒钟向服务器发送一个带外字节,服务器收取该字节将导致它向客户发送回一个带外字节。
客户和服务器每秒中递增他们的cnt 变量一次,每收到一个带外字节又把该变量重置为0。
若计数器到达5(即本进程已5 秒钟没有收到对端的带外字节),就认定连接失效。
双向数据和带外字节都是通过一个TCP 连接交换。
2. 具体目标
根据上述提示分别设计并实现客户程序心搏函数和服务端心搏函数。并在建立连接的TCP 两端运行两个心搏函数。
3. 编程提示
客户服务端主函数均在建立连接后运行各自心搏函数,再执行无限循环并在循环中执行pause 等待。
(客户端心搏函数)
主函数给心搏函数提供:已连接描述字,间隔发送OOB 的秒数(频率),最大间隔次数数(服务无响应的最大次数)。
建立SIGURG 和SIGALRM 信号处理函数,并设置套接口属主为本进程ID。
按照上图提示编写两个信号处理函数。并在信号处理函数中输出信息到标准输出,以显示心搏函数的正常。或通过人为发送SIGUSR 信号来终止进程,以测试心搏函数。
(服务器端心搏函数)
主函数给心搏函数提供:已连接描述字,间隔发送OOB 的秒数(频率),最大间隔次数数(服务无响应的最大次数)。
建立SIGURG 和SIGALRM 信号处理函数,并设置套接口属主为本进程ID。
按照上图提示编写两个信号处理函数。
头文件:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>/*for close*/
#include <fcntl.h>
#include <sys/wait.h>
#include <netdb.h>
#include <string.h> /*for memset*/
#include <signal.h>
#include <errno.h>
(1)验证带外数据的发送与接收的各种方式:
【1】 使用SIGURG 信号,在信号处理函数中调用recv 函数,并使用MSG_OOB 标记,接收不在线的带外数据
主要代码:
客户端:(oobsend01.c)
void Send(int fd, const void *ptr, size_t nbytes, int flags){
if (send(fd, ptr, nbytes, flags) != (ssize_t)nbytes)
perror("send error");
}
void Write(int fd, void *ptr, size_t nbytes){
if (write(fd, ptr, nbytes) != nbytes)
perror("write error");
}
int main(int argc, char *argv[])
{
int sockfd;
int len;
struct sockaddr_in serv_addr; //服务器端网络地址结构体
struct sockaddr_in my_addr;//本地地址
char buf[BUFSIZ]; //数据传送的缓冲区
/*创建客户端套接字*/
if((sockfd=socket(PF_INET,SOCK_STREAM,0))<0){
perror("socket"); /*返回-1 出错*/
return 1;
}
//初始化
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_port= htons(8000);
serv_addr.sin_addr.s_addr=inet_addr("127.0.0.1");
/*连接*/
if(connect(sockfd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr))<0){
perror("connect");
return 1;
}
Write(sockfd, "123", 3);
printf("wrote 3 bytes of normal data\n");
sleep(1);
Send(sockfd, "4", 1, MSG_OOB);
printf("wrote 1 byte of OOB data\n");
sleep(3);
Write(sockfd, "56", 2);
printf("wrote 2 bytes of normal data\n");
sleep(1);
Send(sockfd, "7", 1, MSG_OOB);
printf("wrote 1 byte of OOB data\n");
sleep(1);
Write(sockfd, "89", 2);
printf("wrote 2 bytes of normal data\n");
sleep(1);
//每个输出操作之间有个sleep,目的是让每个write或send的数据作为单个TCP分节在本端发送并在对端接受
exit(0);
close(sockfd);//关闭套接字
return 0;
}
服务器端:(oobrecv01.c)
int listenfd, connfd;
void sig_urg(int signo)
{
int n;
char buff[100];
printf("SIGURG received !\n");
if((n = recv(connfd, buff, sizeof(buff)-1, MSG_OOB))<0)//读取带外数据
perror("recv");
buff[n] = 0; /* null terminate */
printf("read %d OOB byte: %s\n", n, buff);
}
int main(int argc, char *argv[])
{
int n,len;
struct sockaddr_in my_addr; //服务器网络地址结构体
struct sockaddr_in remote_addr; //客户端网络地址结构体
char buf[BUFSIZ];
memset(&my_addr,0,sizeof(my_addr)); //数据初始化--清零
my_addr.sin_family=AF_INET;
my_addr.sin_addr.s_addr=INADDR_ANY;
my_addr.sin_port=htons(8000);
/*创建服务器端套接字*/
if((listenfd=socket(PF_INET,SOCK_STREAM,0))<0){
perror("socket");
return 1;
}
int on = 1;
if((setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)))<0) {
perror("setsockopt failed");
exit(1);
}
/*绑定*/
if (bind(listenfd,(struct sockaddr *)&my_addr,sizeof(struct sockaddr))<0){
perror("bind");
return 1;
}
/*监听连接请求--监听队列长度为5*/
listen(listenfd,5);
for(;;){
/*等待客户端连接请求到达*/
if((connfd = accept(listenfd, (struct sockaddr *)NULL,NULL)) < 0) {
if(errno == EINTR)
continue;
else
perror("accept error");
}
signal(SIGURG, sig_urg);//安装信号处理函数
if((len=fcntl(connfd, F_SETOWN, getpid()))== -1)//设置已连接套接口属主
perror("fcntl");
//当有新的紧急指针到达时,接受进程被通知到
//首先,内核给接受进程套接字的属主进程发送SIGURG信号
//前提是接收进程已经调用fcntl为这个套接字建立属主,属主进程已为这个信号建立信号处理函数
for ( ; ; ) {
if ( (n = read(connfd, buf, sizeof(buf)-1)) == 0) {
printf("received EOF\n");
exit(0);
}
else if(n<0)
{
if (errno==EINTR)continue;
else
{
printf("read erro! %s\n",strerror(errno));
exit(0);
}
}
else {
printf("n= %d \n",n);
buf[n] = 0; /* null terminate */
printf("read %d bytes: %s\n", n, buf);
}
}
close(connfd);
}
close(listenfd);
return 0;
}
运行结果:
客户端:
服务器端:
分析:
该程序共发送9个字节,每个输出操作之间有一个1秒钟的sleep。间以停顿的目的是让每个write或send数据作为单个TCP分节在本端发送并在对端接收。发送进程带外数据的每次发送产生递交给接收进程的SIGURG信号,后者接着读入单个带外字节。
【2】 采用select 函数等待套接口描述字的异常条件到达,在recv 函数中使用MSG_OOB 标记,接收不在线的带外数据。
主要代码:
客户端与【1】中客户端程序(oobend01.c)一样。
服务器端:(oobrecv02.c)
ssize_t Read(int fd, void *ptr, size_t nbytes)
{
ssize_t n;
if ( (n = read(fd, ptr, nbytes)) == -1)
perror("read error");
return(n);
}
int main(int argc, char **argv)
{
int n;
struct sockaddr_in my_addr; //服务器网络地址结构体
struct sockaddr_in remote_addr; //客户端网络地址结构体
int listenfd, connfd, justreadoob=0;
fd_set rset, xset;
char buf[BUFSIZ];
memset(&my_addr,0,sizeof(my_addr)); //数据初始化--清零
my_addr.sin_family=AF_INET;
my_addr.sin_addr.s_addr=INADDR_ANY;
my_addr.sin_port=htons(8000);
/*创建服务器端套接字*/
if((listenfd=socket(PF_INET,SOCK_STREAM,0))<0){
perror("socket");
return 1;
}
int on = 1;
if((setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)))<0) {
perror("setsockopt failed");
exit(1);
}
/*绑定*/
if (bind(listenfd,(struct sockaddr *)&my_addr,sizeof(struct sockaddr))<0){
perror("bind");
return 1;
}
/*监听连接请求--监听队列长度为5*/
listen(listenfd,5);
for(;;){
/*等待客户端连接请求到达*/
if((connfd = accept(listenfd, (struct sockaddr *)NULL,NULL)) < 0) {
if(errno == EINTR)
continue;
else
perror("accept error");
}
FD_ZERO(&rset);//读集合
FD_ZERO(&xset);//异常集合
for ( ; ; ) {
FD_SET(connfd, &rset);
if(justreadoob==0) FD_SET(connfd, &xset);
if((select(connfd + 1, &rset, NULL, &xset, NULL))<0)
perror("select");
if (FD_ISSET(connfd, &xset)) {//有带外数据
if((n = recv(connfd, buf, sizeof(buf)-1, MSG_OOB))<0)
perror("recv");
buf[n] = 0; /* null terminate */
printf("read %d OOB byte: %s\n", n, buf);
justreadoob=1;//已经读过带外数据标记
FD_CLR(connfd, &xset);
}
if (FD_ISSET(connfd, &rset)) {//普通数据
if ( (n = Read(connfd, buf, sizeof(buf)-1)) == 0) {
printf("received EOF\n");
exit(0);
}
buf[n] = 0; /* null terminate */
printf("read %d bytes: %s\n", n, buf);
justreadoob=0;
}
}
close(connfd);
}
close(listenfd);
return 0;
}
运行结果:
客户端:
服务器端:
分析:
同一个带外数据不能读入多次,因为首次读入之后,内核就清空这个单字节的缓冲区。再次指定MSG_OOB标志调用recv时,它将返回EINVAL。上面的服务器端代码中,声明了一个justreadoob变量,用于指示我们是否刚刚读过带外数据,这个标志决定是否select异常条件。当设置justreadoob标志时,我们还得在异常描述符集中清除已连接套接字描述符对应的位。
【3】 使用sockatmark 检查带外数据是否到达,再利用recv 函数接收在线或不在线的带外数据。
主要代码:
客户端:(oobsend03.c)
与【1】中客户端程序差不多,只是将发送数据部分改为:
//3普通数据+1带外数据+1普通数据
//每个输出操作之间没有停顿
Write(sockfd, "123", 3);
printf("wrote 3 bytes of normal data\n");
Send(sockfd, "4", 1, MSG_OOB);
printf("wrote 1 byte of OOB data\n");
Write(sockfd, "5", 1);
printf("wrote 1 byte of normal data\n");
服务器端:(oobrecv03.c)
int Sockatmark(int fd){
int n;
if ( (n = sockatmark(fd)) < 0)
perror("sockatmark error");
return(n);
}
ssize_t Read(int fd, void *ptr, size_t nbytes){
ssize_t n;
if ( (n = read(fd, ptr, nbytes)) == -1)
perror("read error");
return(n);
}
int main(int argc, char **argv){
int n,on=1;
struct sockaddr_in my_addr; //服务器网络地址结构体
struct sockaddr_in remote_addr; //客户端网络地址结构体
int listenfd, connfd;
char buf[BUFSIZ];
memset(&my_addr,0,sizeof(my_addr));
my_addr.sin_family=AF_INET;
my_addr.sin_addr.s_addr=INADDR_ANY;
my_addr.sin_port=htons(8000);
if((listenfd=socket(PF_INET,SOCK_STREAM,0))<0){
perror("socket");
return 1;
}
if (bind(listenfd,(struct sockaddr *)&my_addr,sizeof(struct sockaddr))<0){
perror("bind");
return 1;
}
listen(listenfd,5);
//在线接收带外数据
if((setsockopt(listenfd,SOL_SOCKET,SO_OOBINLINE,&on,sizeof(on)))<0)
perror("setsockopt");
for(;;){
/*等待客户端连接请求到达*/
if((connfd = accept(listenfd, (struct sockaddr *)NULL,NULL)) < 0) {
if(errno == EINTR)
continue;
else
perror("accept error");
}
sleep(5);//以接受来自发送进程的所有数据,使得能够展示read停在带外标记上
for ( ; ; ) {
if (Sockatmark(connfd))//处于带外标记,=1,用来确定何时碰到带外字节
printf("at OOB mark\n");
if ( (n = Read(connfd, buf, sizeof(buf)-1)) == 0) {
printf("received EOF\n");
exit(0);
}
buf[n] = 0; /* null terminate */
printf("read %d bytes: %s\n", n, buf);
}
close(connfd);
}
close(listenfd);
return 0;
}
运行结果:
客户端:
服务器端:
分析:
服务器端程序中,我们希望在线接受带外数据,所以必须开启SO_OOBINLINE套接字选项。但是如果等到accept返回之后再在已连接套接字上开启这个选项,那时三路握手已经完成,带外数据也可能已经到达。因此就必须在监听套接字上开启这个选项,因为所有套接字选项会从监听套接字传承给已连接套接字。
接受连接之后,接收进程sleep一段时间以接受来自发送进程的所有数据。这样就能够展示read停在带外标记上,即使套接字接收缓冲区中已经有额外数据也不受影响。
程序循环调用read,并显示接收到数据,不过在调用read之前,先用sockatmark检查缓冲区指针是否处于带外标记。
尽管接收进程在首次调用read时接收端TCP已经接收了所有数据(因为接收进程调用了sleep),但是首次read调用因遇到带外标记而仅仅返回3个字节。下一个读入的字节是带外字节(值为4),因为我们早就告知内核在线放置带外数据。
(2)利用带外数据原理设计并实现客户-服务器心搏函数:
主要代码:
客户端:(hbc.c)
/* 给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 cnt; /* #probes since last server response */
static void sig_urg(int), sig_alrm(int);
ssize_t /* Write "n" bytes to a descriptor. */
writen(int fd, const void *vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
if (nwritten < 0 && errno == EINTR)
nwritten = 0; /* and call write() again */
else
return(-1); /* error */
}
nleft -= nwritten;
ptr += nwritten;
}
return(n);
}
void Writen(int fd, void *ptr, size_t nbytes)
{
if (writen(fd, ptr, nbytes) != nbytes)
perror("writen error");
}
void Send(int fd, const void *ptr, size_t nbytes, int flags)
{
if (send(fd, ptr, nbytes, flags) != (ssize_t)nbytes)
perror("send error");
}
void heartbeat_cli(int servfd_arg, int nsec_arg, int maxnprobes_arg)
{/* heartbeat_cli函数检查并且保存参数,给SIGURG和SIGALRM建立信号处理函数,将套接口的属主设为进程ID,alarm调度一个SIGALRM */
int len;
servfd = servfd_arg; /* set globals for signal handlers */
if( (nsec = nsec_arg) < 1)
nsec = 1;
if( (maxnprobes = maxnprobes_arg) < nsec)
maxnprobes = nsec;
cnt = 0;
signal(SIGURG, sig_urg);
if((len=fcntl(servfd, F_SETOWN, getpid()))== -1)//属主设置为本进程ID
perror("fcntl");
signal(SIGALRM, sig_alrm);
alarm(nsec);
}
static void sig_urg(int signo)
{/* 当一个带外通知到来时,就会产生这个信号。我们试图去读带外字节,但如果还没有到(EWOULDBLOCK)也没有关系。由于系统不是在线接收带外数据,因此不会干扰客户读取它的普通数据。既然服务器仍然存活,cnt就重置为0 */
int n;
char c;
if( ( n = recv(servfd, &c, 1, MSG_OOB) ) < 0 )
{
if(errno != EWOULDBLOCK)//EWOULDBLOCK 带外字节还没到达
perror("recv error");
}
printf("recv %d oob byte:%c ",n,c);
cnt = 0; /* reset counter 服务器还活着 */
return; /* may interrupt client code */
}
static void sig_alrm(int signo)
{/* 这个信号以有规律间隔产生。计数器cnt增1, 如果达到了maxnprobes,我们认为服务器或者崩溃或者不可达。在这个例子中,我们结束客户进程,尽管其他的设计也可以使用:可以发送给主循环一个信号,或者作为另外一个参数给heartbeat_cli提供一个客户函数,当服务器看来死掉时调用它 */
if( ++cnt > maxnprobes)
{
fprintf(stderr, "server is unreachable \n");
exit(0);//结束客户进程
}
Send(servfd, "1", 1, MSG_OOB);
printf("(now cnt=%d)\n",cnt);
alarm(nsec);
return; /* may interrupt client code */
}
int main(int argc, char **argv)
{
int sockfd;
int len;
struct sockaddr_in serv_addr;
struct sockaddr_in my_addr;
char buf[MAXLINE];
if((sockfd=socket(PF_INET,SOCK_STREAM,0))<0){
perror("socket");
return 1;
}
bzero(&serv_addr,sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons(8000);
serv_addr.sin_addr.s_addr=inet_addr("127.0.0.1");
if((connect(sockfd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr)))<0){
perror("connect");
return 1;
}
heartbeat_cli(sockfd,1,5);
while(1)
pause();
exit(0);
}
服务器端:(hbs.c)
static int servfd;
static int nsec; /* #seconds between each alarm */
static int maxnalarms; /* #alarms w/no client probe before quit */
static int cnt; /* #alarms since last client probe */
static void sig_urg(int), sig_alrm(int);
ssize_t /* Write "n" bytes to a descriptor. */
writen(int fd, const void *vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
if (nwritten < 0 && errno == EINTR)
nwritten = 0; /* and call write() again */
else
return(-1); /* error */
}
nleft -= nwritten;
ptr += nwritten;
}
return(n);
}
void Writen(int fd, void *ptr, size_t nbytes)
{
if (writen(fd, ptr, nbytes) != nbytes)
perror("writen error");
}
void Send(int fd, const void *ptr, size_t nbytes, int flags)
{
if (send(fd, ptr, nbytes, flags) != (ssize_t)nbytes)
perror("send error");
}
void heartbeat_serv(int servfd_arg, int nsec_arg, int maxnalarms_arg)
{
int len;
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);
if((len=fcntl(servfd, F_SETOWN, getpid()))== -1)
perror("fcntl");
signal(SIGALRM, sig_alrm);
alarm(nsec);
}
static void sig_urg(int signo)
{ /* 当一个带外通知收到时, 服务器试图读入它。就像客户一样,如果带外字节没有到达没有什么关系。带外字节被作为带外数据返回给客户。注意,如果recv返回EWOULDBLOCK错误,那么自动变量c碰巧是什么就送给客户什么。由于我们不用带外字节的值,所以这没有关系。重要的是发送1字节的带外数据,而不管该字节是什么。由于刚收到通知,客户仍存活,所以重置cnt为0 */
int n;
char c;
if( (n = recv(servfd, &c, 1, MSG_OOB)) < 0)
{
if(errno != EWOULDBLOCK)
perror("recv error");
}
printf("send %d oob byte:%c ",n,c);
Send(servfd, &c, 1, MSG_OOB); /* echo back out-of-hand byte */
cnt = 0; //每收到一个带外字节把该变量置为0
return; /* may interrupt server code */
}
static void sig_alrm(int signo)
{ /* 每秒cnt增1, 如果它到达了调用者指定的值maxnalarms,服务器进程将被终止。否则调度一下SIGALRM */
if( ++cnt > maxnalarms)
{
printf("no probes from client\n");
exit(0);
}
printf("(now cnt=%d)\n",cnt);
alarm(nsec);
return; /* may interrupt server code */
}
int main(int argc, char **argv)
{
int listenfd,connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr,servaddr;
void sig_chld(int);
if((listenfd=socket(PF_INET,SOCK_STREAM,0))<0){
perror("socket");
}
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(8000);
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
if ((bind(listenfd,(struct sockaddr *)&servaddr,sizeof(struct sockaddr)))<0){
perror("bind");
return 1;
}
listen(listenfd,5);
signal(SIGCHLD,sig_chld);
clilen=sizeof(cliaddr);
if((connfd=accept(listenfd,(struct sockaddr *)&cliaddr,&clilen))<0){
perror("accept");
}
ssize_t n;
char buf[MAXLINE];
heartbeat_serv(connfd,1,5);
again:
while((n==read(connfd,buf,MAXLINE))>0)
Writen(connfd,buf,n);
if(n<0&&errno==EINTR)
goto again;
else if(n<0)
perror("str_eco");
while(1)
{
pause();
}
return 0;
}
void sig_chld(int signo){
pid_t pid;
int stat;
while((pid=waitpid(-1,&stat,WNOHANG))>0)
printf("child %d terminated\n",pid);
return ;
}
运行结果:(左边是服务器端,右边是客户端)
分析:
如上图所示,客户端每隔1秒就向服务器发送一个带外字节“1”,服务器收取该字节后将导致他向客户端发送回一个带外字节,上图显示的就是服务端发送收到的带外字节给客户端以及客户端的接受情况。当客户端使用ctrl+c退出时,服务器端的计数器累加至5(已5秒没收到客户端的带外字节),就认定该连接失效,因为这时没有其他客户,就退出了。
参考文献
[1]《网络应用程序设计》,西安电子科技大学出版社,方敏、张彤编著
[2]《unix网络编程 卷1:套接字联网API》(第三版),人民邮电出版社
[3]Linux下的ioctl函数详解
https://www.cnblogs.com/tdyizhen1314/p/4896689.html
[4]Socket进程处理被中断的系统调用及Accept函数返回EINTR错误处理
https://blog.csdn.net/keshacookie/article/details/40717059?utm_source=tuicool
[5]linux signal 用法和注意事项
https://www.cnblogs.com/lidabo/p/4581026.html