【网络基础-套接字关闭】close与shutdown以及SO_LINGER属性(精讲)

人们的视线总是会指向那些他们有兴趣靠近、试探、寻找或者拥有的东西。---摘自:《人生十二堂课》

当一个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函数有两个局限:

  1. close把描述符的引用计数减一,仅在该计数变为0时,才关闭套接字。
  2. 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(套接字属性设置函数)的调用将依两个结构成员的值导致下列三种情况的某一种

  1. 如果l_onoff为0,则选项关闭,l_linger的值被忽略。执行close的缺醒操作。
  2. 如果l_onoff为非0值且l_linger为0,那么当套接口关闭时TCP夭折连接。TCP将丢弃保留在套接口发送缓冲区中的任何数据并发送一个RST给对方,而不是通常的四分组连接终止序列。
  3. 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

l_onoff非0,l_linger非0,可以通过close函数的返回值,来确认对方是否已经确认接收了数据(注:这种确认仅可以告诉我们,我们发送的数据已由对方TCP确认,它并不能告诉我们对方应用进程已经读取了数据)。下图4:
                               图4:设置SO_LINGER套接口选项且l_linger为正时的close
shutdown以及close(SO_LINGER)关闭套接字总结(图片来自unix网络编程:卷一)
 
 
 
发布了4 篇原创文章 · 获赞 7 · 访问量 1022

猜你喜欢

转载自blog.csdn.net/JianChiBieFei/article/details/105452808