关于close和shutdown
我们知道TCP是全双工的,可以在接收数据的同时发送数据。
假设有主机A在和主机B通信,可以认为是在两者之间存在两个管道。就像这样:
A ---------> B
A <--------- B
1.close
close可以用来关闭一个文件描述符。也就可以用来关闭一个套接字。
当关闭一个套接字时,该套接字不能再由调用进程使用。如果调用进程再去read、write就会出错。
我们知道关闭一个socket描述符时,会给对方发送一个FIN数据段。比如在主机A中close了与主机B通信的sockA。相当于终止了全双工的那两个管道。而从传输层来看,TCP会尝试将目前发送缓冲区中积压的数据发到链路层上,然后才会发起TCP的4次挥手以彻底关闭TCP连接。
之后在主机A中就不能用sockA来接收数据和发送数据了,同时由于是面向连接的。之前与sockA连接的sockB也收不到数据了。
如果依然通过sockB往主机A上写数据,开始会触发一个RST重连包,然后会收到一个SIGPIPE信号。
但是如果存在父子进程共用socket描述符的时候(比如fork了一个子进程),父子进程都有相同数值的文件描述符,且都是打开的。这时候去关闭父进程中的描述符并不会发送FIN包给对方。只有子进程也关闭了才会发送FIN。
原因在于,fork时,父子进程共享着套接字,套接字描述符的引用计数记录着共享着的进程个数。fork一次时相当于引用计数为2了。这时候去关闭一个,只会让引用计数减一。只有当引用计数为0时(也就是子进程也close了),才会发送FIN给连接方。
(就有点像windows下的句柄handle,是一个内核对象,当每被打开一次时,引用计数就会加一,CloseHandle时引用计数减一,若引用计数为0时,操作系统会回收这个内核对象)
2.shutdown
也可以用来关闭TCP数据传输的一个或两个方向。
原型:
SYNOPSIS
#include <sys/socket.h>
int shutdown(int sockfd, int how);
DESCRIPTION
The shutdown() call causes all or part of a full-duplex connection on
the socket associated with sockfd to be shut down. If how is SHUT_RD,
further receptions will be disallowed. If how is SHUT_WR, further
transmissions will be disallowed. If how is SHUT_RDWR, further recep‐
tions and transmissions will be disallowed.
RETURN VALUE
On success, zero is returned. On error, -1 is returned, and errno is
set appropriately.
参数:第一个表示socket描述符
第二个表示关闭读还是写。具体有三个值:
1)SHUT_WR:关闭读,表示不能用第一个参数对应的描述符往管道里面写数据了。(但是依然可以写数据)
2)SHUT_RD:关闭写,不能写数据了。(依然可以接收数据)
3)SHUT_RDWR:同时关闭读和写
3.close和shutdown的区别
1)close只会让引用计数减一,只有在引用计数减为零的时候才会给对方发送FIN段来断开连接。而shutdown会直接关闭连接,不受引用计数的限制,这就意味着在多进程中,只有调用了这个关闭了写端,那么其他进程也都不能写了。
2)close会关闭两端,shutdown可以选择关闭某个端。(这点非常有用处,比如主机A和B正在通信,A觉得没数据发送了,想要断开连接。然后A调用了close,那么B的数据也将发不过来,但是可以选择用shutdown关闭写端,这时候可以接收完B发的数据)
4.实例,用于更好的分析理解shutdown的机制:
client从标准输入中接收数据发送给server。server用来接收client的数据,并且回射回去。
这里做一个处理,client发送一次数据之后马上按下Ctrl+D(会导致fgets返回NULL),然后shutdown写端(相当于往server发送了FIN段)。server收到数据后,sleep10s再回射回去。
具体关于下面代码的理解可以参考:http://www.cnblogs.com/xcywt/p/8087677.html
#include<sys/types.h> #include<sys/socket.h> #include<sys/select.h> #include<netinet/in.h> #include<arpa/inet.h> #include<stdlib.h> #include<stdio.h> #include<string.h> #include<errno.h> #include<signal.h> #define CLIENTCOUNT 100 void sig_recvpipe(int sig) { printf("recv pipe = %d\n", sig); } int main(int argc, char **argv) { signal(SIGPIPE, sig_recvpipe); int listenfd = socket(AF_INET, SOCK_STREAM, 0); if(listenfd < 0) { perror("socket"); return -1; } unsigned short sport = 8080; if(argc == 2) { sport = atoi(argv[1]); } struct sockaddr_in addr; addr.sin_family = AF_INET; printf("port = %d\n", sport); addr.sin_port = htons(sport); addr.sin_addr.s_addr = inet_addr("127.0.0.1"); if(bind(listenfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); return -2; } if(listen(listenfd, 20) < 0) { perror("listen"); return -3; } struct sockaddr_in connaddr; int len = sizeof(connaddr); int i = 0, ret = 0; int client[CLIENTCOUNT]; for(i = 0; i<CLIENTCOUNT; i++) client[i] = -1; fd_set rset; fd_set allset; FD_ZERO(&rset); FD_ZERO(&allset); FD_SET(listenfd, &allset); int maxfd = listenfd; int nready = 0; char buf[1024] = {0}; while(1) { rset = allset; nready = select(maxfd+1, &rset, NULL, NULL, NULL); if(nready == -1) { perror("select"); return -3; } if(nready == 0) { continue; } if(FD_ISSET(listenfd, &rset)) { int conn = accept(listenfd, (struct sockaddr*)&connaddr, &len); if(conn < 0) { perror("accept"); return -4; } char strip[64] = {0}; char *ip = inet_ntoa(connaddr.sin_addr); strcpy(strip, ip); printf("new client connect, conn:%d,ip:%s, port:%d\n", conn, strip,ntohs(connaddr.sin_port)); FD_SET(conn, &allset); if(maxfd < conn) // update maxfd maxfd = conn; int i = 0; for(i = 0; i<CLIENTCOUNT; i++) { if(client[i] == -1) { client[i] = conn; break; } } if(i == CLIENTCOUNT) { printf("to many client connect\n"); exit(0); } if(--nready <= 0) continue; } for(i = 0; i < CLIENTCOUNT; i++) { if(client[i] == -1) continue; if(FD_ISSET(client[i], &rset)) { ret = read(client[i], buf, sizeof(buf)); if(ret == -1) { perror("read"); return -4; } else if(ret == 0) { printf("client close remove:%d\n", client[i]); FD_CLR(client[i], &allset); close(client[i]); client[i] = -1; // 要在这里移除 } // fputs(buf, stdout); printf("Recv client%d:%s", client[i], buf); sleep(10); write(client[i], buf, sizeof(buf)); memset(buf, 0, sizeof(buf)); if(--nready <= 0) continue; } } } close(listenfd); return 0; }
client端:
#include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<sys/select.h> #include<stdlib.h> #include<stdio.h> #include<string.h> void select_test(int conn) { int ret = 0; fd_set rset; FD_ZERO(&rset); int nready; int maxfd = conn; int fd_stdin = fileno(stdin); if(fd_stdin > maxfd) { maxfd = fd_stdin; } int stdinoff = 0; int len = 0; char readbuf[1024] = {0}; char writebuf[1024] = {0}; while(1) { FD_ZERO(&rset); if(!stdinoff) FD_SET(fd_stdin, &rset); FD_SET(conn, &rset); nready = select(maxfd+1, &rset, NULL, NULL, NULL); if(nready == -1) { perror("select"); exit(0); } else if(nready == 0) { continue; } if(FD_ISSET(conn, &rset)) { ret = read(conn, readbuf, sizeof(readbuf)); if(ret == 0) { printf("server close1\n"); break; } else if(-1 == ret) { perror("read1"); break; } fputs(readbuf, stdout); memset(readbuf, 0, sizeof(readbuf)); } if(FD_ISSET(fd_stdin, &rset)) { if(fgets(writebuf, sizeof(writebuf), stdin) == NULL) { #if 0 printf("After 5s client exit\n"); close(conn); sleep(5); exit(EXIT_FAILURE); #else shutdown(conn, SHUT_WR); stdinoff = 1; #endif } else { write(conn, writebuf, sizeof(writebuf)); memset(writebuf, 0, sizeof(writebuf)); } } } close(conn); } int sockfd = 0; int main(int argc, char **argv) { sockfd = socket(AF_INET, SOCK_STREAM, 0); if(sockfd < 0) { perror("socket"); return -1; } unsigned short sport = 8080; if(argc == 2) { sport = atoi(argv[1]); } struct sockaddr_in addr; addr.sin_family = AF_INET; printf("port = %d\n", sport); addr.sin_port = htons(sport); addr.sin_addr.s_addr = inet_addr("127.0.0.1"); if(connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { perror("connect"); return -2; } struct sockaddr_in addr2; socklen_t len = sizeof(addr2); if(getpeername(sockfd, (struct sockaddr*)&addr2, &len) < 0) { perror("getsockname"); return -3; } printf("Server: port:%d, ip:%s\n", ntohs(addr2.sin_port), inet_ntoa(addr2.sin_addr)); select_test(sockfd); close(sockfd); return 0; }
编译运行:
makefile:
CC=gcc CFLAGS=-Wall -g LIBS=-lpthread all:echoser echocli echoser:server.c $(CC) $< $(CFLAGS) $(LIBS) -o $@ echocli:client.c $(CC) $< $(CFLAGS) $(LIBS) -o $@ .PHONY:clean clean: rm -f *.o echoser echocli *~
client端运行:
在发送完1111111时马上按下Ctrl+d,将读端关闭。然后会发现10s以后还是可以收到server的数据。
xcy@xcy-virtual-machine:~/test/sock8_shutdown$ ./echocli port = 8080 Server: port:8080, ip:127.0.0.1 11111111 11111111 server close1 xcy@xcy-virtual-machine:~/test/sock8_shutdown$
server端运行:
server收到数据,10s后发送给client。之后还会read返回0,会认为是client关闭了,然后就把套接字关闭了。最后client也能收到read返回0。
xcy@xcy-virtual-machine:~/test/sock8_shutdown$ ./echoser port = 8080 new client connect, conn:4,ip:127.0.0.1, port:55852 Recv client4:11111111 client close remove:4 ^C xcy@xcy-virtual-machine:~/test/sock8_shutdown$
查看TCP状态:
当按下Ctrl+d时去查看状态,下面第2行可以看出来client已经变成CLOSE_WAIT状态了,server变成了FIN_WAIT2.
等10s到了,再去看client变成了TIME_WAIT状态(要保持2MSL)
具体可以参考:十一种状态 http://www.cnblogs.com/xcywt/p/8082428.html
xcy@xcy-virtual-machine:~$ netstat -an | grep 8080 tcp 0 0 127.0.0.1:8080 0.0.0.0:* LISTEN tcp 1 0 127.0.0.1:8080 127.0.0.1:55852 CLOSE_WAIT tcp 0 0 127.0.0.1:55852 127.0.0.1:8080 FIN_WAIT2 xcy@xcy-virtual-machine:~$ netstat -an | grep 8080 tcp 0 0 127.0.0.1:8080 0.0.0.0:* LISTEN tcp 0 0 127.0.0.1:55852 127.0.0.1:8080 TIME_WAIT xcy@xcy-virtual-machine:~$ netstat -an | grep 8080 xcy@xcy-virtual-machine:~$
我们可以看出来,client可以关闭写端,但是还是可以接收到到server发来的数据。