TCP连接关闭:close与shutdown的区别与处理SIGPIPE

TCP连接关闭:close与shutdown的区别与处理SIGPIPE

优雅关闭连接

前言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导致进程退出。

清芳推荐

根据世界杯数据来看,这届世界杯点球真有多少个?冷门惹人喜爱
宜人贷借款
bt365体育在线投注

宜人贷借款

📅 07-05 👀 9266
C罗与梅西的巅峰对决历史交手战绩回顾与未来赛事安排