一、核心技术:IPv4/IPv6 双栈兼容的关键设置
要实现双栈兼容,需理解四个核心概念:hints.ai_family
=AF_UNSPEC
、hints.ai_flags
=AI_PASSIVE
、getaddrinfo
函数、INET6_ADDRSTRLEN
宏。它们共同解决了 IPv4 与 IPv6 协议差异带来的适配问题。
1. hints.ai_family = AF_UNSPEC:协议无关的地址解析
hints
是getaddrinfo
的查询条件结构体,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_PASSIVE
是getaddrinfo
的标志位,仅用于服务器端,作用是:
当getaddrinfo
的第一个参数(node)为NULL时,自动将地址设为通配地址(Wildcard Address)—— 即服务器监听本机所有网络接口(包括所有 IPv4/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_in
和struct sockaddr_in6
)。
使用流程:
初始化hints结构体(指定协议类型、套接字类型、标志位);
调用getaddrinfo
获取地址列表;
遍历列表,创建套接字并绑定 / 连接;
调用freeaddrinfo
释放内存(避免内存泄漏)。
1.4 INET6_ADDRSTRLEN:安全存储 IP 地址字符串
IP 地址需从二进制(如struct in_addr
)转为字符串(如192.168.1)才能显示,不同协议的字符串长度不同:
为什么用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 服务器核心逻辑解析
三、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):
灵活性设计:
支持命令行参数指定服务器地址和端口(如./client ::1 9527连接 IPv6 本地服务器,./client 192.168.100 9527连接远程 IPv4 服务器)。