我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 shutdown
函数,它用于部分或完全地关闭一个面向连接的套接字(如 TCP 套接字)的数据传输。
1. 函数介绍
shutdown
是一个 Linux 系统调用,专门用于更精细地控制已连接套接字的关闭过程。与 close
函数不同(close
会完全关闭套接字,释放其文件描述符),shutdown
允许你:
- 关闭数据流的一个方向:例如,告诉对方“我不会再发送数据了”(但仍可以接收数据)。
- 关闭数据流的两个方向:完全禁止在此套接字上进行任何发送和接收操作(但仍保持文件描述符打开,直到调用
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
/recv
:shutdown
会影响这些函数的行为。例如,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;
}
代码解释:
- 客户端创建 TCP 套接字并连接到服务器。
- 使用
write
向服务器发送一条消息。 - 关键步骤: 调用
shutdown(sock, SHUT_WR)
。- 这会向服务器发送一个 TCP
FIN
包,表明客户端不会再发送数据。 - 服务器的
read
调用在收到这个FIN
后会返回 0(EOF)。 - 但是,客户端的套接字仍然打开,并且仍然可以接收数据。
- 这会向服务器发送一个 TCP
- 客户端进入一个
while
循环,使用read
继续接收服务器可能发送的任何回复数据,直到服务器也关闭连接(read
返回 0)。 - 最后,调用
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;
}
代码解释:
- 服务器创建、绑定、监听套接字,并
accept
客户端连接。 - 服务器进入一个
while
循环,使用read
从客户端接收数据。 - 当
read
返回 0 时,表示客户端已调用shutdown(SHUT_WR)
或close
,其发送方向已关闭。 - 服务器向客户端发送一个最终的回复消息。
- 关键步骤: 服务器调用
shutdown(client_fd, SHUT_WR)
。- 这会向客户端发送一个 TCP
FIN
包。 - 客户端的
read
调用在收到这个FIN
后会返回 0。
- 这会向客户端发送一个 TCP
- 最后,服务器调用
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
: 这是一种更优雅和明确的关闭方式。- 发送完数据后,调用
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
的区别,并在需要时正确使用它,是编写健壮网络应用程序的关键技能之一。