shutdown系统调用及示例

我们继续学习 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. 返回值

  • 成功时: 返回 0。

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

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&#91;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&#91;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&#91;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&#91;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)。

  • 这会向客户端发送一个 TCP FIN 包。

  • 客户端的 read 调用在收到这个 FIN 后会返回 0。

最后,服务器调用 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&#91;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系统调用及示例/

data-ad-format="auto" data-full-width-responsive="true">