优雅关闭连接
前言closeshutdown两函数的区别若被动方一直不发第三次挥手代码epoll试试
前言
今天突然发现最近搞的那个HTTP服务器的一个bug。 以前有个突然服务器崩溃的问题,不过是偶然发生的,所以一直搁置没有解决。 今天调试过程中突然发现一个致命问题,就是客户端发的HTTP请求在最后一次执行后直接调用了close,而服务端返回的数据导致这个客户端无法接受到,经百度发现确实存在使得进程直接退出的问题。
close
close函数或者shutdown函数调用后都会向对端发送FIN。 一般是客户端作为发起断开连接的主动方,而close函数调用后会将调用者的读写端全部关闭,因此,当被动关闭方要进行读写处理就会触发异常。
主动关闭端close以后收到被动关闭端发来的数据,会直接回复RST,内核连接断开,但此时被动关闭端的应用层并不知道已经断开。 若被动关闭方调用的是read,则会触发RST机制,然后断开。 若调用的是write,则会产生SIGPIPE中断,不进行捕捉则默认退出进程。
shutdown
函数原型
int shutdown(int sock, int howto);
其中 howto 为断开方式。有以下取值:
SHUT_RD:关闭读。这时应用层不应该再尝试接收数据,内核协议栈中就算接收缓冲区收到数据也会被丢弃。
SHUT_WR:关闭写。如果发送缓冲区中还有数据没发,会将将数据传递到目标主机。
SHUT_RDWR:关闭读和写。相当于close()了。
FIN 是指"我不再发送数据",因此shutdown() 关闭读不会给对方发FIN, 关闭写才会发FIN。
两函数的区别
close函数会关闭套接字,如果有其他进程共享,那么这个套接字仍然是打开的,可以读写,并不会发生四次挥手;
shutdown则会根据how选项切断进程共享的套接字的该功能,比如所有试图读的进程都会接收到EOF标识,所有试图写的进程将会检测到SIGPIPE信号;
注意:showdown后仍然要调用close关闭socket
若被动方一直不发第三次挥手
第三次挥手,是由被动方主动触发的,比如调用close()。
如果由于代码错误或者其他一些原因,被动方就是不执行第三次挥手。
这时候,主动方会根据自身第一次挥手的时候用的是 close() 还是 shutdown(fd, SHUT_WR) ,有不同的行为表现。
如果是 shutdown(fd, SHUT_WR) ,说明主动方其实只关闭了写,但还可以读,此时会一直处于 FIN-WAIT-2, 死等被动方的第三次挥手。
如果是 close(), 说明主动方读写都关闭了,这时候会处于 FIN-WAIT-2一段时间,这个时间由 net.ipv4.tcp_fin_timeout 控制,一般是 60s,这个值正好跟2MSL一样 。超过这段时间之后,状态不会变成 TIME-WAIT,而是直接变成CLOSED。
代码
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
void sysError(const char* str){
perror(str);
exit(1);
}
void sign_func(){
printf("Catch SIGPIPE\n");
sleep(1);
}
int main(){
struct sockaddr_in serveraddr, clientaddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(6666);
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
int lfd,cfd;
lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd==-1) sysError("socket error ");
if(bind(lfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr))==-1) sysError("bind error ");
if(listen(lfd,128)==-1) sysError("listen error ");
socklen_t clientaddrlen=sizeof(clientaddr);
if((cfd = accept(lfd,(struct sockaddr*)&clientaddr,&(clientaddrlen)))==-1) sysError("accept error ");
struct sigaction sa;
memset(&sa, '\0', sizeof(sa));
sa.sa_handler = sign_func;
sigfillset(&sa.sa_mask);
sigaction(SIGPIPE, &sa, NULL);
char buffer[BUFSIZ];
while(1){
int n = read(cfd,buffer,sizeof(buffer));
write(STDOUT_FILENO,buffer,n);
for(int i=0;i buffer[i] = toupper(buffer[i]); sleep(1); } write(cfd,buffer,n); } close(cfd); close(lfd); return 0; } 结果如下: 客户端发完数据不等服务端回复立刻关闭,强制退出效果和close一样 服务端等待数据处理完成尝试write写时就会产生SIGPIPE 服务端任务逻辑是将客户端发来的数据小写转大写,sleep的作用表示服务端要处理很长时间。 epoll试试 #include #include #include #include #include #include #include #include #include #include #include #define MAXLINE 8192 #define SERV_PORT 6666 #define OPEN_MAX 5000 void sysError(const char* str){ perror(str); exit(1); } void sign_func(){ printf("Catch SIGPIPE\n"); sleep(1); } int main(){ int listenfd, connfd, sockfd; int n, num = 0; ssize_t nready, efd, res; char buf[MAXLINE], str[INET_ADDRSTRLEN]; socklen_t cli_len; struct sockaddr_in cliaddr, servaddr; struct epoll_event tep, ep[OPEN_MAX]; listenfd = socket(AF_INET, SOCK_STREAM, 0); int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); listen(listenfd, 20); struct sigaction sa; memset(&sa, '\0', sizeof(sa)); sa.sa_handler = SIG_IGN; sigfillset(&sa.sa_mask); sigaction(SIGPIPE, &sa, NULL); efd = epoll_create(OPEN_MAX); if (efd == -1) sysError("epoll_create error"); tep.events = EPOLLIN; tep.data.fd = listenfd; res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep); if (res == -1) sysError("epoll_ctl error"); while (1) { nready = epoll_wait(efd, ep, OPEN_MAX, -1); if (nready == -1) sysError("epoll_wait error"); for (int i = 0; i < nready; ++i) { if (!(ep[i].events & EPOLLIN)) continue; if (ep[i].data.fd == listenfd) { cli_len = sizeof(cliaddr); connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cli_len); printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port)); printf("cfd %d---client %d\n", connfd, ++num); tep.events = EPOLLIN; tep.data.fd = connfd; res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep); if (res == -1) sysError("epoll_ctl error"); } else { sockfd = ep[i].data.fd; n = read(sockfd, buf, MAXLINE); // if (n == 0) // { // res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL); // if (res == -1) // sysError("epoll_ctl error"); // close(sockfd); // printf("client[%d] closed connection\n", sockfd); // } // else if (n < 0) // { // perror("read n<0 error:"); // res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL); // close(sockfd); // } // else // { for (int i = 0; i < n; ++i){ buf[i] = toupper(buf[i]); sleep(1); } //write(STDOUT_FILENO, buf, n); write(sockfd, buf, n); // } } } } close(listenfd); return 0; } 同时开启两个客户端进行连接,其中一个客户端发送完立刻断开 结果如下: 可以看到,其中一个客户端断开以后,服务端无论是通过信号捕捉还是SIG_IGN忽略信号,其他连接再次建立或者传输数据均不受影响。 而没有这两个动作处理的话就会使得服务器直接退出,这也验证了前言中我那个服务器偶然崩溃的现象。 把上面例子中的read函数注释取消掉再次实验: #include #include #include #include #include #include #include #include #include #include #include #define MAXLINE 8192 #define SERV_PORT 6666 #define OPEN_MAX 5000 void sysError(const char* str){ perror(str); exit(1); } void sign_func(){ printf("Catch SIGPIPE\n"); sleep(1); } int main(){ int listenfd, connfd, sockfd; int n, num = 0; ssize_t nready, efd, res; char buf[MAXLINE], str[INET_ADDRSTRLEN]; socklen_t cli_len; struct sockaddr_in cliaddr, servaddr; struct epoll_event tep, ep[OPEN_MAX]; listenfd = socket(AF_INET, SOCK_STREAM, 0); int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); listen(listenfd, 20); // struct sigaction sa; // memset(&sa, '\0', sizeof(sa)); // sa.sa_handler = SIG_IGN; // sigfillset(&sa.sa_mask); // sigaction(SIGPIPE, &sa, NULL); efd = epoll_create(OPEN_MAX); if (efd == -1) sysError("epoll_create error"); tep.events = EPOLLIN; tep.data.fd = listenfd; res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep); if (res == -1) sysError("epoll_ctl error"); while (1) { nready = epoll_wait(efd, ep, OPEN_MAX, -1); if (nready == -1) sysError("epoll_wait error"); for (int i = 0; i < nready; ++i) { if (!(ep[i].events & EPOLLIN)) continue; if (ep[i].data.fd == listenfd) { cli_len = sizeof(cliaddr); connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cli_len); printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port)); printf("cfd %d---client %d\n", connfd, ++num); tep.events = EPOLLIN; tep.data.fd = connfd; res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep); if (res == -1) sysError("epoll_ctl error"); } else { sockfd = ep[i].data.fd; n = read(sockfd, buf, MAXLINE); if (n == 0) { res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL); if (res == -1) sysError("epoll_ctl error"); sleep(6); close(sockfd); printf("client[%d] closed connection\n", sockfd); } else if (n < 0) { perror("read n<0 error:"); res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL); close(sockfd); } else { for (int i = 0; i < n; ++i){ buf[i] = toupper(buf[i]); sleep(1); } //write(STDOUT_FILENO, buf, n); write(sockfd, buf, n); write(sockfd, buf, n); } } } } close(listenfd); return 0; } read的注释取消掉,注意这次代码有两次write 因为当主动方调用close后,被动方调用write时,主动方第一次会产生一个,当前接收到的数据丢弃并返回RST的动作,此时被动端收到RST直接关闭。再次调用write向主动方写,就会触发SIGPIPE导致进程退出。