{T}

TCP连接关闭:优雅关闭与半关闭

TCP 连接是全双工(Full-Duplex)通信通道,数据可在两个方向上独立传输。连接关闭过程同样需要考虑双向数据流的独立处理。本文将深入分析 TCP 连接关闭的两种方式——close()shutdown(),并探讨"优雅关闭"(Graceful Close)与"半关闭"(Half-Close)的实现机制。

TCP 连接的双向特性

TCP 连接支持双向独立的数据传输:

  • 客户端到服务器端方向:客户端通过套接字写入数据,服务器端读取数据。
  • 服务器端到客户端方向:服务器端通过套接字写入数据,客户端读取数据。

在大多数应用场景中,TCP 连接的关闭并非同时关闭两个方向,而是先关闭一个方向,待另一方向的数据传输完成后,再关闭剩余方向。这种模式被称为"半关闭"(Half-Close)。

半关闭的应用场景

典型的半关闭场景:客户端主动发起连接关闭,关闭客户端到服务器端的数据流方向。此时:

  1. 客户端不再向服务器端写入数据。
  2. 服务器端读取完客户端数据后,检测到 EOF(End of File)。
  3. 服务器端可能仍需处理客户端的最后请求(如数据库操作),并将结果返回客户端。
  4. 服务器端完成处理后,关闭服务器端到客户端的数据流方向。
  5. 连接完全关闭。

若服务器端处理不当,可能导致处理结果无法正常传送给客户端,破坏应用逻辑。

close() 函数

close() 函数是最常用的连接关闭方式:

c
int close(int sockfd);

返回值:成功返回 0,失败返回 -1。

行为机制

close() 函数对套接字执行以下操作:

  1. 引用计数减一:套接字可被多个进程共享(如通过 fork() 创建子进程)。每个套接字维护一个引用计数,fork() 时计数加一,close() 时计数减一。
  2. 引用计数归零时的处理
    • 输入方向:套接字被标记为不可读,后续读操作返回错误。
    • 输出方向:内核尝试发送发送缓冲区中的残留数据,随后发送 FIN 报文,后续写操作返回错误。

若对端在本地关闭后仍继续发送数据,本地将回复 RST 报文,通知对端连接已关闭。

close() 的局限性

close() 函数无法实现单向关闭——它总是关闭两个方向的数据流。当需要仅关闭一个方向时,应使用 shutdown() 函数。

shutdown() 函数

shutdown() 函数提供更精细的关闭控制:

c
int shutdown(int sockfd, int howto);

返回值:成功返回 0,失败返回 -1。

howto 参数选项

参数值宏定义行为描述
0SHUT_RD关闭读方向。后续读操作直接返回 EOF。接收缓冲区已有数据被丢弃,新到达的数据被 ACK 后静默丢弃。
1SHUT_WR关闭写方向(半关闭)。发送缓冲区数据被发送,随后发送 FIN 报文。后续写操作返回错误。
2SHUT_RDWR同时关闭读和写两个方向。

close() 与 shutdown() 的关键差异

特性close()shutdown()
资源释放引用计数归零时释放套接字资源不释放套接字资源
引用计数受引用计数影响,可能不立即关闭不受引用计数影响,立即生效
FIN 报文仅在引用计数归零时发送总是发送 FIN 报文
单向关闭不支持支持(SHUT_RDSHUT_WR

关键结论:当需要通知对端本地已完成数据发送时,应使用 shutdown(sockfd, SHUT_WR),而非 close()。前者保证 FIN 报文被发送,后者可能因引用计数未归零而不发送 FIN。

优雅关闭与强制关闭的对比

图表渲染中…

半关闭状态机

TCP 连接在半关闭过程中的状态转换如下:

图表渲染中…

实践示例

以下示例演示 close()shutdown() 的行为差异。

客户端程序

客户端从标准输入接收用户命令,通过套接字发送给服务器端,并显示服务器端的响应。用户输入 "close" 时调用 close() 关闭连接;输入 "shutdown" 时调用 shutdown() 关闭写方向。

c
#include "lib/common.h"
#define MAXLINE 4096

int main(int argc, char **argv) {
    if (argc != 2) {
        error(1, 0, "usage: graceclient <IPaddress>");
    }

    int socket_fd;
    socket_fd = socket(AF_INET, SOCK_STREAM, 0);

    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);

    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;

    fd_set readmask;
    fd_set allreads;

    FD_ZERO(&allreads);
    FD_SET(0, &allreads);
    FD_SET(socket_fd, &allreads);

    for (;;) {
        readmask = allreads;
        int rc = select(socket_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) {
                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);
        }

        if (FD_ISSET(0, &readmask)) {
            if (fgets(send_line, MAXLINE, stdin) != NULL) {
                if (strncmp(send_line, "shutdown", 8) == 0) {
                    FD_CLR(0, &allreads);
                    if (shutdown(socket_fd, SHUT_WR)) {
                        error(1, errno, "shutdown failed");
                    }
                } else if (strncmp(send_line, "close", 5) == 0) {
                    FD_CLR(0, &allreads);
                    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);
                }
            }
        }
    }
}

服务器端程序

服务器端接收客户端数据,格式化后返回响应。为模拟处理延迟,服务器端在发送响应前休眠 5 秒。

c
#include "lib/common.h"

static int count;

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

int main(int argc, char **argv) {
    int listenfd;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    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);

    int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
    if (rt1 < 0) {
        error(1, errno, "bind failed");
    }

    int rt2 = listen(listenfd, LISTENQ);
    if (rt2 < 0) {
        error(1, errno, "listen failed");
    }

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

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

    if ((connfd = accept(listenfd, (struct sockaddr *) &client_addr, &client_len)) < 0) {
        error(1, errno, "accept 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");
        }
    }
}

实验结果分析

场景一:使用 close() 关闭连接

code
# 客户端输出
$ ./graceclient 127.0.0.1
data1
now sending data1
send bytes: 5
data2
now sending data2
send bytes: 5
close

# 服务器端输出
$ ./graceserver
received 5 bytes: data1
send bytes: 9
received 5 bytes: data2
send bytes: 9
client closed

客户端调用 close() 后,两个方向均被关闭。服务器端发送的响应数据到达时,客户端回复 RST 报文。服务器端继续发送数据时,触发 SIGPIPE 信号(已在程序中忽略),导致数据丢失。

场景二:使用 shutdown() 关闭写方向

code
# 客户端输出
$ ./graceclient 127.0.0.1
data1
now sending data1
send bytes: 5
data2
now sending data2
send bytes: 5
shutdown
Hi, data1
Hi, data2
server terminated

# 服务器端输出
$ ./graceserver
received 5 bytes: data1
send bytes: 9
received 5 bytes: data2
send bytes: 9
client closed

客户端调用 shutdown(sockfd, SHUT_WR) 后,仅关闭写方向,读方向仍可用。服务器端的响应数据正常到达客户端。服务器端检测到 EOF 后关闭连接,客户端正常退出。

SIGPIPE 信号处理

当进程向已收到 RST 的套接字写入数据时,内核将向进程发送 SIGPIPE 信号,默认行为是终止进程。在网络编程中,应妥善处理该信号:

c
// 方式一:忽略信号
signal(SIGPIPE, SIG_IGN);

// 方式二:自定义处理函数
static void sig_pipe(int signo) {
    printf("received SIGPIPE\n");
    // 可选:执行清理操作后退出
}
signal(SIGPIPE, sig_pipe);

内核版本注释:在 Linux 2.6.32+ 中,可通过 MSG_NOSIGNAL 标志在 send() 调用时禁用 SIGPIPE 信号,而不影响全局信号处理配置:

c
send(sockfd, buffer, len, MSG_NOSIGNAL);

总结

  • close() 函数将套接字引用计数减一,仅在引用计数归零时关闭连接的两个方向。
  • shutdown() 函数不受引用计数影响,可精确控制关闭方向,且总是发送 FIN 报文。
  • 在需要通知对端本地已完成数据发送的场景中,应使用 shutdown(sockfd, SHUT_WR) 实现优雅关闭。
  • 应正确处理 SIGPIPE 信号,避免进程异常终止。

思考题

  1. 在服务器端程序中,直接调用 exit(0) 即可完成 FIN 报文的发送,原因是什么?为何无需显式调用 close()shutdown()
  2. SIG_IGN(忽略信号)与自定义信号处理函数有何区别?在何种场景下应选择前者或后者?

版本信息

项目内容
更新日期2026-06-09
目标内核Linux 7.0
关键特性MSG_NOSIGNAL 标志(2.6.32+)、SO_LINGER 选项