问题来源:
近期开发一个同时存在长连接和短连接的TCP server。
在存在长连接的client时,server接收采用的是非阻塞的模式。如何确保在客户端异常结束进程之后,可判断客户端已死,断掉连接,结束线程。
解决方案:
1>、使用应用层面的心跳包
2>、使用协议层面的TCP 保活机制, keepalive。
具体实现:
心跳包不做过多解释,具体记录一下keepalive的实现。
协议解读(RFC1122#TCP Keep-Alives):
- TCP Keepalive虽不是标准规范,但操作系统一旦实现,默认情况下须为关闭,可以被上层应用开启和关闭。
- TCP Keepalive必须在没有任何数据(包括ACK包)接收之后的周期内才会被发送,允许配置,默认值不能够小于2个小时
- 不包含数据的ACK段在被TCP发送时没有可靠性保证,意即一旦发送,不确保一定发送成功。系统实现不能对任何特定探针包作死连接对待
- 规范建议keepalive保活包不应该包含数据,但也可以包含1个无意义的字节,比如0x0。
- SEG.SEQ = SND.NXT-1,即TCP保活探测报文序列号将前一个TCP报文序列号减1。SND.NXT = RCV.NXT,即下一次发送正常报文序号等于ACK序列号。
在linux中的实现:
可在系统中设置(这里使用的是ubuntu):
echo 1>
/proc/sys/net/ipv4/tcp_keepalive_time
echo 10 > /proc/sys/net/ipv4/tcp_keepalive_intvl
echo 10 > /proc/sys/net/ipv4/tcp_keepalive_probes
设置应用部分:
#include
#include
#include
#include
#include
#include
#include
#include
#include
int set_keepalive() { int ret = 0; int keep_alive = 1; int keep_idle = 10; int keep_interval = 10; int keep_count = 1; do{ /*开启keepalive属性*/ if((ret = setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, (void*)&keep_alive, sizeof(keep_alive))) < 0) break; /*10秒内没有任何数据交互则进行探测*/ if((ret = setsockopt(fd, SOL_TCP, TCP_KEEPIDLE, (void*)&keep_idle, sizeof(keep_idle))) < 0) break; /*探测时发探测包的时间间隔为5秒,缺省值:75(s)*/ if((ret = setsockopt(fd, SOL_TCP, TCP_KEEPINTVL, (void*)&keep_interval, sizeof(keep_interval))) < 0) break; /*探测重试的次数,全部超时则认定连接失效,缺省值:9(次)*/ if((ret = setsockopt(fd, SOL_TCP, TCP_KEEPCNT, (void*)&keep_count, sizeof(keep_count))) < 0) break; }while(0); return ret; }#include
#include
#include
#include
#include
#include
#include
#include
#include
/************************************************************************************* * @func static ssize_t my_read(int fd, void *buffer, size_t length) * @brief 向指定的socket读数据 * @param fd:socket描述符 buffer:接收缓存 length:数据长度 * @return reads:已接收的数据长度 **************************************************************************************/ static ssize_t my_read(int fd, void *buffer, size_t length) { ssize_t bytes_left, bytes_read = -1; char *ptr = (char *)buffer; int ret = -1; fd_set readfds; struct timeval tv; FD_ZERO(&readfds); FD_SET(fd, &readfds); bytes_left = length; while (bytes_left > 0) { tv.tv_sec = 0; tv.tv_usec = 1000; ret = select(fd + 1, &readfds, NULL, NULL, &tv); if (ret == 0) { break; } else if(ret < 0) { printf("#############select errno [%d]###############\n", errno); return -1; } if(FD_ISSET(fd, &readfds)) { bytes_read = read(fd, ptr, bytes_left); } else { break; } if (bytes_read < 0) { if (errno == EINTR || errno == EAGAIN) { continue; } else { printf("#############recv errno [%d]###############\n", errno); return -errno; } } else if (0 == bytes_read) { // return (length - bytes_left) ? (length - bytes_left) : (-1); break; } bytes_left -= bytes_read; ptr += bytes_read; } return (length - bytes_left); }
1,用户进程被杀死,tcp协议栈将异常断开连接,另一方可以获得通知,如errno 104(
Connection reset by peer)
2,用户进程退出且未调用closesocket,另一方可以获得通知errno 104
3,系统关机(正常的),另一方可以获得通知errno 104
4,系统关机(异常的),另一方不一定可以获得通知
5,系统断电,另一方无法获得通知
6,网络突然故障(例如网线被拔掉),另一方无法获得通知
由此可见,断电和断网时,对方无法获得通知,recv将一直阻塞直到永远。keepalive主要就是为了应对这种情况,若干保活包不能送达,对方即认为连接丢失。
这时errno读取为110(
Connection timed out)