shutdown系统调用及示例

我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 shutdown 函数,它用于部分完全地关闭一个面向连接的套接字(如 TCP 套接字)的数据传输


1. 函数介绍

shutdown 是一个 Linux 系统调用,专门用于更精细地控制已连接套接字的关闭过程。与 close 函数不同(close 会完全关闭套接字,释放其文件描述符),shutdown 允许你:

  1. 关闭数据流的一个方向:例如,告诉对方“我不会再发送数据了”(但仍可以接收数据)。
  2. 关闭数据流的两个方向:完全禁止在此套接字上进行任何发送和接收操作(但仍保持文件描述符打开,直到调用 close)。

你可以把 shutdown 想象成电话通话中的话筒控制

  • 全双工通话:你可以说话(发送),也可以听对方说话(接收)。
  • shutdown(SHUT_WR):相当于你按下了“禁麦”按钮。你不能再说话(发送数据),但你仍然可以听到对方说话(接收数据)。
  • shutdown(SHUT_RD):相当于你戴上了耳塞。你听不到对方说话(接收数据),但(理论上)你还可以说话(发送数据)——不过对方可能听不到或会收到错误。
  • shutdown(SHUT_RDWR):相当于你挂断了电话的通话功能。你既不能说也不能听,但电话线(套接字文件描述符)本身可能还没被物理拔掉(close)。

这对于实现优雅的连接关闭(如 TCP 的四次挥手)和单向通信非常有用。


2. 函数原型

#include <sys/socket.h> // 必需

int shutdown(int sockfd, int how);

3. 功能

  • 部分关闭: 根据 how 参数,关闭套接字 sockfd 的发送能力、接收能力或两者。
  • 发送信号: 对于 TCP 套接字,shutdown 会触发相应的 TCP 连接终止序列(如发送 FIN 包)来通知对端。
  • 状态改变: 改变套接字的内部状态,使其无法再执行被禁止的操作。

4. 参数

  • int sockfd: 这是一个已连接(对于 TCP)或已绑定/连接(对于 UDP,如果使用了 connect)的有效套接字文件描述符
  • int how: 这个参数指定了要执行的关闭操作类型。它必须是以下值之一:
    • SHUT_RD关闭接收方向。套接字不再接收数据。任何传入的数据都可能被丢弃,后续的 read 或 recv 调用将返回 0(表示 EOF)。
    • SHUT_WR关闭发送方向。套接字不再发送数据。对于 TCP,这会发送一个 FIN 包给对方,表示本端不再发送数据。后续的 write 或 send 调用将失败(通常返回错误 EPIPE 或导致 SIGPIPE 信号)。
    • SHUT_RDWR关闭接收和发送方向。这相当于同时执行 SHUT_RD 和 SHUT_WR。对于 TCP,这会关闭两个方向的数据流。

5. 返回值

  • 成功时: 返回 0。
  • 失败时: 返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 EBADF sockfd 不是有效的文件描述符,EINVAL how 参数无效,ENOTCONN 套接字未连接等)。

6. 相似函数,或关联函数

  • close: 完全关闭套接字,释放其文件描述符。如果套接字的引用计数变为 0,其效果类似于 shutdown(SHUT_RDWR) 后再释放资源。通常在 shutdown 之后调用 close
  • read / write / send / recvshutdown 会影响这些函数的行为。例如,shutdown(SHUT_RD) 后 read 会立即返回 0。
  • TCP 协议shutdown 的行为与 TCP 连接的状态转换密切相关,特别是 FIN 包的发送和接收。

7. 示例代码

示例 1:TCP 客户端使用 shutdown 实现半关闭

这个例子演示了 TCP 客户端如何在发送完所有数据后,使用 shutdown(SHUT_WR) 告诉服务器它不会再发送更多数据,然后继续接收服务器的回复。

// shutdown_client.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define PORT 8084
#define SERVER_IP "127.0.0.1"
#define BUFFER_SIZE 1024

int main() {
    int sock;
    struct sockaddr_in serv_addr;
    char *message = "Here is the complete message from client.";
    char buffer[BUFFER_SIZE];
    ssize_t bytes_sent, bytes_received;

    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
        fprintf(stderr, "Invalid address\n");
        close(sock);
        exit(EXIT_FAILURE);
    }

    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("connection failed");
        close(sock);
        exit(EXIT_FAILURE);
    }

    printf("Connected to server.\n");

    // 1. 发送数据到服务器
    bytes_sent = write(sock, message, strlen(message));
    if (bytes_sent < 0) {
        perror("write failed");
        close(sock);
        exit(EXIT_FAILURE);
    } else {
        printf("Sent %zd bytes to server.\n", bytes_sent);
    }

    // 2. 关闭发送方向 (SHUT_WR)
    // 这告诉服务器:'我的数据发完了,不会再发了'
    // 但客户端仍然可以接收服务器发送的数据
    printf("Shutting down write direction (SHUT_WR)...\n");
    if (shutdown(sock, SHUT_WR) < 0) {
        perror("shutdown SHUT_WR failed");
        // 即使 shutdown 失败,也应尝试关闭套接字
    } else {
        printf("Write direction shut down successfully.\n");
    }

    // 3. 继续接收服务器的回复
    printf("Now reading server's response...\n");
    while ((bytes_received = read(sock, buffer, BUFFER_SIZE - 1)) > 0) {
        buffer[bytes_received] = '\0';
        printf("Received from server: %s", buffer);
    }

    if (bytes_received < 0) {
        perror("read failed");
    } else {
        printf("Server closed connection (EOF received).\n");
    }

    // 4. 最后关闭套接字文件描述符
    close(sock);
    printf("Client socket closed.\n");

    return 0;
}

代码解释:

  1. 客户端创建 TCP 套接字并连接到服务器。
  2. 使用 write 向服务器发送一条消息。
  3. 关键步骤: 调用 shutdown(sock, SHUT_WR)
    • 这会向服务器发送一个 TCP FIN 包,表明客户端不会再发送数据。
    • 服务器的 read 调用在收到这个 FIN 后会返回 0(EOF)。
    • 但是,客户端的套接字仍然打开,并且仍然可以接收数据。
  4. 客户端进入一个 while 循环,使用 read 继续接收服务器可能发送的任何回复数据,直到服务器也关闭连接(read 返回 0)。
  5. 最后,调用 close(sock) 完全关闭套接字文件描述符。

示例 2:TCP 服务器使用 shutdown 响应客户端

这个例子演示了 TCP 服务器如何在收到客户端的 FIN(即 read 返回 0)后,使用 shutdown 和 close 来优雅地关闭连接。

// shutdown_server.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define PORT 8084
#define BACKLOG 10
#define BUFFER_SIZE 1024

int main() {
    int server_fd, client_fd;
    struct sockaddr_in address, client_address;
    socklen_t client_addr_len = sizeof(client_address);
    char buffer[BUFFER_SIZE];
    char *reply = "Server received your message. Here is the server's final reply.";
    ssize_t bytes_received;
    int opt = 1;

    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    if (listen(server_fd, BACKLOG) < 0) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("Server listening on port %d\n", PORT);

    client_fd = accept(server_fd, (struct sockaddr *)&client_address, &client_addr_len);
    if (client_fd < 0) {
        perror("accept failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("Client connected.\n");

    // 1. 从客户端接收数据
    printf("Receiving data from client...\n");
    while ((bytes_received = read(client_fd, buffer, BUFFER_SIZE - 1)) > 0) {
        buffer[bytes_received] = '\0';
        printf("Received from client: %s", buffer);
    }

    if (bytes_received < 0) {
        perror("read failed");
        close(client_fd);
        close(server_fd);
        exit(EXIT_FAILURE);
    } else {
        // bytes_received == 0, 表示客户端已关闭发送方向 (SHUT_WR)
        printf("Client has shut down its write direction (EOF received).\n");
    }

    // 2. 向客户端发送最终回复
    printf("Sending final reply to client...\n");
    if (write(client_fd, reply, strlen(reply)) != (ssize_t)strlen(reply)) {
        perror("write reply failed");
    } else {
        printf("Final reply sent.\n");
    }

    // 3. 关闭服务器的写方向
    // 这会向客户端发送 FIN,表明服务器也不会再发送数据
    printf("Shutting down server's write direction (SHUT_WR)...\n");
    if (shutdown(client_fd, SHUT_WR) < 0) {
        perror("shutdown SHUT_WR failed");
    } else {
        printf("Server's write direction shut down.\n");
    }

    // 4. (可选) 继续等待一段时间,看客户端是否也关闭
    // 在这个简单例子中,我们直接关闭
    printf("Closing client socket.\n");
    close(client_fd);

    close(server_fd);
    printf("Server sockets closed.\n");

    return 0;
}

代码解释:

  1. 服务器创建、绑定、监听套接字,并 accept 客户端连接。
  2. 服务器进入一个 while 循环,使用 read 从客户端接收数据。
  3. 当 read 返回 0 时,表示客户端已调用 shutdown(SHUT_WR) 或 close,其发送方向已关闭。
  4. 服务器向客户端发送一个最终的回复消息。
  5. 关键步骤: 服务器调用 shutdown(client_fd, SHUT_WR)
    • 这会向客户端发送一个 TCP FIN 包。
    • 客户端的 read 调用在收到这个 FIN 后会返回 0。
  6. 最后,服务器调用 close(client_fd) 完全关闭与该客户端的连接。

示例 3:对比 shutdown 和 close

这个例子通过伪代码和解释来说明 shutdown 和 close 的区别。

// 假设 sock 是一个已连接的 TCP 套接字

// --- 情况一:只使用 close ---
write(sock, "Hello", 5);
close(sock); // 1. 发送 FIN (如果这是最后一个引用)
             // 2. 释放文件描述符
             // 3. 内核可能立即终止连接或尝试优雅关闭

// --- 情况二:使用 shutdown 后再 close (优雅关闭) ---
write(sock, "Hello", 5);
shutdown(sock, SHUT_WR); // 1. 发送 FIN,告诉对方'我发完了'
                         // 2. 套接字仍然打开,仍可 read

char buffer[1024];
ssize_t n;
while ((n = read(sock, buffer, sizeof(buffer))) > 0) {
    // 处理客户端最后发送的数据
}
// read 返回 0,表示客户端也关闭了

close(sock); // 3. 此时 close 只是释放本地文件描述符
             //    TCP 连接已经通过 FIN/ACK 交互优雅地关闭了

解释:

  • 仅使用 close: 这种方式简单直接。当 close 被调用且该套接字的引用计数变为 0 时,内核会尝试关闭连接。这通常涉及发送 FIN,但整个过程是隐式的。如果在发送缓冲区还有数据时立即 close,行为可能取决于系统实现(数据可能被发送,也可能被丢弃)。
  • 使用 shutdown + close: 这是一种更优雅明确的关闭方式。
    1. 发送完数据后,调用 shutdown(SHUT_WR) 明确表示“数据发送完毕”。这会可靠地发送 FIN 给对方。
    2. 程序继续使用 read 来接收对方可能在收到 FIN 后发送的剩余数据。
    3. 当 read 也返回 0(收到对方的 FIN 并回复 ACK)时,双方都确认了连接的单向关闭。
    4. 最后调用 close 仅仅是清理本地资源(文件描述符)。

重要提示与注意事项:

  1. 仅适用于连接型套接字shutdown 主要用于面向连接的套接字,如 TCP (SOCK_STREAM)。对于无连接的套接字,如 UDP (SOCK_DGRAM),它的行为是未定义的或没有意义的。
  2. 不释放文件描述符shutdown 不会关闭套接字的文件描述符。你仍然需要调用 close() 来最终释放资源。
  3. TCP 语义shutdown 的行为与底层 TCP 协议紧密相关。SHUT_WR 导致发送 FINSHUT_RD 影响接收缓冲区的行为。
  4. 优雅关闭: 在需要确保所有数据都被发送和接收的场景中(如 HTTP/1.1 Connection: close),使用 shutdown 是实现优雅关闭的标准方法。
  5. 错误处理: 始终检查 shutdown 的返回值。在套接字已经关闭或无效时调用它会失败。
  6. SHUT_RD 的实用性SHUT_RD 的使用场景相对较少。关闭接收通常意味着你不再关心对方的数据,直接 close 或在 read 返回 0 后 close 通常就足够了。

总结:

shutdown 是一个用于精细控制 TCP 连接关闭过程的系统调用。它允许程序在完全终止连接之前,单方面地关闭数据流的一个或两个方向。这对于实现协议规定的优雅关闭序列(如 HTTP)和处理单向数据流非常重要。理解它与 close 的区别,并在需要时正确使用它,是编写健壮网络应用程序的关键技能之一。

此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注