网络编程学习: 08 如何优雅地关闭连接

关键词总结:close shutdown

代码路径见Github 专栏代码

TCP 是双向的,这里说的方向,指的是数据流的写入 - 读出的方向。在绝大数情况下,TCP 连接都是先关闭一个方向,此时另外一个方向还是可以正常进行数据传输。(客户端到服务器端的方向,指的是客户端通过套接字接口,向服务器端发送 TCP 报文;而服务器端到客户端方向则是另一个传输方向。)

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

如果服务器端处理不好,就会导致最后的关闭过程是“粗暴”的,很可能是服务器端处理完的信息没办法正常传送给客户端,破坏了用户侧的使用场景。

关闭连接的方式

close 函数

int close(int sockfd)

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

close函数会对套接字引用计数减一,如果发现套接字引用计数为0,,就会对套接字进行彻底释放,并且会关闭 TCP 两个方向的数据流

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

在输入方向,系统内核会将该套接字设置为不可读,任何读操作都会返回异常。
在输出方向,系统内核尝试将发送缓冲区的数据发送给对端,并最后向对端发送一个 FIN 报文,接下来如果再对该套接字进行写操作会返回异常。如果对端没有检测到套接字已关闭,还继续发送报文,就会收到一个 RST 报文,告诉对端:“Hi, 我已经关闭了,别再给我发数据了。”

close 函数并不能帮助我们关闭连接的一个方向,shutdown 函数可以

shutdown 函数

int shutdown(int sockfd, int howto)

成功则为 0,若出错则为 -1

howto 三个主要选项:

  • SHUT_RD(0):关闭连接的“读”这个方向,对该套接字进行读操作直接返回 EOF。套接字上接收缓冲区已有的数据将被丢弃,新的数据到达会对其进行ACK,但会悄悄丢掉数据。
  • SHUT_WR(1):关闭连接的“写”这个方向。不管套接字引用计数的值是多少,都会直接关闭连接的写方向。套接字上发送缓冲区已有的数据将被立即发送出去,并发送一个 FIN 报文给对端。应用程序如果对该套接字进行写操作会报错。
  • SHUT_RDWR(2):相当于 SHUT_RD 和 SHUT_WR 操作各一次,关闭套接字的读和写两个方向。

SHUT_RDWR 的 shutdown 和 close 区别:
都是关闭连接的读和写两个方向
第一个差别:close 会关闭连接,并释放所有连接对应的资源,而 shutdown 并不会释放掉套接字和所有的资源。
第二个差别:close 存在引用计数的概念,并不一定导致该套接字不可用;shutdown 则不管引用计数,直接使得该套接字不可用
第三个差别:close 的引用计数导致不一定会发出 FIN 结束报文,而 shutdown 则总是会发出 FIN 结束报文

close 和 shutdown 的差别

Linux: fd_set用法

服务端代码

#include <stdio.h>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <zconf.h>
#include <errno.h>
#include <signal.h>

#define MAXLINE     4096
#define SERV_PORT 12345
#define    LISTENQ        1024

static int count;

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


int main(int argc, char **argv) {
    // 创建了一个 TCP 套接字;
    int listenfd;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    // 设置本地服务器 IPv4 地址,绑定到了 ANY 地址和指定的端口;
    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(SERV_PORT);
    // 执行 bind
    int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
    if (rt1 < 0) {
        error(1, errno, "bind failed ");
    }
    // listen
    int rt2 = listen(listenfd, LISTENQ);
    if (rt2 < 0) {
        error(1, errno, "listen failed ");
    }

    signal(SIGINT, sig_int);
    signal(SIGPIPE, SIG_DFL);

    int connfd;
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);

    // accept
    if ((connfd = accept(listenfd, (struct sockaddr *) &client_addr, &client_len)) < 0) {
        error(1, errno, "bind failed ");
    }

    char message[MAXLINE];
    count = 0;

    for (;;) {
        int n = read(connfd, message, MAXLINE);
        if (n < 0) {
            error(1, errno, "error read");
        } else if (n == 0) {
            error(1, 0, "client closed \n");
        }
        message[n] = 0;
        printf("received %d bytes: %s\n", n, message);
        count++;

        char send_line[MAXLINE];
        sprintf(send_line, "Hi, %s", message);

        sleep(5);

        int write_nc = send(connfd, send_line, strlen(send_line), 0);
        printf("send bytes: %zu \n", write_nc);
        if (write_nc < 0) {
            error(1, errno, "error write");
        }
    }

}

客户端代码

#include <stdio.h>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <zconf.h>
#include <errno.h>
#include <signal.h>

#define MAXLINE     4096
#define SERV_PORT 12345


int main(int argc, char **argv) {

    if (argc != 2) {
        perror("usage: client ");
    }

    // 创建了一个 TCP 套接字;
    int socket_fd; 
    socket_fd = socket(AF_INET, SOCK_STREAM, 0);

    // 连接的目标服务器 IPv4 地址,绑定到了指定的 IP 和端口;
    struct sockaddr_in server_addr; 
    bzero(&server_addr, sizeof(server_addr)); 
    server_addr.sin_family = AF_INET; 
    server_addr.sin_port = htons(SERV_PORT); 
    inet_pton(AF_INET, argv[1], &server_addr.sin_addr); 

    // 使用创建的套接字,向目标 IPv4 地址发起连接请求;
    socklen_t server_len = sizeof(server_addr); 
    int connect_rt = connect(socket_fd, (struct sockaddr *) &server_addr, server_len); 
    if (connect_rt < 0) { 
        error(1, errno, "connect failed "); 
    }

    char send_line[MAXLINE], recv_line[MAXLINE + 1]; 
    int n;

    // 使用 select 做准备,初始化描述字集合
    fd_set readmask;
    fd_set allreads;
    // 参见 https://blog.csdn.net/bailyzheng/article/details/7477446
    FD_ZERO(&allreads); /*将allreads清零使集合中不含任何fd*/
    FD_SET(0, &allreads);    /*将allreads的第0位置1,如allreads原来是00000000,则现在变为100000000,这样fd==1的文件描述字就被加进allreads中了*/
    FD_SET(socket_fd, &allreads);  /*将socket_fd加入allreads集合*/

    for (;;) {

        readmask = allreads;
        int rc = select(socket_fd + 1, &readmask, NULL, NULL, NULL);

        if (rc <= 0)
            error(1, errno, "select failed");
        //   /*测试socket_fd是否在readmask集合中*/    
        if (FD_ISSET(socket_fd, &readmask)) {
            // 连接套接字上有数据
            n = read(socket_fd, recv_line, MAXLINE);
            if (n < 0) {
                error(1, errno, "read error");
            } else if (n == 0) {
                error(1, 0, "server terminated \n");
            }
            recv_line[n] = 0;
            fputs(recv_line, stdout);
            fputs("\n", stdout);
        }
         /*测试readmask的第0位是否为1*/
        if (FD_ISSET(0, &readmask)) {
            // 标准输入
            if (fgets(send_line, MAXLINE, stdin) != NULL) {
                if (strncmp(send_line, "shutdown", 8) == 0) {
                    // 将allreads的第0位置0
                    FD_CLR(0, &allreads);
                    // 调用 shutdown 函数关闭写方向
                    if (shutdown(socket_fd, 1)) {
                        error(1, errno, "shutdown failed");
                    }
                } else if (strncmp(send_line, "close", 5) == 0) {
                    FD_CLR(0, &allreads);
                    // 调用 close 函数关闭连接
                    if (close(socket_fd)) {
                        error(1, errno, "close 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));
                    if (rt < 0) {
                        error(1, errno, "write failed ");
                    }
                    printf("send bytes: %zu \n", rt);
                }

            }
        }
    }


    return 0;
}

依次启动服务器,再启动客户端,在标准输入上输入data1、Hello leacock
在这里插入图片描述

客户端 close 掉整个连接之后,服务器端接收到 SIGPIPE 信号,直接退出。客户端并没有收到服务器端的应答数据。
在这里插入图片描述
客户端和服务器端交互的时序图
在这里插入图片描述

再次启动服务器,再启动客户端,依次在标准输入上输入 data1、data2 和 shutdown 函数
在这里插入图片描述
服务器端输出了 data1、data2;客户端也输出了“Hi,data1”和“Hi,data2”,客户端和服务器端各自完成了自己的工作后,正常退出。
客户端和服务器端交互的时序图
在这里插入图片描述

参考资料:

网络编程实战(极客时间)链接:
http://gk.link/a/10g9X


GitHub链接:
https://github.com/lichangke
CSDN首页:
https://me.csdn.net/leacock1991
欢迎大家来一起交流学习

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

墨1024

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值