「Linux 网络编程」基于 TCP 的半关闭

1 基于 TCP 的半关闭

TCP 具有核心特性——全双工通信(即通信双方可同时双向传输数据)。半关闭是 TCP 为适配 单向数据传输完成后需保留反向通道 场景设计的机制,本质是 仅关闭连接的一个方向(发送或接收),保留另一个方向的通信能力,而非完全断开连接。


1.1 半关闭的核心逻辑

TCP 默认的 完全关闭通过四次挥手完成)会断开双向通道,但实际场景中常需 单向传输结束后,反向仍需传数据

例如,客户端向服务器发送完文件(客户端→服务器的发送已完成),但服务器需向客户端返回 文件接收成功/失败 的确认信息(服务器→客户端的接收需保留)。此时若直接完全关闭,确认信息将无法传输 —— 半关闭恰好解决了“单向收尾、反向保留”的需求


1.2 半关闭的实现

TCP 的连接关闭依赖 FIN报文(Finish,标识 此方向无更多数据要发送),半关闭的本质是 通信一方主动发送FIN报文关闭发送端,但保留接收端,另一方通过 ACK 确认后,完成单方向的关闭;反向通道仍可正常传输数据,直到另一方也发送 FIN 报文,才完成全关闭。

结合 TCP 的 四次挥手 过程,半关闭对应前两次挥手的核心阶段,即 半关闭状态 = 第一次挥手 + 第二次挥手

当主动关闭方调用 shutdown(SHUT_WR)(半关闭核心系统调用函数) 时:

  1. 第一次挥手:发送 FIN 报文

    表示"我没有数据要发送了",但还可以接收数据。主动关闭方进入 FIN_WAIT_1 状态。

  2. 第二次挥手:接收对方的 ACK 响应

    对端确认收到 FIN,主动关闭方进入 FIN_WAIT_2 状态。此时连接处于半关闭状态

在代码中的体现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 客户端执行半关闭
printf("Calling shutdown(SHUT_WR)...\n");
shutdown(sock, SHUT_WR); // 触发第一次挥手

// 此时:
// - 客户端不能再发送数据 (SHUT_WR)
// - 但可以继续接收服务器发送的数据
// - 连接处于半关闭状态

// 服务器检测到FIN后:
// - 服务器知道客户端不再发送数据
// - 但服务器仍可以继续向客户端发送数据
// - 服务器发送ACK确认(第二次挥手)

// 客户端可以继续接收数据
recv(sock, buffer, BUFFER_SIZE, 0); // 在半关闭状态下仍然可以接收


2 半关闭核心调用 shutdown

2.1 函数原型及参数说明

1
2
3
#include <sys/socket.h>

int shutdown(int sockfd, int how);

参数说明

  • sockfd

    • 要关闭的套接字文件描述符
    • 必须是已连接的套接字
  • how

    指定关闭的方式,有三种选择:

    参数值 说明
    SHUT_RD (0) 关闭读通道,不再接收数据
    SHUT_WR (1) 关闭写通道,不再发送数据
    SHUT_RDWR (2) 同时关闭读写通道

返回值

  • 成功:返回 0
  • 失败:返回 -1,并设置 errno

2.2 功能详解

  1. SHUT_RD - 关闭读通道

    1
    shutdown(sockfd, SHUT_RD);

    效果

    • 套接字不能再接收数据
    • 接收缓冲区中的现有数据会被丢弃
    • 后续的 recv(), read() 调用返回 0(EOF)
    • 对端如果发送数据,会收到 RST 复位包
  2. SHUT_WR - 关闭写通道(最常用)

    1
    shutdown(sockfd, SHUT_WR);

    效果

    • 套接字不能再发送数据
    • 发送缓冲区中未发送的数据会继续发送
    • 发送 FIN 包给对端,触发 TCP 四次挥手的前两次
    • 进入半关闭状态,仍然可以接收数据
  3. SHUT_RDWR - 同时关闭读写

    1
    shutdown(sockfd, SHUT_RDWR);

    效果

    • 结合了 SHUT_RDSHUT_WR 的效果
    • 不能发送也不能接收数据
    • 但连接还没有完全关闭,需要调用 close()

2.3 与 close() 的关键区别

方面 shutdown() close()
关闭粒度 可以 单独控制读/写方向 总是关闭整个套接字
连接状态 连接仍然存在,可以 半关闭 连接完全终止
多进程影响 影响所有进程中的该连接 只影响当前进程的文件描述符
引用计数 不减少文件描述符引用计数 减少引用计数,为 0 时真正关闭
TCP行为 发送 FIN 包,进入半关闭状态 发送 FIN 包,完全关闭连接


3 半关闭简单 TCP 本地通信示例

3.1 服务端代码

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
// server_shutdown.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 8080
#define BUF_SIZE 1024
#define BACKLOG 5

int main() {
int server_fd;
struct sockaddr_in server_addr;
int client_fd;
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);

// 1. 创建 server socket
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket failed");
exit(EXIT_FAILURE);
}

// 设置套接字选项,避免地址占用错误
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
perror("setsockopt failed");
close(server_fd);
exit(EXIT_FAILURE);
}

// 2. 配置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = INADDR_ANY;

// 3. 绑定地址
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}

// 4. 设置监听上限
if (listen(server_fd, BACKLOG) < 0) {
perror("listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("TCP Echo Server started on port %d\n", PORT);
printf("Server is listening for connections...\n");

// 5. 接受和处理客户端连接,同时获取到客户端地址信息
client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd < 0) {
perror("accept failed");
exit(EXIT_FAILURE);
}
printf("Client connected\n");

char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
printf("Handling client %s:%d\n", client_ip, ntohs(client_addr.sin_port));

// 接受客户端数据,发送响应
char buffer[BUF_SIZE];
memset(buffer, 0, BUF_SIZE);

ssize_t recv_len = 0;
while ((recv_len = recv(client_fd, buffer, BUF_SIZE, 0)) > 0) {
printf("Received from %s: %s", client_ip, buffer);

// 发送响应
char *response = "Message received by server\n";
send(client_fd, response, strlen(response), 0);

memset(buffer, 0, BUF_SIZE);
}

if (recv_len == 0) {
printf("Client disconnected\n");
} else {
perror("recv");
}

// 6. 关闭套接字
close(client_fd);
close(server_fd);

return 0;
}

3.2 客户端代码

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
// client_shutdown.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define SERVER_IP "127.0.0.1"
#define PORT 8080
#define BUF_SIZE 1024

int main() {
int sockfd;
struct sockaddr_in server_addr;

// 1. 创建客户端 socket
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}

// 2. 配置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);

// 将字符串IP转换为二进制格式
if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
perror("invalid address");
close(sockfd);
exit(EXIT_FAILURE);
}

// 3. 连接服务器
printf("Connecting to server %s:%d...\n", SERVER_IP, PORT);
if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("connect failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Connected to server successfully!\n");

// 4. 发送数据到服务器
char* message = "Hello Server, this is Marisa.\n";
ssize_t send_len = send(sockfd, message, strlen(message), 0);
if (send_len < 0) {
perror("send failed");
exit(EXIT_FAILURE);
}
printf("Message sent\n");

// * 使用 shutdown 关闭写端,但仍然可以接收服务端回显的数据
printf("Calling shutdown(SHUT_WR)...\n");
if (shutdown(sockfd, SHUT_WR) == 0) {
printf("Write side shutdown, but can still receive server data\n");
}
// 如果此处使用 close,读写端都被关闭,因此无法接收后续服务器的响应

// 5. 接收服务器的响应
char buffer[BUF_SIZE];
memset(buffer, 0, BUF_SIZE);
ssize_t recv_len = 0;
if ((recv_len = recv(sockfd, buffer, BUF_SIZE, 0)) > 0) {
printf("Server response: %s", buffer);
}

close(sockfd);
printf("Connection closed. Goodbye!\n");

return 0;
}

3.3 编译和运行测试



4 半关闭的典型适用场景

  1. HTTP/1.0 的短连接

    HTTP/1.0 默认使用“短连接”,客户端发送请求后,通过 Connection: close 头告知服务器“请求已发送完成”;服务器返回响应后,会主动发送FIN报文关闭 “服务器→客户端” 的发送端(半关闭),客户端接收响应后再发送 FIN,完成全关闭。

  2. 文件传输协议(如FTP的数据连接)

    FTP的数据连接用于传输文件:客户端向服务器发送“下载请求”后,关闭自己的数据连接发送端(半关闭),仅保留接收端用于接收文件;服务器传输完文件后,发送FIN关闭自己的发送端,完成全关闭。

  3. 双向数据流的“单向收尾”

    例如,即时通讯中,用户A向用户B发送“结束对话”的消息后,关闭自己的发送端(不再发消息),但保留接收端以接收用户B的最后回复;用户B回复后,再关闭自己的发送端,最终断开连接。


「Linux 网络编程」基于 TCP 的半关闭
https://marisamagic.github.io/2025/10/09/20251009/
作者
MarisaMagic
发布于
2025年10月9日
许可协议