人们的视线总是会指向那些他们有兴趣靠近、试探、寻找或者拥有的东西。---摘自:《人生十二堂课》
当一个TCP连接终止时,需要调用close或者shutdown函数来关闭对应的套接字。那么对于这两个关闭接口,各自适用于什么情况呢,有什么不同呢?以及如何通过套接字属性控制它们产生不同的行为呢?
下面我们就来详细说一说close以及shutdown函数的用法以及它们的区别:
close:关闭套接字
#include <unistd.h>
int close(int sockfd);
返回值:成功:0 出错:-1
参数:sockfd:要关闭的套接字描述符
功能:close一个TCP套接字的默认行为是把该套接口做上“已关闭”标记,并立即返回到进程。
这个套接字不能再为进程所用:它不能作为read或write函数的第一个参数。但TCP将试图发送已在发送队列中待发的任何数据,然后按正常的TCP连接终止序列进行操作(四次挥手)。图示如下图1:
图1:调用close函数执行过程
为了更好的理解close函数的功能,我们需要引用一个“描述符引用计数”的概念。
描述符引用计数:
对于套接字是和linux文件一样有“引用计数”的属性的,如果一个服务器通过fork函数创建子进程来对已连接套接字(sockfd)进行处理,那么当它调用完fork后,已连接套接字描述符(sockfd)的引用计数就从一变成了二(类似于硬连接)。对sockfd调用close将使其引用计数的值从二减为一。并不会触发TCP的四次挥手函数。只有当套接字的引用计数为0时,才会关闭套接字触发TCP的四次挥手。
通过上面介绍我们可以分析出close函数有两个局限:
- close把描述符的引用计数减一,仅在该计数变为0时,才关闭套接字。
- close终止读和写两个方向的数据传送。
那么如果我们想要在调用终止函数时就关闭套接字,并且想要灵活的关闭套接字的读或者写时,显然缺省属性的close时满足不了需求的,下面我们引进一个关闭套接字更灵活的函数shutdown(类似于linux的关机命令)。
shutdown函数:
#include <sys/socket.h>
int shutdown(int sockfd,int howto);
返回值:成功:0 失败:-1
参数:sockfd:要关闭的套接字描述符
howto:控制shutdown函数的行为
howto有三种值,它们分别控制shutdown函数的不同行为:
- SHUT_RD:关闭连接的读这一半--套接字中不再有数据可接收,而且套接字接收缓冲区中现有数据都被丢失。进程不能再对这样的套接字调用任何读函数。对一个TCP套接字这样调用shutdown函数后,由该套接字接收的来自对端的任何数据都被确认,然后悄然丢弃。
- SHUT_WR:关闭连接的写这一半--对于TCP套接字,这称为半关闭。当前留在套接字发送缓冲区中的数据将被发送掉,后跟TCP的正常连接终止序列。不管套接字描述符的引用计数是否等于0,这样的写半部关闭照样执行。进程不能再对这样的套接字调用任何写函数。
- SHUT_RDWR:连接的读半部和写半部都关闭--这与调用shutdown两次等效:第一次调用指定SHUT_RD,第二次调用指定SHUT_WR。
举个服务端的例子:服务器通过fork创建子进程对已连接套接字进行业务处理,在父进程处分别调用close以及shutdown函数对已连接套接字进行关闭
int main(int argc,char **argv)
{
//1. 声明ipv4套接字地址结构变量
struct sockaddr_in server_addr,client_addr;
socklen_t addr_len = sizeof(struct sockaddr_in);
bzero(&server_addr,sizeof(struct sockaddr_in));
bzero(&client_addr,sizeof(struct sockaddr_in));
int sockfd,conn_fd;
//网络地址结构中端口和ip都要用网络序进行存储
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons((uint16_t)server_port);
//server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//创建ipv4 字节流套接字
if((sockfd = Socket(AF_INET,SOCK_STREAM,0)) == -1)//0 right -1 error
{
return -1;
}
if(bind(sockfd,(const struct sockaddr *)&server_addr,addr_len) == -1)//0 right -1 error
{
fprintf(stderr, "bind fail value of erron:%d Error message: %s\n", errno,strerror(errno));
return -1;
}
if(listen(sockfd,listen_num)== -1)//0 right -1 error
{
fprintf(stderr, "listen fail value of erron:%d Error message: %s\n", errno,strerror(errno));
return -1;
}
//接收SIGCHLD信号
signal(SIGCHLD,SIGCHLDHandler);
pid_t pid;
while(1)
{
conn_fd = accept(sockfd,(struct sockaddr *)&client_addr,&addr_len);
if(conn_fd == -1)
{
fprintf(stderr, "accept fail value of erron:%d Error message: %s\n", errno,strerror(errno));
continue;
}
if((pid = Fork()) == 0)
{
close(sockfd);//在子进程中关闭监听套接口
doit(conn_fd,&client_addr);//调用处理函数
close(conn_fd);//关闭已连接套接口
exit(0);
}
close(conn_fd);//调用close关闭已连接套接口,但是引用计数不为0,所以套接口未关闭,在子进程中仍然可以正常通信
//shutdown(conn_fd,SHUT_RDWR);//关闭已连接套接口,直接关闭套接口的读和写,子进程也不可以再用这个套接口
}
return 0;
}
执行结果:
父进程调用close:客户与服务器仍然可以正常通信(服务器对与客户端发过来的数据,在前面加上hello后,发送给客户,客户端打印)。
父进程调用shutdown:客户与服务器不可以在进行数据交互(服务器接收不到客户端发送的数据)。
我们已经了解了套接字关闭函数close、shutdown以及它们的区别,下面我们来进阶一下,说一说套接口有关它们两个的属性:SO_LINGER。
SO_LINGER套接口选项:
此选项通过套接口属性设置函数setsockopt进行设置。用于控制指定函数close对面向连接的协议如何操作(例如对TCP而不是对UDP)。缺醒操作是close立即返回,但如果有数据残留在套接口发送缓冲区中,系统将试着将这些数据发送给对方。
SO_LINGER套接口选项使我们可以改变close缺省设置,此选项要求在用户进程与内核间传递如下结构:
设置函数示例:
struct linger st_linger;
setsockopt(fd,SOL_SOCKET,SO_LINGER,(void *)&st_linger,&sizeof(st_linger));
头文件:<sys/socket.h>
struct linger
{
int l_onoff; /*0=off,nonzero = on */
int l_linger; /*linger time,Posix.1g specifies units as seconds */
};
对setsockopt(套接字属性设置函数)的调用将依两个结构成员的值导致下列三种情况的某一种:
- 如果l_onoff为0,则选项关闭,l_linger的值被忽略。执行close的缺醒操作。
- 如果l_onoff为非0值且l_linger为0,那么当套接口关闭时TCP夭折连接。TCP将丢弃保留在套接口发送缓冲区中的任何数据并发送一个RST给对方,而不是通常的四分组连接终止序列。
- l_onoff为非0值且l_linger也为非0值,那么当套接口关闭时内核将拖延一段时间(这个时间由l_linger控制,延迟l_linger秒)。也就是说,如果在套接口发送缓冲区中仍残留有数据,进程将处于睡眠状态,一直到(a)所有数据都已发送完且均被对方确认或(b)延迟时间到。(延伸:如果套接口被设置为非阻塞型,它将不等待close完成,即使延迟时间不为0也是如此。)
注:当使用SO_LINGER选项的这个特性时,应用进程检查close的返回值是非常重要的,因为,如果在数据发送完并被确认前延迟时间到的话,close将返回EWOULD-BLOCK错误且套接口发送缓冲区中的任何数据都丢失。
对于上面SO_LINGER属性可以控制的几种情况,套接口上的close确切来说是什么时候返回的。我们假设客户写数据到套接口上,然后调用close。
l_onoff为0,close缺省情况,下图2:
图2:close的缺省操作:立即返回
l_onoff非0,l_linger=0,下图3:
图3:丢弃发送缓冲区中数据,发送RST