欢迎光临
我们一直在努力

网络程序中常用的三种心跳机制----服务器端

下面我们依次看一下这三种模型:

在TCP协议中提供了保活计时器,这个计时器默认是两个小时,可以看一下它们的相关内核参数:

套接字选项提供了对它的控制

 再看一下服务器端实现

 setsockopt(lisfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&re, re); setsockopt(lisfd, SOL_SOCKET, SO_REUSEPORT, (const void *)&re, re); setnonblock(lisfd); setkeepalive(lisfd, 10, 2, 10);//将监听描述符号设置了这个属性 accept后返回的文件描述符都会继承此属性 void loop(int lisfd) { int epfd = epoll_create(MAXCLIENT); if(epfd < 0) return; struct epoll_event events[MAXCLIENT]; memset(events, 0, sizeof(struct epoll_event) * MAXCLIENT); struct epoll_event ev; ev.data.fd = lisfd; ev.events = EPOLLIN | EPOLLET; epoll_ctl(epfd, EPOLL_CTL_ADD, lisfd, &ev); struct sockaddr_in cliaddr; socklen_t len = sizeof(cliaddr); int clifd = -1; char buff[SIZE] = {0}; while(1) { lable: int ready = epoll_wait(epfd, events, MAXCLIENT, -1); if(ready == -1) { if(errno == EINTR) goto lable; } if(ready) { int i; for(i = 0;i < ready; ++i) { if(events[i].data.fd == lisfd) { lable2: while((clifd = accept(lisfd, (struct sockaddr *)&cliaddr, &len)) > 0) { setnonblock(clifd); ev.data.fd = clifd; ev.events = EPOLLIN | EPOLLPRI | EPOLLERR; epoll_ctl(epfd, EPOLL_CTL_ADD, clifd, &ev); } if(clifd == -1) { if(errno != EAGAIN){ goto lable2; } } }else if(events[i].events & EPOLLIN) { size_t rd = read(events[i].data.fd, buff, SIZE); if(rd) { printf("%s\n", buff); write(events[i].data.fd, buff, rd); memset(buff, 0, SIZE); } if (rd == 0) { fprintf(stderr, "closed\n"); close(events[i].data.fd); epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, &(events[i])); } if (rd == -1)// 这里在《UNIX 网络编程》中  说到 如果探测包长时间没有收到反馈,可能就会由路由器发送icmp的错误,errno=EHOSTUNREACH 或者 ETIMEDOUT {                 //但是我的试验结果是并没有走到这里  而是rd== 0,服务器向客户端IP 发送了一个RST 。 这里如果服务器继续往这个客户端写的话,那么就会
                                      //造成路由器发送ICMP给服务器  服务器send 返回-1 errno=EHOSTUNREACH 或者 ETIMEDOUT。
if (errno ==ECONNRESET ) { fprintf(stderr, "connect reset\n"); } else if (errno == EHOSTUNREACH) { fprintf(stderr, "host unreach\n"); } else if (errno ==ETIMEDOUT ) { fprintf(stderr, "timeout\n"); } } } else if(events[i].events & EPOLLPRI) { // } else if(events[i].events & EPOLLOUT) { // } else if(events[i].events & EPOLLERR) { fprintf(stderr, "have error\n" ); } } } } }

我将客户端与服务器之间的网络断开,抓取服务器端的数据包如下

最终服务器在read返回0,表明客户端已断开。注意这里与客户端正常关闭返回没有什么区别,从应用层是没有办法知道与客户端断开的(除非继续向客户端send  直至发生重传放弃)。那么对后续的处理就不知道是正常close还是因网络原因临时断开。如果服务器并不关心这些那么服务器就可以很好处理不断重传问题,它会在超时后主动切断网络。客户端最后再连接上时也会被服务器拒绝。

在这里的实例客户端与服务器实现简单的回射,一段时间未交互时,服务器主动发送HEARTBEAT,然后客户端收到后回应HEARTBEAT,当多次未回应时表示网络已经断开。主要代码如下:

 time_t cache; int n; int maxfd = -1; while(1) { lable: int ready = epoll_wait(epfd, events, MAXCLIENT, 300); if(ready == -1) { if(errno == EINTR) goto lable; } time_t now = time(NULL); cache = now; if(ready) { int i; for(i = 0;i < ready; ++i) { if(events[i].data.fd == lisfd) { lable2: while((clifd = accept(lisfd, (struct sockaddr *)&cliaddr, &len)) > 0) { maxfd = clifd > maxfd ? clifd : maxfd; setnonblock(clifd); ev.data.fd = clifd; ev.events = EPOLLIN | EPOLLPRI | EPOLLERR; epoll_ctl(epfd, EPOLL_CTL_ADD, clifd, &ev); base->cli_map[clifd].fd = clifd;//以文件描述符为数组标号 base->cli_map[clifd].idle = now; //记录时间 base->cli_map[clifd].times = 0;  //已经探测了多少次 base->cli_map[clifd].interval= 20; //探测间隔时间 base->cli_map[clifd].flags = CONNECTED; //状态 (base->cliNum)++; //客户端个数 } if(clifd == -1) { if(errno != EAGAIN){ goto lable2; } } }else if(events[i].events & EPOLLIN) { size_t rd = read(events[i].data.fd, buff, SIZE); if(rd) { printf("%s\n", buff); if (strcmp(buff, hb) == 0)  //收到的是HEARTBEAT { //if(base->cli_map[events[i].data.fd].times){ // (base->cli_map[events[i].data.fd].times)--;  //探测次数 //} }else{ write(events[i].data.fd, buff, rd); } base->cli_map[events[i].data.fd].idle = cache; //更新交互时间 base->cli_map[events[i].data.fd].times = 0; //无反馈坚持次数至0 memset(buff, 0, SIZE); } if (rd == 0) { fprintf(stderr, "client close normally\n" ); close(events[i].data.fd); memset(&(base->cli_map[events[i].data.fd]),0, sizeof(struct Event)); } } else if(events[i].events & EPOLLPRI) { //  } } } for (n = 0; n <= maxfd; ++n) { if ((base->cli_map[n].flags == CONNECTED) && (cache >= (base->cli_map[n].idle +base->cli_map[n].interval)) && (base->cli_map[n].times < 3)) {         //如果超时没有数据交互 且探测次数小于3次 则发送HB write(base->cli_map[n].fd, hb, strlen(hb));      base->cli_map[n].idle = cache;  //更新时间 (base->cli_map[n].times)++; //次数+1 } else if (base->cli_map[n].times == 3)  //如果是3次了则表明可能断开了 { fprintf(stderr, "%d may be offline \n", base->cli_map[n].fd); base->cli_map[n].flags = OFFLINE; base->cli_map[n].times = 0; ev.events = EPOLLIN; ev.data.fd = base->cli_map[n].fd; epoll_ctl(epfd, EPOLL_CTL_DEL, base->cli_map[n].fd , &ev); close(base->cli_map[n].fd); //memset(&(base->cli_map[i]), 0, sizeof(struct Event));  } } } }

这里只是在单个线程中简单的实现,如果长时间没有交互会定时的发送HB,如果网络断开的话那么在规定时间没有相应就认为是网络断开了,这时能够知道可能网络断开这件事可以对此连接进行下一步的处理,这里可以将HB放在单个线程中去实现。

带外数据的HB的实现和上面大致相同,只是注意紧急数据只用一个字节,发送和接收都为单个字节,否则多于字节会当成正常数据来接收。在EPOLL中EPOLLPRI表示接收到了紧急数据,在select异常表示收到了紧急数据。

这样做的好处在于有利用读取时的数据分离,但整体来说和正常数据HB相同。

另外:要注意的是TCP keepalive的心跳机制,在《unix网络编程》一书中提到,对于大多数内核这个参数是基于整个内核维护时间参数的,而不是基于每个套接字的维护的,因此如果修改了keepalive时间,可能会影响到该主机上所有开启这个选项的套接字。但是对于一般服务器内只有一个server下无影响,再者这种情况下是无需对端特别去实现的。  这里如果只是避免一端在断开网络的情况下 不断尝试重传并且不在乎与另一端断开网络的状态还是使用TCP keepalive较为方便,但是如果十分关心一端断线的状态那就使用应用层自己实现的心跳机制。

 

  • 海报
海报图正在生成中...
赞(0) 打赏
声明:
1、本博客不从事任何主机及服务器租赁业务,不参与任何交易,也绝非中介。博客内容仅记录博主个人感兴趣的服务器测评结果及一些服务器相关的优惠活动,信息均摘自网络或来自服务商主动提供;所以对本博客提及的内容不作直接、间接、法定、约定的保证,博客内容也不具备任何参考价值及引导作用,访问者需自行甄别。
2、访问本博客请务必遵守有关互联网的相关法律、规定与规则;不能利用本博客所提及的内容从事任何违法、违规操作;否则造成的一切后果由访问者自行承担。
3、未成年人及不能独立承担法律责任的个人及群体请勿访问本博客。
4、一旦您访问本博客,即表示您已经知晓并接受了以上声明通告。
文章名称:《网络程序中常用的三种心跳机制----服务器端》
文章链接:https://www.456zj.com/7011.html
本站资源仅供个人学习交流,请于下载后24小时内删除,不允许用于商业用途,否则法律问题自行承担。

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址