TCP的连接终止序列:
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函数
#includeint close(int fd);// 成功返回0,失败返回-1,并且置位errno
close在TCP中的默认行为是把该套接字标记为已关闭,然后立即返回到调用进程。该套接字描述符不能再由调用进程使用,也就是不能再作为read,write的第一个参数。然而TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后再发送TCP连接终止序列。
close的错误返回三种情况:
- EBADF:表示参数fd为非法描述符
- EINTR:close调用被信号中断
- EIO:I/O时出现错误
一个常见的TCP并发服务器模型:
#include#include#include#include#include#includeint 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);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); int ret = bind(sockfd,(struct sockaddr* )&serv_addr,sizeof(serv_addr));
ret = listen(sockfd,LISTENQ); while(1){
client_len = sizeof(client_addr); int client_fd = accept(sockfd,(struct sockaddr *)&client_addr,&client_len);
pid_t pid = fort(); if(pid < 0){ continue;
} else if(pid == 0){
do_serv();
close();
} else close(client_fd);
} return 0;
}
上述代码中close被调用2次,因为子进程和父进程共享了client_fd,其引用计数为2,如果子进程只调用一次,则会导致client_fd的套接字永远不能关闭,导致进程描述符耗尽而无法为其他请求提供服务。并且当close调用时,TCP的读写2端都被关闭。
close在网络编程中的局限:
- 当一个描述符被共享使用时,调用close函数只是将其引用计数减一,仅仅在引用计数为0的时候,该套接字才被关闭。
- close终止读和写2个方向的数据传输。TCP为全双工协议,有时候当我们完成数据发送后,可能需要等待对端发送数据,此时可以调用shutdown来实现此功能。
shutdown函数
int shutdown(int sockfd,int howto);
//返回:成功返回0,失败返回-1
howto的可选项:
SHUT_RD:关闭连接的读——套接字中不再读取数据,而且套接字接受缓冲区中的数据也会被丢弃。进程不能再对这个套接字调用任何读取函数。
SHUT_WR:关闭连接的写——对于TCP套接字,这成为半关闭。当前留在套接字发送缓冲区中的数据将被发送掉,然后TCP的正常连接终止序列。无论这个套接字描述符的引用计数是否为0,shutdown都会激发TCP终止序列,以后进程不能再对这个套接字调用任何写函数。
SHUT_RDWR:关闭读、写——相当于分别调取了shutdown两次并传递参数SHUT_RD和SHUT_WR。shutdown使用此选项与close的区别是,shutdown立马关闭套接字的读写通道,但是close只会在引用计数为0的情况才关闭读写通道。
close和shutdown的对比示例
主程序
#include#include#include#include#include#include#includeextern 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关闭套接字的写通道,但是此套接字任然可以从服务端读取数据,保证了数据不会丢失。