TCP连接关闭:优雅关闭与半关闭
TCP 连接是全双工(Full-Duplex)通信通道,数据可在两个方向上独立传输。连接关闭过程同样需要考虑双向数据流的独立处理。本文将深入分析 TCP 连接关闭的两种方式——close() 与 shutdown(),并探讨"优雅关闭"(Graceful Close)与"半关闭"(Half-Close)的实现机制。
TCP 连接的双向特性
TCP 连接支持双向独立的数据传输:
- 客户端到服务器端方向:客户端通过套接字写入数据,服务器端读取数据。
- 服务器端到客户端方向:服务器端通过套接字写入数据,客户端读取数据。
在大多数应用场景中,TCP 连接的关闭并非同时关闭两个方向,而是先关闭一个方向,待另一方向的数据传输完成后,再关闭剩余方向。这种模式被称为"半关闭"(Half-Close)。
半关闭的应用场景
典型的半关闭场景:客户端主动发起连接关闭,关闭客户端到服务器端的数据流方向。此时:
- 客户端不再向服务器端写入数据。
- 服务器端读取完客户端数据后,检测到 EOF(End of File)。
- 服务器端可能仍需处理客户端的最后请求(如数据库操作),并将结果返回客户端。
- 服务器端完成处理后,关闭服务器端到客户端的数据流方向。
- 连接完全关闭。
若服务器端处理不当,可能导致处理结果无法正常传送给客户端,破坏应用逻辑。
close() 函数
close() 函数是最常用的连接关闭方式:
int close(int sockfd);返回值:成功返回 0,失败返回 -1。
行为机制
close() 函数对套接字执行以下操作:
- 引用计数减一:套接字可被多个进程共享(如通过
fork()创建子进程)。每个套接字维护一个引用计数,fork()时计数加一,close()时计数减一。 - 引用计数归零时的处理:
- 输入方向:套接字被标记为不可读,后续读操作返回错误。
- 输出方向:内核尝试发送发送缓冲区中的残留数据,随后发送 FIN 报文,后续写操作返回错误。
若对端在本地关闭后仍继续发送数据,本地将回复 RST 报文,通知对端连接已关闭。
close() 的局限性
close() 函数无法实现单向关闭——它总是关闭两个方向的数据流。当需要仅关闭一个方向时,应使用 shutdown() 函数。
shutdown() 函数
shutdown() 函数提供更精细的关闭控制:
int shutdown(int sockfd, int howto);返回值:成功返回 0,失败返回 -1。
howto 参数选项
| 参数值 | 宏定义 | 行为描述 |
|---|---|---|
| 0 | SHUT_RD | 关闭读方向。后续读操作直接返回 EOF。接收缓冲区已有数据被丢弃,新到达的数据被 ACK 后静默丢弃。 |
| 1 | SHUT_WR | 关闭写方向(半关闭)。发送缓冲区数据被发送,随后发送 FIN 报文。后续写操作返回错误。 |
| 2 | SHUT_RDWR | 同时关闭读和写两个方向。 |
close() 与 shutdown() 的关键差异
| 特性 | close() | shutdown() |
|---|---|---|
| 资源释放 | 引用计数归零时释放套接字资源 | 不释放套接字资源 |
| 引用计数 | 受引用计数影响,可能不立即关闭 | 不受引用计数影响,立即生效 |
| FIN 报文 | 仅在引用计数归零时发送 | 总是发送 FIN 报文 |
| 单向关闭 | 不支持 | 支持(SHUT_RD、SHUT_WR) |
关键结论:当需要通知对端本地已完成数据发送时,应使用 shutdown(sockfd, SHUT_WR),而非 close()。前者保证 FIN 报文被发送,后者可能因引用计数未归零而不发送 FIN。
优雅关闭与强制关闭的对比
半关闭状态机
TCP 连接在半关闭过程中的状态转换如下:
实践示例
以下示例演示 close() 与 shutdown() 的行为差异。
客户端程序
客户端从标准输入接收用户命令,通过套接字发送给服务器端,并显示服务器端的响应。用户输入 "close" 时调用 close() 关闭连接;输入 "shutdown" 时调用 shutdown() 关闭写方向。
#include "lib/common.h"
#define MAXLINE 4096
int main(int argc, char **argv) {
if (argc != 2) {
error(1, 0, "usage: graceclient <IPaddress>");
}
int socket_fd;
socket_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], &server_addr.sin_addr);
socklen_t server_len = sizeof(server_addr);
int connect_rt = connect(socket_fd, (struct sockaddr *) &server_addr, server_len);
if (connect_rt < 0) {
error(1, errno, "connect failed");
}
char send_line[MAXLINE], recv_line[MAXLINE + 1];
int n;
fd_set readmask;
fd_set allreads;
FD_ZERO(&allreads);
FD_SET(0, &allreads);
FD_SET(socket_fd, &allreads);
for (;;) {
readmask = allreads;
int rc = select(socket_fd + 1, &readmask, NULL, NULL, NULL);
if (rc <= 0)
error(1, errno, "select failed");
if (FD_ISSET(socket_fd, &readmask)) {
n = read(socket_fd, recv_line, MAXLINE);
if (n < 0) {
error(1, errno, "read error");
} else if (n == 0) {
error(1, 0, "server terminated\n");
}
recv_line[n] = 0;
fputs(recv_line, stdout);
fputs("\n", stdout);
}
if (FD_ISSET(0, &readmask)) {
if (fgets(send_line, MAXLINE, stdin) != NULL) {
if (strncmp(send_line, "shutdown", 8) == 0) {
FD_CLR(0, &allreads);
if (shutdown(socket_fd, SHUT_WR)) {
error(1, errno, "shutdown failed");
}
} else if (strncmp(send_line, "close", 5) == 0) {
FD_CLR(0, &allreads);
if (close(socket_fd)) {
error(1, errno, "close failed");
}
sleep(6);
exit(0);
} else {
int i = strlen(send_line);
if (send_line[i - 1] == '\n') {
send_line[i - 1] = 0;
}
printf("now sending %s\n", send_line);
size_t rt = write(socket_fd, send_line, strlen(send_line));
if (rt < 0) {
error(1, errno, "write failed");
}
printf("send bytes: %zu\n", rt);
}
}
}
}
}服务器端程序
服务器端接收客户端数据,格式化后返回响应。为模拟处理延迟,服务器端在发送响应前休眠 5 秒。
#include "lib/common.h"
static int count;
static void sig_int(int signo) {
printf("\nreceived %d datagrams\n", count);
exit(0);
}
int main(int argc, char **argv) {
int listenfd;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERV_PORT);
int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
if (rt1 < 0) {
error(1, errno, "bind failed");
}
int rt2 = listen(listenfd, LISTENQ);
if (rt2 < 0) {
error(1, errno, "listen failed");
}
signal(SIGINT, sig_int);
signal(SIGPIPE, SIG_IGN);
int connfd;
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
if ((connfd = accept(listenfd, (struct sockaddr *) &client_addr, &client_len)) < 0) {
error(1, errno, "accept failed");
}
char message[MAXLINE];
count = 0;
for (;;) {
int n = read(connfd, message, MAXLINE);
if (n < 0) {
error(1, errno, "error read");
} else if (n == 0) {
error(1, 0, "client closed\n");
}
message[n] = 0;
printf("received %d bytes: %s\n", n, message);
count++;
char send_line[MAXLINE];
sprintf(send_line, "Hi, %s", message);
sleep(5);
int write_nc = send(connfd, send_line, strlen(send_line), 0);
printf("send bytes: %zu\n", write_nc);
if (write_nc < 0) {
error(1, errno, "error write");
}
}
}实验结果分析
场景一:使用 close() 关闭连接
# 客户端输出
$ ./graceclient 127.0.0.1
data1
now sending data1
send bytes: 5
data2
now sending data2
send bytes: 5
close
# 服务器端输出
$ ./graceserver
received 5 bytes: data1
send bytes: 9
received 5 bytes: data2
send bytes: 9
client closed客户端调用 close() 后,两个方向均被关闭。服务器端发送的响应数据到达时,客户端回复 RST 报文。服务器端继续发送数据时,触发 SIGPIPE 信号(已在程序中忽略),导致数据丢失。
场景二:使用 shutdown() 关闭写方向
# 客户端输出
$ ./graceclient 127.0.0.1
data1
now sending data1
send bytes: 5
data2
now sending data2
send bytes: 5
shutdown
Hi, data1
Hi, data2
server terminated
# 服务器端输出
$ ./graceserver
received 5 bytes: data1
send bytes: 9
received 5 bytes: data2
send bytes: 9
client closed客户端调用 shutdown(sockfd, SHUT_WR) 后,仅关闭写方向,读方向仍可用。服务器端的响应数据正常到达客户端。服务器端检测到 EOF 后关闭连接,客户端正常退出。
SIGPIPE 信号处理
当进程向已收到 RST 的套接字写入数据时,内核将向进程发送 SIGPIPE 信号,默认行为是终止进程。在网络编程中,应妥善处理该信号:
// 方式一:忽略信号
signal(SIGPIPE, SIG_IGN);
// 方式二:自定义处理函数
static void sig_pipe(int signo) {
printf("received SIGPIPE\n");
// 可选:执行清理操作后退出
}
signal(SIGPIPE, sig_pipe);内核版本注释:在 Linux 2.6.32+ 中,可通过 MSG_NOSIGNAL 标志在 send() 调用时禁用 SIGPIPE 信号,而不影响全局信号处理配置:
send(sockfd, buffer, len, MSG_NOSIGNAL);总结
close()函数将套接字引用计数减一,仅在引用计数归零时关闭连接的两个方向。shutdown()函数不受引用计数影响,可精确控制关闭方向,且总是发送 FIN 报文。- 在需要通知对端本地已完成数据发送的场景中,应使用
shutdown(sockfd, SHUT_WR)实现优雅关闭。 - 应正确处理
SIGPIPE信号,避免进程异常终止。
思考题
- 在服务器端程序中,直接调用
exit(0)即可完成 FIN 报文的发送,原因是什么?为何无需显式调用close()或shutdown()? SIG_IGN(忽略信号)与自定义信号处理函数有何区别?在何种场景下应选择前者或后者?
版本信息
| 项目 | 内容 |
|---|---|
| 更新日期 | 2026-06-09 |
| 目标内核 | Linux 7.0 |
| 关键特性 | MSG_NOSIGNAL 标志(2.6.32+)、SO_LINGER 选项 |