我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 shutdown 函数,它用于部分或完全地关闭一个面向连接的套接字(如 TCP 套接字)的数据传输。
1. 函数介绍 shutdown 是一个 Linux 系统调用,专门用于更精细地控制已连接套接字的关闭过程。与 close 函数不同(close 会完全关闭套接字,释放其文件描述符),shutdown 允许你:
data-ad-format="fluid"
data-ad-layout-key="-7k+ex-4a-9w+4a">
关闭数据流的一个方向:例如,告诉对方“我不会再发送数据了”(但仍可以接收数据)。
关闭数据流的两个方向:完全禁止在此套接字上进行任何发送和接收操作(但仍保持文件描述符打开,直到调用 close)。
你可以把 shutdown 想象成电话通话中的话筒控制:
全双工通话:你可以说话(发送),也可以听对方说话(接收)。
shutdown(SHUT_WR):相当于你按下了“禁麦”按钮。你不能再说话(发送数据),但你仍然可以听到对方说话(接收数据)。
shutdown(SHUT_RD):相当于你戴上了耳塞。你听不到对方说话(接收数据),但(理论上)你还可以说话(发送数据)——不过对方可能听不到或会收到错误。
shutdown(SHUT_RDWR):相当于你挂断了电话的通话功能。你既不能说也不能听,但电话线(套接字文件描述符)本身可能还没被物理拔掉(close)。
这对于实现优雅的连接关闭(如 TCP 的四次挥手)和单向通信非常有用。
2. 函数原型 1 2 3 4 #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. 返回值
6. 相似函数,或关联函数
close: 完全关闭套接字,释放其文件描述符。如果套接字的引用计数变为 0,其效果类似于 shutdown(SHUT_RDWR) 后再释放资源。通常在 shutdown 之后调用 close。
read / write / send / recv: shutdown 会影响这些函数的行为。例如,shutdown(SHUT_RD) 后 read 会立即返回 0。
TCP 协议: shutdown 的行为与 TCP 连接的状态转换密切相关,特别是 FIN 包的发送和接收。
7. 示例代码 示例 1:TCP 客户端使用 shutdown 实现半关闭 这个例子演示了 TCP 客户端如何在发送完所有数据后,使用 shutdown(SHUT_WR) 告诉服务器它不会再发送更多数据,然后继续接收服务器的回复。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 // 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; }
代码解释:
客户端创建 TCP 套接字并连接到服务器。
使用 write 向服务器发送一条消息。
关键步骤: 调用 shutdown(sock, SHUT_WR)。
这会向服务器发送一个 TCP FIN 包,表明客户端不会再发送数据。
服务器的 read 调用在收到这个 FIN 后会返回 0(EOF)。
但是,客户端的套接字仍然打开,并且仍然可以接收数据。
客户端进入一个 while 循环,使用 read 继续接收服务器可能发送的任何回复数据,直到服务器也关闭连接(read 返回 0)。
最后,调用 close(sock) 完全关闭套接字文件描述符。
示例 2:TCP 服务器使用 shutdown 响应客户端 这个例子演示了 TCP 服务器如何在收到客户端的 FIN(即 read 返回 0)后,使用 shutdown 和 close 来优雅地关闭连接。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 // 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; }
代码解释:
服务器创建、绑定、监听套接字,并 accept 客户端连接。
服务器进入一个 while 循环,使用 read 从客户端接收数据。
当 read 返回 0 时,表示客户端已调用 shutdown(SHUT_WR) 或 close,其发送方向已关闭。
服务器向客户端发送一个最终的回复消息。
关键步骤: 服务器调用 shutdown(client_fd, SHUT_WR)。
最后,服务器调用 close(client_fd) 完全关闭与该客户端的连接。
示例 3:对比 shutdown 和 close 这个例子通过伪代码和解释来说明 shutdown 和 close 的区别。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // 假设 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: 这是一种更优雅和明确的关闭方式。
发送完数据后,调用 shutdown(SHUT_WR) 明确表示“数据发送完毕”。这会可靠地发送 FIN 给对方。
程序继续使用 read 来接收对方可能在收到 FIN 后发送的剩余数据。
当 read 也返回 0(收到对方的 FIN 并回复 ACK)时,双方都确认了连接的单向关闭。
最后调用 close 仅仅是清理本地资源(文件描述符)。
重要提示与注意事项: 仅适用于连接型套接字: shutdown 主要用于面向连接的套接字,如 TCP (SOCK_STREAM)。对于无连接的套接字,如 UDP (SOCK_DGRAM),它的行为是未定义的或没有意义的。
不释放文件描述符: shutdown 不会关闭套接字的文件描述符。你仍然需要调用 close() 来最终释放资源。
TCP 语义: shutdown 的行为与底层 TCP 协议紧密相关。SHUT_WR 导致发送 FIN,SHUT_RD 影响接收缓冲区的行为。
优雅关闭: 在需要确保所有数据都被发送和接收的场景中(如 HTTP/1.1 Connection: close),使用 shutdown 是实现优雅关闭的标准方法。
错误处理: 始终检查 shutdown 的返回值。在套接字已经关闭或无效时调用它会失败。
SHUT_RD 的实用性: SHUT_RD 的使用场景相对较少。关闭接收通常意味着你不再关心对方的数据,直接 close 或在 read 返回 0 后 close 通常就足够了。
总结:
shutdown 是一个用于精细控制 TCP 连接关闭过程的系统调用。它允许程序在完全终止连接之前,单方面地关闭数据流的一个或两个方向。这对于实现协议规定的优雅关闭序列(如 HTTP)和处理单向数据流非常重要。理解它与 close 的区别,并在需要时正确使用它,是编写健壮网络应用程序的关键技能之一。
https://www.calcguide.tech/2025/08/11/shutdown系统调用及示例/