TCP建立一个连接需要三次握手,但是终止一个连接需要四次挥手: 1. 当某个应用进程主动调用close时,它向对端发送一个FIN分节,表示这端需要关闭连接 2. 当对端接收到FIN分节时,read函数返回0,它的TCP发送一个ACK,表示接收到了主动close端的FIN分节。主动关闭端的TCP在接受到ACK后处于FIN_WAIT状态,表示需要等待对端的FIN分节到达。 3. 被动关闭端发送FIN分节给主动关闭端,主动关闭端收到FIN后,发送ACK给对端,处于TIME_WAIT状态,表示等待被动关闭端的ACK确认 4. 被动关闭端接收到ACK后四次挥手完成,两端套接字关闭完成
close在TCP中的默认行为是把该套接字标记为已关闭,然后立即返回到调用进程。该套接字描述符不能再由调用进程使用,也就是不能再作为read,write的第一个参数。然而TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后再发送TCP连接终止序列。
close的错误返回三种情况: - EBADF:表示参数fd为非法描述符 - EINTR:close调用被信号中断 - EIO:I/O时出现错误
上述代码中close被调用2次,因为子进程和父进程共享了client_fd,其引用计数为2,如果子进程只调用一次,则会导致client_fd的套接字永远不能关闭,导致进程描述符耗尽而无法为其他请求提供服务。并且当close调用时,TCP的读写2端都被关闭。
close在网络编程中的局限: - 当一个描述符被共享使用时,调用close函数只是将其引用计数减一,仅仅在引用计数为0的时候,该套接字才被关闭。 - close终止读和写2个方向的数据传输。TCP为全双工协议,有时候当我们完成数据发送后,可能需要等待对端发送数据,此时可以调用shutdown来实现此功能。
howto的可选项: SHUT_RD:关闭连接的读——套接字中不再读取数据,而且套接字接受缓冲区中的数据也会被丢弃。进程不能再对这个套接字调用任何读取函数。 SHUT_WR:关闭连接的写——对于TCP套接字,这成为半关闭。当前留在套接字发送缓冲区中的数据将被发送掉,然后TCP的正常连接终止序列。无论这个套接字描述符的引用计数是否为0,shutdown都会激发TCP终止序列,以后进程不能再对这个套接字调用任何写函数。 SHUT_RDWR:关闭读、写——相当于分别调取了shutdown两次并传递参数SHUT_RD和SHUT_WR。shutdown使用此选项与close的区别是,shutdown立马关闭套接字的读写通道,但是close只会在引用计数为0的情况才关闭读写通道。
主程序
#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/socket.h>#include <sys/select.h>#include <arpa/inet.h>#include <netinet/in.h>extern void client_echo(FILE *fp,int sockfd);int main(){ int sockfd = socket(AF_INET,SOCK_STREAM,0); struct sockaddr_in serv_addr; struct sockaddr_in client_addr; int client_len; serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(LISTEN_PORT); char *strip = inet_pton(AF_INET,"127.0.0.1",&serv_addr.sin_addr); int ret = connect(sockfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)); client_echo(stdin,sockfd);//从stdin读取数据发送到sockfd并回写 return 0;}使用close的实现
void client_echo(FILE *fp,int sockfd){ if(fp == NULL) return; int fd = fineno(fp); fd_set read_set; FD_ZERO(&read_set); int stdin_eof = 0; int nread = 0; char recvbuf[1024]; while(1){ FD_SET(fd,&read_set); FD_SET(sockfd,&read_set); int maxfd = max(fd,sockfd); int ret = select(maxfd + 1,&read_set,NULL,NULL,NULL); if(ret < 0){ if(errno == EINTR) continue; // 由于中断引发的失败,重试 else { // 其他错误,退出 perror("select"); return; } } if(FD_ISSET(sockfd,&read_set)){ if( (nread = read(sockfd,recvbuf,sizelf(recvbuf))) == 0){ PRintf("read EOF from serv/n"); close(sockfd); exit(1); } fputs(recvbuf,stdout); } if(FD_ISSET(fd,&read_set)){ if(fgets(recvbuf,sizeof(recvbuf),fd) == 0){ printf("read EOF from terminate/n"); close(sockfd); exit(1); } write(sockfd,recvbuf,strlen(recvbuf)); } }}在上面的程序中,当客户端从终端读取到CTRL+D的时候将使fgets函数返回0,此时引发客户端主动关闭close,退出客户端的连接处理函数后,将立即退出程序(main函数结束)。此时服务端如果有数据正在发送,则将会丢失。处理这种问题的方式是,当客户端不再write时,只关闭TCP的write,但是任然保留其read通道。可以通过shutdown来实现。
使用shutdown的实现
void client_echo(FILE *fp,int sockfd){ if(fp == NULL) return; int fd = fineno(fp); fd_set read_set; FD_ZERO(&read_set); int stdin_eof = 0; int nread = 0; char recvbuf[1024]; int maxfd; while(1){ FD_SET(fd,&read_set); FD_SET(sockfd,&read_set); if(stdin_eof != 0){ FD_CLR(fd,&read_set); maxfd = sockfd; } else maxfd = max(fd,sockfd); int ret = select(maxfd + 1,&read_set,NULL,NULL,NULL); if(ret < 0){ if(errno == EINTR) continue; // 由于中断引发的失败,重试 else { // 其他错误,退出 perror("select"); return; } } if(FD_ISSET(sockfd,&read_set)){ if( (nread = read(sockfd,recvbuf,sizelf(recvbuf))) == 0){ if(stdin_eof == 1){ return; } else{ printf("read EOF from serv/n"); close(sockfd); exit(1); } } fputs(recvbuf,stdout); } if(FD_ISSET(fd,&read_set)){ if(fgets(recvbuf,sizeof(recvbuf),fd) == 0){ stdin_eof = 1; shutdown(sockfd,SHUT_WR); // send FIN FD_CLR(fd,&read_set); } else write(sockfd,recvbuf,strlen(recvbuf)); } }}在shutdown_client中,当从终端读取到EOF时,将调用shutdown关闭套接字的写通道,但是此套接字任然可以从服务端读取数据,保证了数据不会丢失。
新闻热点
疑难解答