我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 shutdown 函数,它用于部分或完全地关闭一个面向连接的套接字(如 TCP 套接字)的数据传输。
1. 函数介绍 shutdown 是一个 Linux 系统调用,专门用于更精细地控制已连接套接字的关闭过程。与 close 函数不同(close 会完全关闭套接字,释放其文件描述符),shutdown 允许你:
关闭数据流的一个方向:例如,告诉对方“我不会再发送数据了”(但仍可以接收数据)。
关闭数据流的两个方向:完全禁止在此套接字上进行任何发送和接收操作(但仍保持文件描述符打开,直到调用 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 #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" ); 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); } printf ("Shutting down write direction (SHUT_WR)...\n" ); if (shutdown (sock, SHUT_WR) < 0 ) { perror ("shutdown SHUT_WR failed" ); } else { printf ("Write direction shut down successfully.\n" ); } 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" ); } 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 int main() { int server_fd, client_fd; struct sockaddr_in address, client_address; socklen_t client_addr_len = sizeof(client_address); char buffer& 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& 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 write (sock, "Hello", 5 );close (sock); write (sock, "Hello", 5 );shutdown (sock, SHUT_WR); char buffer[ ;1024 ]; ssize_t n; while ((n = read(sock, buffer, sizeof(buffer))) > 0 ) { }close (sock);
解释:
仅使用 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系统调用及示例/
相关阅读