一、核心技术:IPv4/IPv6 双栈兼容的关键设置

要实现双栈兼容,需理解四个核心概念:hints.ai_family=AF_UNSPEChints.ai_flags=AI_PASSIVEgetaddrinfo函数、INET6_ADDRSTRLEN宏。它们共同解决了 IPv4 与 IPv6 协议差异带来的适配问题。

1. hints.ai_family = AF_UNSPEC:协议无关的地址解析

hintsgetaddrinfo的查询条件结构体,ai_family指定地址族(协议类型):

  • AF_INET:仅解析 IPv4 地址(对应struct sockaddr_in);

  • AF_INET6:仅解析 IPv6 地址(对应struct sockaddr_in6);

  • AF_UNSPEC:不限制协议,同时解析 IPv4 和 IPv6 地址。

为什么选AF_UNSPEC

现代服务器需同时响应 IPv4 和 IPv6 客户端的连接(例如用户可能通过192.168.100或fe80::1访问)。AF_UNSPEC让getaddrinfo返回两种协议的地址列表,程序只需遍历列表即可创建对应套接字,无需手动区分协议。

1.2 hints.ai_flags = AI_PASSIVE:服务器的通配地址绑定

AI_PASSIVEgetaddrinfo的标志位,仅用于服务器端,作用是:

getaddrinfo的第一个参数(node)为NULL时,自动将地址设为通配地址(Wildcard Address)—— 即服务器监听本机所有网络接口(包括所有 IPv4/IPv6 网卡)。

  • IPv4 通配地址:0.0.0.0(表示监听所有 IPv4 接口);

  • IPv6 通配地址:::(表示监听所有 IPv6 接口)。

为什么需要它?

若硬编码绑定到某个具体地址(如127.0.0.1),服务器只能接收该地址的连接;而AI_PASSIVE让服务器自动适配所有网络接口,无需关心本机的 IP 配置,灵活性更高。

1.3 getaddrinfo:统一的地址解析入口

传统 IPv4 编程依赖inet_addr(仅解析 IPv4 字符串),而getaddrinfo是 POSIX 标准的协议无关解析函数,核心优势:

支持 IPv4/IPv6 双栈解析;

可解析域名(如localhost自动转为127.0.0.1或::1);

返回的struct addrinfo列表包含套接字创建所需的所有信息(地址族、类型、协议、地址长度);

自动处理地址结构体差异(无需手动转换struct sockaddr_instruct sockaddr_in6)。

使用流程

初始化hints结构体(指定协议类型、套接字类型、标志位);

调用getaddrinfo获取地址列表;

遍历列表,创建套接字并绑定 / 连接;

调用freeaddrinfo释放内存(避免内存泄漏)。

1.4 INET6_ADDRSTRLEN:安全存储 IP 地址字符串

IP 地址需从二进制(如struct in_addr)转为字符串(如192.168.1)才能显示,不同协议的字符串长度不同:

  • IPv4 地址最长:255.255.255.255(15 个字符),对应宏INET_ADDRSTRLEN(值为 16,含终止符\0);

  • IPv6 地址最长:ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff(39 个字符),若含作用域标识(如fe80::1%eth0)则更长,对应宏INET6_ADDRSTRLEN(值为 46,含终止符)。

为什么用INET6_ADDRSTRLEN**?**

双栈程序需同时处理两种地址的字符串转换,INET6_ADDRSTRLEN的长度足够容纳 IPv4 和 IPv6 地址,避免缓冲区溢出(如用INET_ADDRSTRLEN存储 IPv6 地址会截断)。

二、TCP 回声服务器实现(双栈兼容)

服务器核心逻辑:创建监听套接字 → 循环接受客户端连接 → fork 子进程处理回声请求 → 回收子进程避免僵尸进程。

2.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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <netdb.h>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <cerrno>
#include <cstdlib>

using namespace std;

/**
* 创建并配置TCP监听套接字(支持IPv4/IPv6双栈)
* @return 成功返回监听套接字描述符,失败则退出程序
*/
int createListener() {
struct addrinfo hints,*result,*p;
memset(&hints, 0, sizeof(hints)); // 初始化hints为0
hints.ai_family = AF_UNSPEC; // 双栈兼容:同时解析IPv4和IPv6
hints.ai_socktype = SOCK_STREAM; // TCP套接字类型
hints.ai_flags = AI_PASSIVE; // 服务器模式:绑定通配地址
const char* service = "9527"; // 监听端口(可修改)

// 解析地址信息:将"端口"转为二进制地址结构
int err = getaddrinfo(NULL, service, &hints, &result);
if (err) {
cerr << "[错误] getaddrinfo解析失败:" << gai_strerror(err) << endl;
exit(EXIT_FAILURE);
}

int listenfd = -1;
// 遍历地址列表,尝试创建并配置套接字
for (p = result; p != nullptr; p = p->ai_next) {
// 创建套接字(自动适配IPv4/IPv6)
listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (listenfd == -1) {
cerr << "[警告] 创建套接字失败:" << strerror(errno) << ",尝试下一个地址..." << endl;
continue;
}

// 2. 设置地址重用:避免服务器重启时"地址已在使用"错误
int opt = 1;
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
cerr << "[错误] setsockopt失败:" << strerror(errno) << endl;
close(listenfd);
continue;
}

// 3. 绑定套接字到端口(IPv4绑定0.0.0.0:9527,IPv6绑定:::9527)
if (bind(listenfd, p->ai_addr, p->ai_addrlen) == -1) {
cerr << "[警告] 绑定端口失败:" << strerror(errno) << ",尝试下一个地址..." << endl;
close(listenfd);
continue;
}

// 4. 开始监听:backlog=5(队列中最多5个等待连接)
if (listen(listenfd, 5) == -1) {
cerr << "[错误] 监听失败:" << strerror(errno) << endl;
close(listenfd);
continue;
}

// 成功创建监听套接字,退出循环
break;
}

// 释放地址列表内存(必须调用,避免内存泄漏)
freeaddrinfo(result);

// 检查是否成功创建监听套接字
if (p == nullptr || listenfd == -1) {
cerr << "[错误] 所有地址尝试失败,无法创建监听套接字" << endl;
exit(EXIT_FAILURE);
}

cout << "[信息] 服务器启动成功,监听端口 " << service << "(支持IPv4/IPv6)" << endl;
return listenfd;
}

/**
* 打印客户端IP地址(兼容IPv4/IPv6)
* @param ss 存储客户端地址的结构体(sockaddr_storage可容纳任意地址类型)
*/
void printClientIP(struct sockaddr_storage &ss) {
char ipstr[INET6_ADDRSTRLEN]; // 足够存储IPv4/IPv6地址的缓冲区
void*addr;

// 根据地址族提取IP地址(区分IPv4和IPv6)
if (ss.ss_family == AF_INET) { // IPv4地址
struct sockaddr_in*ipv4 = (struct sockaddr_in*)&ss;
addr = &(ipv4->sin_addr); // 指向IPv4地址字段
} else { // IPv6地址
struct sockaddr_in6*ipv6 = (struct sockaddr_in6*)&ss;
addr = &(ipv6->sin6_addr); // 指向IPv6地址字段
}

// 将二进制地址转为字符串(inet_ntop:network to presentation)
if (inet_ntop(ss.ss_family, addr, ipstr, sizeof(ipstr)) == nullptr) {
cerr << "[错误] 转换IP地址失败:" << strerror(errno) << endl;
return;
}

cout << "[信息] 新客户端连接:" << ipstr << endl;
}

/**
* 处理单个客户端的回声请求:接收数据并原样返回
* @param connfd 与客户端连接的套接字描述符
*/
void handleEcho(int connfd) {
char buf[1024];
ssize_t recvLen; // 接收的字节数(ssize_t支持负数,表示错误)

// 循环接收客户端数据(直到客户端关闭连接)
while ((recvLen = recv(connfd, buf, sizeof(buf) - 1, 0)) > 0) {
buf[recvLen] = '\0'; // 手动添加字符串终止符(recv不自动加)
cout << "[信息] 收到客户端数据:" << buf << endl;

// 确保所有数据发送到客户端(send可能只发送部分数据)
ssize_t totalSent = 0;
while (totalSent < recvLen) {
ssize_t sent = send(connfd, buf + totalSent, recvLen - totalSent, 0);
if (sent == -1) {
cerr << "[错误] 发送数据失败:" << strerror(errno) << endl;
close(connfd);
return;
}
totalSent += sent;
}

memset(buf, 0, sizeof(buf)); // 清空缓冲区,准备下一次接收
}

// 处理recv返回值:0=客户端关闭,-1=错误
if (recvLen == 0) {
cout << "[信息] 客户端正常关闭连接" << endl;
} else if (recvLen == -1) {
cerr << "[错误] 接收数据失败:" << strerror(errno) << endl;
}

close(connfd); // 关闭连接套接字
cout << "[信息] 客户端连接已释放" << endl;
}

/**
* 信号处理函数:回收所有终止的子进程,避免僵尸进程
* @param sig 接收到的信号(此处为SIGCHLD:子进程终止时触发)
*/
void handleSigchld(int sig) {
(void)sig; // 忽略未使用的参数警告
int savedErrno = errno; // 保存errno(waitpid会修改errno)

// 非阻塞回收所有子进程(WNOHANG:无终止子进程时立即返回)
while (waitpid(-1, nullptr, WNOHANG) > 0);

errno = savedErrno; // 恢复errno(不影响其他逻辑)
}

int main(int argc, char*argv[]) {
// 注册SIGCHLD信号处理函数(子进程终止时自动回收)
struct sigaction sa;
sa.sa_handler = handleSigchld; // 绑定信号处理函数
sigemptyset(&sa.sa_mask); // 信号处理期间不屏蔽其他信号
sa.sa_flags = SA_RESTART; // 被信号中断的系统调用自动重启(如accept)
if (sigaction(SIGCHLD, &sa, nullptr) == -1) {
cerr << "[错误] sigaction注册失败:" << strerror(errno) << endl;
exit(EXIT_FAILURE);
}

// 2. 创建监听套接字
int listenfd = createListener();

// 3. 循环接受客户端连接(服务器核心逻辑)
while (true) {
struct sockaddr_storage clientAddr; // 存储客户端地址(兼容IPv4/IPv6)
socklen_t clientAddrLen = sizeof(clientAddr);

// 接受客户端连接(若被SIGCHLD中断,会因SA_RESTART自动重试)
int connfd = accept(listenfd, (struct sockaddr*)&clientAddr, &clientAddrLen);
if (connfd == -1) {
cerr << "[错误] 接受连接失败:" << strerror(errno) << endl;
continue; // 继续等待下一个连接
}

// 打印客户端IP
printClientIP(clientAddr);

// 4. fork子进程处理当前连接(父进程继续接受新连接)
pid_t pid = fork();
if (pid == -1) {
cerr << "[错误] fork子进程失败:" << strerror(errno) << endl;
close(connfd);
continue;
}

if (pid == 0) { // 子进程:处理回声请求
close(listenfd); // 子进程不需要监听套接字(避免句柄泄漏)
handleEcho(connfd);
exit(EXIT_SUCCESS); // 处理完毕后退出子进程
} else { // 父进程:释放连接套接字(子进程已复制句柄)
close(connfd);
}
}

// 理论上不会执行到这里(上面是无限循环)
close(listenfd);
return 0;
}

2.2 服务器核心逻辑解析

  • 监听套接字创建(createListener

    • 通过getaddrinfo获取双栈地址列表,遍历列表创建套接字并绑定端口。SO_REUSEADDR解决服务器重启时的 “地址占用” 问题,AI_PASSIVE确保监听所有网络接口。
  • 客户端 IP 打印(printClientIP

    • 使用struct sockaddr_storage(可容纳任意地址类型)存储客户端地址,通过inet_ntop将二进制地址转为字符串,兼容 IPv4 和 IPv6。
  • 回声处理(handleEcho

    • 循环接收客户端数据,通过send原样返回。需注意recv返回 0 表示客户端关闭,-1 表示错误;send可能分多次发送,需循环确保数据完整。
  • 子进程回收(handleSigchld

    • 子进程终止时会触发SIGCHLD信号,通过waitpid非阻塞回收所有子进程,避免僵尸进程。SA_RESTART确保accept等系统调用被信号中断后自动重试。

三、TCP 回声客户端实现(双栈兼容)

客户端核心逻辑:解析服务器地址 → 建立 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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <netdb.h>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <cerrno>
#include <cstdlib>
#include <cstdio>

using namespace std;

/**
* 建立与TCP服务器的连接(支持IPv4/IPv6双栈)
* @param node 服务器IP或域名(如"127.0.0.1"、"::1"、"localhost")
* @param service 服务器端口(如"9527")
* @return 成功返回连接套接字描述符,失败则退出程序
*/
int connectToServer(const char*node, const char*service) {
struct addrinfo hints,*result,*p;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC; // 双栈兼容:同时解析IPv4和IPv6
hints.ai_socktype = SOCK_STREAM; // TCP套接字类型

// 解析服务器地址(支持域名/IP、IPv4/IPv6)
int err = getaddrinfo(node, service, &hints, &result);
if (err) {
cerr << "[错误] 解析服务器地址失败:" << gai_strerror(err) << endl;
exit(EXIT_FAILURE);
}

int connfd = -1;
// 遍历地址列表,尝试连接服务器
for (p = result; p != nullptr; p = p->ai_next) {
// 创建套接字(自动适配服务器协议)
connfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (connfd == -1) {
cerr << "[警告] 创建套接字失败:" << strerror(errno) << ",尝试下一个地址..." << endl;
continue;
}

// 2. 连接服务器(自动适配IPv4/IPv6地址)
if (connect(connfd, p->ai_addr, p->ai_addrlen) == -1) {
cerr << "[警告] 连接服务器失败:" << strerror(errno) << ",尝试下一个地址..." << endl;
close(connfd);
continue;
}

// 连接成功,退出循环
break;
}

// 释放地址列表内存
freeaddrinfo(result);

// 检查连接是否成功
if (p == nullptr || connfd == -1) {
cerr << "[错误] 无法连接到服务器 " << node << ":" << service << endl;
exit(EXIT_FAILURE);
}

cout << "[信息] 成功连接到服务器 " << node << ":" << service << endl;
return connfd;
}

/**
* 处理与服务器的回声交互:读取用户输入→发送→接收响应→显示
* @param connfd 与服务器连接的套接字描述符
*/
void handleEchoInteraction(int connfd) {
char sendBuf[1024] = {0}; // 发送缓冲区
char recvBuf[1024] = {0}; // 接收缓冲区

cout << "[提示] 请输入要发送的内容(输入\"quit\"退出):" << endl;

// 循环处理用户输入(直到输入quit)
while (true) {
// 读取用户输入(支持带空格的输入,fgets比cin>>更友好)
if (!fgets(sendBuf, sizeof(sendBuf), stdin)) {
cerr << "\n[错误] 读取输入失败或到达EOF" << endl;
break;
}

// 2. 去除fgets保留的换行符(如输入"hello",fgets会存为"hello\n")
size_t len = strlen(sendBuf);
if (len > 0 && sendBuf[len - 1] == '\n') {
sendBuf[len - 1] = '\0';
}

// 3. 检查是否退出(输入quit)
if (strcmp(sendBuf, "quit") == 0) {
cout << "[信息] 正在退出客户端..." << endl;
break;
}

// 4. 发送数据到服务器(确保所有数据发送完成)
len = strlen(sendBuf);
ssize_t totalSent = 0;
while (totalSent < len) {
ssize_t sent = send(connfd, sendBuf + totalSent, len - totalSent, 0);
if (sent == -1) {
cerr << "[错误] 发送数据失败:" << strerror(errno) << endl;
close(connfd);
return;
}
totalSent += sent;
}

// 5. 接收服务器的回声响应
ssize_t recvLen = recv(connfd, recvBuf, sizeof(recvBuf) - 1, 0);
if (recvLen == -1) {
cerr << "[错误] 接收响应失败:" << strerror(errno) << endl;
close(connfd);
return;
} else if (recvLen == 0) {
cerr << "[错误] 服务器已关闭连接" << endl;
close(connfd);
return;
}

// 6. 显示服务器响应
recvBuf[recvLen] = '\0';
cout << "[服务器响应] " << recvBuf << endl;
cout << "[提示] 请输入下一条内容(输入\"quit\"退出):" << endl;

// 清空缓冲区,准备下一次交互
memset(sendBuf, 0, sizeof(sendBuf));
memset(recvBuf, 0, sizeof(recvBuf));
}

// 关闭连接,释放资源
close(connfd);
cout << "[信息] 已断开与服务器的连接" << endl;
}

int main(int argc, char*argv[]) {
// 支持命令行参数:./client 服务器IP 端口(默认127.0.0.1:9527)
const char* node = (argc > 1) ? argv[1] : "127.0.0.1";
const char* service = (argc > 2) ? argv[2] : "9527";

// 连接到服务器(双栈兼容)
int connfd = connectToServer(node, service);

// 2. 处理回声交互
handleEchoInteraction(connfd);

return 0;
}

3.2 客户端核心逻辑解析

服务器连接(connectToServer)

通过getaddrinfo解析服务器地址(支持域名、IPv4、IPv6),遍历列表尝试连接。若服务器同时提供 IPv4 和 IPv6 地址,客户端会自动选择可用协议。

交互处理(handleEchoInteraction)

  • 使用fgets读取用户输入(支持带空格的内容,如 “hello world”);

  • 去除fgets保留的换行符,避免多余字符发送;

  • 支持quit命令退出,提升用户体验;

  • 循环send确保数据完整,处理recv的各种返回情况(响应、服务器关闭、错误)。

灵活性设计

支持命令行参数指定服务器地址和端口(如./client ::1 9527连接 IPv6 本地服务器,./client 192.168.100 9527连接远程 IPv4 服务器)。