Linux网络编程 - close OR shutdown?

我们知道,一个 TCP 连接需要经过三次握手进入数据传输阶段,最后来到连接关闭阶段。在最后的连接关闭阶段,我们需要重点关注的是“半连接”状态。因为 TCP 是双向的,这里说的方向,指的是数据流的写入 - 读出的方向。比如客户端到服务器端的方向,指的是客户端通过套接字接口,向服务器端发送 TCP 报文;而服务器端到客户端方向则是另一个传输方向。在绝大数情况下,TCP 连接都是先关闭一个方向,此时另外一个方向还是可以正常进行数据传输。

当客户端主动发起连接的中断,将自己到服务器端的数据流方向关闭,此时,客户端不再往服务器端写入数据,服务器端读完客户端数据后就不会再有新的报文到达。但这并不意味着,TCP 连接已经完全关闭,很有可能的是,服务器端正在对客户端的最后报文进行处理,比如去访问数据库,存入一些数据;或者是计算出某个客户端需要的值,当完成这些操作之后,服务器端把结果通过套接字写给客户端,我们说这个套接字的状态此时是“半关闭”的。最后,服务器端才有条不紊地关闭剩下的半个连接,结束这一段 TCP 连接的使命。

这里描述的,是服务器端“优雅”地关闭了连接。如果服务器端处理不好,就会导致最后的关闭过程是“粗暴”的,达不到我们上面描述的“优雅”关闭的目标,形成的后果,很可能是服务器端处理完的信息没办法正常传送给客户端,破坏了用户侧的使用场景。

close函数

int close(int sockfd)  //对已连接的套接字执行 close 操作就可以,若成功则为 0,若出错则为 -1。

套接字引用计数是什么意思呢?因为套接字可以被多个进程共享,你可以理解为我们给每个套接字都设置了一个积分,如果我们通过 fork 的方式产生子进程,套接字就会积分 +1, 如果我们调用一次 close 函数,套接字积分就会 -1。这就是套接字引用计数的含义。

close 函数具体是如何关闭两个方向的数据流呢?

在输入方向,系统内核会将该套接字设置为不可读,任何读操作都会返回异常。

在输出方向,系统内核尝试将发送缓冲区的数据发送给对端,并最后向对端发送一个 FIN 报文,接下来如果再对该套接字进行写操作会返回异常。

如果对端没有检测到套接字已关闭,还继续发送报文,就会收到一个 RST 报文,告诉对端:“Hi, 我已经关闭了,别再给我发数据了。”

我们会发现,close 函数并不能帮助我们关闭连接的一个方向,那么如何在需要的时候关闭一个方向呢?幸运的是,设计 TCP 协议的人帮我们想好了解决方案,这就是 shutdown 函数。

shutdown函数

int shutdown(int sockfd, int howto)   // 对已连接的套接字执行 shutdown 操作,若成功则为 0,若出错则为 -1。

howto 是这个函数的设置选项,它的设置有三个主要选项:

  • SHUT_RD(0):关闭连接的“读”这个方向,对该套接字进行读操作直接返回 EOF。从数据角度来看,套接字上接收缓冲区已有的数据将被丢弃,如果再有新的数据流到达,会对数据进行 ACK,然后悄悄地丢弃。也就是说,对端还是会接收到 ACK,在这种情况下根本不知道数据已经被丢弃了。
  • SHUT_WR(1):关闭连接的“写”这个方向,这就是常被称为”半关闭“的连接。此时,不管套接字引用计数的值是多少,都会直接关闭连接的写方向。套接字上发送缓冲区已有的数据将被立即发送出去,并发送一个 FIN 报文给对端。应用程序如果对该套接字进行写操作会报错。
  • SHUT_RDWR(2):相当于 SHUT_RD 和 SHUT_WR 操作各一次,关闭套接字的读和写两个方向。

讲到这里,不知道你是不是有和我当初一样的困惑,使用 SHUT_RDWR 来调用 shutdown 不是和 close 基本一样吗,都是关闭连接的读和写两个方向。

close 和 shutdown 的差别

第一个差别:close 会关闭连接,并释放所有连接对应的资源,而 shutdown 并不会释放掉套接字和所有的资源。

第二个差别:close 存在引用计数的概念,并不一定导致该套接字不可用;shutdown 则不管引用计数,直接使得该套接字不可用,如果有别的进程企图使用该套接字,将会受到影响。

第三个差别:close 的引用计数导致不一定会发出 FIN 结束报文,而 shutdown 则总是会发出 FIN 结束报文,这在我们打算关闭连接通知对端的时候,是非常重要的。

实验体会 close 和 shutdown 的差别

下面,我们通过构建一组客户端和服务器程序,来进行 close 和 shutdown 的实验。

客户端程序,从标准输入不断接收用户输入,把输入的字符串通过套接字发送给服务器端,同时,将服务器端的应答显示到标准输出上。在这里,我们会第一次接触到 select 多路复用,这里不展开讲,你只需要记住,使用 select 使得我们可以同时完成对连接套接字和标准输入两个 I/O 对象的处理。

/*client code*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include    <sys/select.h>
#include <unistd.h>

#define    MESSAGE_SIZE 10240000
#define    MAXLINE     4096

int main() {
    int sockfd;
    int connect_rt;
    struct sockaddr_in serv_addr;

    sockfd = socket(PF_INET, SOCK_STREAM, 0);
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(7878);
    inet_pton(AF_INET, "192.168.133.131", &serv_addr.sin_addr);

    connect_rt = connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    if (connect_rt < 0)
    {
        fprintf(stderr, "Connect failed !\n");
        exit(0);
    }

    char send_line[MAXLINE], recv_line[MAXLINE + 1];
    int n;
    fd_set readmask;
    fd_set allreads;
    FD_ZERO(&allreads);
    FD_SET(0, &allreads);       // add  stdin->0 to allreads
    FD_SET(sockfd, &allreads);   // add sockfd to  addreads
    for (;;) {
        readmask = allreads;
        int rc = select(sock_fd + 1, &readmask, NULL, NULL, NULL);
        if (rc <= 0)
            error(1, errno, "select failed");
        if (FD_ISSET(socket_fd, &readmask)) {
            n = read(socket_fd, recv_line, MAXLINE);
            if (n < 0) {               //如果有异常则报错退出
                fprintf(stderr, "read error\n");
                exit(0);
            } else if (n == 0) {       //如果读到服务器端发送的 EOF 则正常退出
                fprintf(stderr, "server terminated\n");
                exit(0);
            }
            recv_line[n] = 0;
            fputs(recv_line, stdout);
            fputs("\n", stdout);
        }
        if (FD_ISSET(0, &readmask)) {
            if (fgets(send_line, MAXLINE, stdin) != NULL) {
        /*
          当标准输入上有数据可读,读入后进行判断。如果输入的是“shutdown”,则关闭标准输入的 I/O 
          事件感知,并调用 shutdown 函数关闭写方向;如果输入的是”close“,则调用 close 函数关闭 
          连接
        */
                if (strncmp(send_line, "shutdown", 8) == 0) {
                    FD_CLR(0, &allreads);
                    if (shutdown(socket_fd, 1)) {
                        printf("shutdown failed");
                    }
                } else if (strncmp(send_line, "close", 5) == 0) {
                    FD_CLR(0, &allreads);
                    if (close(socket_fd)) {
                        printf("shutdown failed");
                    }
                    sleep(6);
                    exit(0);
                } else {
                    int i = strlen(send_line);
                    if (send_line[i - 1] == '\n') {
                        send_line[i - 1] = 0;
                    }
                    printf("now sending %s\n", send_line);
                    size_t rt = write(socket_fd, send_line, strlen(send_line));
                    printf("send bytes: %zu \n", rt);
                }
            }
        }
    }
}
/*server  code*/
#include    <sys/types.h>    /* basic system data types */
#include    <sys/socket.h>    /* basic socket definitions */
#include    <netinet/in.h>    /* sockaddr_in{} and other Internet defns */
#include    <arpa/inet.h>    /* inet(3) functions */
#include    <errno.h>
#include    <signal.h>
#include    <stdio.h>
#include    <stdlib.h>
#include    <string.h>
#include    <unistd.h>
#include    <string.h>        /* for convenience */

static int count;
static void sig_int(int signo)
{       
        printf("\nreceived %d datagrams\n", count);
        exit(0);
}

int main()
{
        int listenfd, connfd;
        socklen_t  cli_addr_len;
        struct sockaddr_in serv_addr, cli_addr;

        listenfd = socket(PF_INET, SOCK_STREAM, 0);
        bzero(&serv_addr, sizeof(serv_addr));
        bzero(&cli_addr, sizeof(cli_addr));
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_port = htons(7878);
        serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
        bind(listenfd, (struct sockaddr*) &serv_addr, sizeof(serv_addr));

        listen(listenfd, SOMAXCONN);
        signal(SIGINT, sig_int);
        signal(SIGPIPE, SIG_DFL);
        connfd = accept(listenfd, (struct sockaddr* )&cli_addr, &cli_addr_len);
        char message[4096];
        count = 0;
        for(;;){
                int n = read(connfd, message, 4096);
                if(n < 0){
                        printf("error read\n");
                        exit(0);
                }else if(n == 0){
                        printf("client closed\n");
                        exit(0);
                }

                message[n] = 0;
                printf("received %d bytes: %s\n", n, message);
                count ++;
                char send_line[4096];
                sprintf(send_line, "Hi, %s", message);

                sleep(5);
                int write_nc = send(connfd, send_line, strlen(send_line), 0);
                printf("send bytes: %d \n", write_nc);
        }
        return 0;
}

我们启动服务器,再启动客户端,依次在标准输入上输入 data1、data2 和 close,观察一段时间后我们看到:

close:

# ./tcp_client 
data1
now sending data1
send bytes: 5 
data2
now sending data2
send bytes: 5 
close
# ./tcp_server 
received 5 bytes: data1
send bytes: 9 
received 5 bytes: data2
send bytes: 9 
client closed

shutdown:

# ./tcp_client 
data1
now sending data1
send bytes: 5 
data2
now sending data2
send bytes: 5 
shutdown
Hi, data1
Hi, data2
server terminated
# ./tcp_server 
received 5 bytes: data1
send bytes: 9 
received 5 bytes: data2
send bytes: 9 
client closed

   下面分别展示了close 和 shutdown 场景的 时序图:

                              

这张图详细解释了客户端和服务器端交互的时序图。

左边显示客户端调用 close 函数关闭了整个连接,当服务器端发送的“Hi, data1”分组到底时,客户端给回送一个 RST 分组;服务器端再次尝试发送“Hi, data2”第二个应答分组时,系统内核通知 SIGPIPE 信号。这是因为,在 RST 的套接字进行写操作,会直接触发 SIGPIPE 信号。

右边显示服务器端输出了 data1、data2;客户端也输出了“Hi,data1”和“Hi,data2”,客户端和服务器端各自完成了自己的工作后,正常退出。因为客户端调用 shutdown 函数只是关闭连接的一个方向,服务器端到客户端的这个方向还可以继续进行数据的发送和接收,所以“Hi,data1”和“Hi,data2”都可以正常传送;当服务器端读到 EOF 时,立即向客户端发送了 FIN 报文,客户端在 read 函数中感知了 EOF,也进行了正常退出。

注:在今天的服务器端程序中,直接调用exit(0)完成了 FIN 报文的发送,这是为什么呢?为什么不调用 close 函数或 shutdown 函数呢?

因为在调用exit之后进程会退出,而进程相关的所有的资源,文件,内存,信号等内核分配的资源都会被释放,在linux中,一切皆文件,本身socket就是一种文件类型,内核会为每一个打开的文件创建file结构并维护指向改结构的引用计数,每一个进程结构中都会维护本进程打开的文件数组,数组下标就是fd,内容就指向上面的file结构,close本身就可以用来操作所有的文件,做的事就是,删除本进程打开的文件数组中指定的fd项,并把指向的file结构中的引用计数减一,等引用计数为0的时候,就会调用内部包含的文件操作close,针对于socket,它内部的实现应该就是调用shutdown,只是参数是关闭读写端,从而比较粗暴的关闭连接。

温故而知新 !

发布了56 篇原创文章 · 获赞 39 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_24436765/article/details/104237176