第一种:Linux服务器+Windows客户端
UDP-L-W------------------------------------
1. UDP 服务器(Linux 端)
udp_server_linux.cpp#include <iostream> #include <cstring> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define PORT 8080 // 监听端口 #define BUF_SIZE 1024 // 缓冲区大小 int main() { // 1. 创建 UDP 套接字(SOCK_DGRAM = UDP) int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { perror("socket 创建失败"); return -1; } // 2. 绑定 IP + 端口(服务器必须 bind) struct sockaddr_in serv_addr, cli_addr; socklen_t cli_len = sizeof(cli_addr); memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(PORT); // 端口转网络字节序 serv_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡 if (bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) { perror("bind 绑定失败"); close(sockfd); return -1; } std::cout << "UDP 服务器(Linux)已启动,监听端口 " << PORT << "..." << std::endl; char buffer[BUF_SIZE]; while (true) { // 3. 阻塞接收客户端数据 ssize_t recv_len = recvfrom( sockfd, buffer, BUF_SIZE, 0, (struct sockaddr*)&cli_addr, &cli_len ); if (recv_len < 0) { perror("recvfrom 接收失败"); continue; } buffer[recv_len] = '\0'; // 打印客户端信息 std::cout << "【收到】来自 " << inet_ntoa(cli_addr.sin_addr) << ":" << ntohs(cli_addr.sin_port) << " 的消息:" << buffer << std::endl; // 4. 回复客户端 const char* reply = "Linux 服务器已收到你的 UDP 消息!"; sendto( sockfd, reply, strlen(reply), 0, (struct sockaddr*)&cli_addr, cli_len ); std::cout << "【回复】已发送给客户端" << std::endl << std::endl; } close(sockfd); return 0; }2. UDP 客户端(Windows 端)
udp_client_windows.cpp#include <iostream> #include <cstring> // Windows 专用 Winsock 头文件 #include <winsock2.h> #include <ws2tcpip.h> // 链接 Winsock 库(MinGW 编译需加 -lws2_32,VS 自动识别) #pragma comment(lib, "ws2_32.lib") #define SERVER_IP "192.168.1.100" // 替换为你的 Linux 服务器实际 IP! #define SERVER_PORT 8080 // 服务器端口(与服务器一致) #define BUF_SIZE 1024 int main() { // 1. 初始化 Winsock 库(Windows 必须) WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { std::cerr << "Winsock 初始化失败,错误码:" << WSAGetLastError() << std::endl; return -1; } // 2. 创建 UDP 套接字(SOCK_DGRAM = UDP) SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd == INVALID_SOCKET) { std::cerr << "socket 创建失败,错误码:" << WSAGetLastError() << std::endl; WSACleanup(); return -1; } // 3. 填充服务器地址 struct sockaddr_in serv_addr; memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERVER_PORT); // IP 地址转换(Windows 与 Linux 用法一致) if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) { std::cerr << "IP 地址转换失败" << std::endl; closesocket(sockfd); WSACleanup(); return -1; } // 4. 向服务器发送 UDP 消息 const char* send_msg = "Hello Linux UDP Server! 我是 Windows 客户端~"; int send_len = sendto( sockfd, send_msg, (int)strlen(send_msg), 0, (struct sockaddr*)&serv_addr, sizeof(serv_addr) ); if (send_len == SOCKET_ERROR) { std::cerr << "sendto 发送失败,错误码:" << WSAGetLastError() << std::endl; closesocket(sockfd); WSACleanup(); return -1; } std::cout << "【发送】给 Linux 服务器:" << send_msg << std::endl; // 5. 接收服务器回复 char buffer[BUF_SIZE]; int recv_len = recvfrom( sockfd, buffer, BUF_SIZE, 0, NULL, NULL ); if (recv_len == SOCKET_ERROR) { std::cerr << "recvfrom 接收失败,错误码:" << WSAGetLastError() << std::endl; closesocket(sockfd); WSACleanup(); return -1; } buffer[recv_len] = '\0'; std::cout << "【收到】Linux 服务器回复:" << buffer << std::endl; // 6. 清理资源(Windows 必须) closesocket(sockfd); WSACleanup(); return 0; }3. 编译 & 运行步骤
1. 编译 Linux 服务器
# 进入服务器代码目录 g++ udp_server_linux.cpp -o udp_server # 运行服务器 ./udp_server2. 编译 Windows 客户端
方式 1:MinGW 编译(推荐)
# 进入客户端代码目录 g++ udp_client_windows.cpp -o udp_client.exe -lws2_32 # 运行客户端 udp_client.exe方式 2:Visual Studio 编译
- 新建「空项目」,添加
udp_client_windows.cpp- 直接编译运行(
#pragma comment(lib, "ws2_32.lib")会自动链接库)4. 关键注意事项(必看)
服务器 IP 配置
- Windows 客户端的
SERVER_IP必须填 Linux 服务器的实际局域网 IP(如192.168.1.100),不能用 127.0.0.1(除非 Linux 服务器在 WSL 中,且网络模式为桥接)。- 查看 Linux 服务器 IP:
ifconfig或ip addr(找eth0/wlan0的inet地址)。防火墙放行
- Linux 服务器:放行 UDP 8080 端口
bash
运行
sudo ufw allow 8080/udp # Ubuntu/Debian sudo firewall-cmd --add-port=8080/udp --permanent # CentOS/RHEL sudo firewall-cmd --reload- Windows 客户端:允许程序通过防火墙(首次运行时弹窗选择「允许」)。
跨平台 API 差异
功能 Linux Windows 头文件 <sys/socket.h><netinet/in.h><winsock2.h><ws2tcpip.h>套接字类型 intSOCKET关闭套接字 close()closesocket()库初始化 无需 WSAStartup()库清理 无需 WSACleanup()错误处理 perror()WSAGetLastError()5. 运行效果示例
Linux 服务器终端
UDP 服务器(Linux)已启动,监听端口 8080... 【收到】来自 192.168.1.105:54321 的消息:Hello Linux UDP Server! 我是 Windows 客户端~ 【回复】已发送给客户端Windows 客户端终端
【发送】给 Linux 服务器:Hello Linux UDP Server! 我是 Windows 客户端~ 【收到】Linux 服务器回复:Linux 服务器已收到你的 UDP 消息!
TCP-L-W--------------------------------------
1. TCP 服务器(Linux 端)
tcp_server_linux.cpp#include <iostream> #include <cstring> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define PORT 8080 // 监听端口 #define BUF_SIZE 1024 // 缓冲区大小 int main() { // 1. 创建 TCP 套接字(SOCK_STREAM = TCP) int listen_fd = socket(AF_INET, SOCK_STREAM, 0); if (listen_fd < 0) { perror("socket 创建失败"); return -1; } // 2. 设置套接字选项(避免端口占用后重启报错) int opt = 1; if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) { perror("setsockopt 失败"); close(listen_fd); return -1; } // 3. 绑定 IP + 端口 struct sockaddr_in serv_addr; memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(PORT); serv_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡 if (bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) { perror("bind 绑定失败"); close(listen_fd); return -1; } // 4. 开启监听(转为被动套接字) if (listen(listen_fd, 5) < 0) { // 监听队列长度 5 perror("listen 监听失败"); close(listen_fd); return -1; } std::cout << "TCP 服务器(Linux)已启动,监听端口 " << PORT << "..." << std::endl; while (true) { // 5. 阻塞等待客户端连接(创建新的通信套接字) struct sockaddr_in cli_addr; socklen_t cli_len = sizeof(cli_addr); int conn_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, &cli_len); if (conn_fd < 0) { perror("accept 接受连接失败"); continue; } // 打印客户端信息 std::cout << "\n【新连接】客户端 " << inet_ntoa(cli_addr.sin_addr) << ":" << ntohs(cli_addr.sin_port) << " 已连接" << std::endl; // 6. 与客户端通信(循环接收/发送数据) char buffer[BUF_SIZE]; while (true) { // 清空缓冲区 memset(buffer, 0, BUF_SIZE); // 接收客户端数据(阻塞) ssize_t recv_len = read(conn_fd, buffer, BUF_SIZE); // 客户端断开连接(recv_len = 0)或接收失败 if (recv_len <= 0) { if (recv_len == 0) { std::cout << "【断开连接】客户端 " << inet_ntoa(cli_addr.sin_addr) << ":" << ntohs(cli_addr.sin_port) << " 主动断开" << std::endl; } else { perror("read 接收数据失败"); } close(conn_fd); // 关闭通信套接字 break; } // 打印客户端消息 std::cout << "【收到】客户端消息:" << buffer << std::endl; // 回复客户端 std::string reply = "Linux 服务器已收到:" + std::string(buffer); write(conn_fd, reply.c_str(), reply.length()); std::cout << "【回复】已发送:" << reply << std::endl; } } close(listen_fd); // 理论上服务器不会走到这里 return 0; }2. TCP 客户端(Windows 端)
tcp_client_windows.cpp#include <iostream> #include <cstring> // Windows 专用 Winsock 头文件 #include <winsock2.h> #include <ws2tcpip.h> // 链接 Winsock 库(VS 自动识别,MinGW 编译需加 -lws2_32) #pragma comment(lib, "ws2_32.lib") #define SERVER_IP "192.168.1.100" // 替换为你的 Linux 服务器实际 IP! #define SERVER_PORT 8080 // 与服务器端口一致 #define BUF_SIZE 1024 int main() { // 1. 初始化 Winsock 2.2 库(Windows 必须) WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { std::cerr << "Winsock 初始化失败,错误码:" << WSAGetLastError() << std::endl; return -1; } // 2. 创建 TCP 套接字(SOCK_STREAM = TCP) SOCKET sock_fd = socket(AF_INET, SOCK_STREAM, 0); if (sock_fd == INVALID_SOCKET) { std::cerr << "socket 创建失败,错误码:" << WSAGetLastError() << std::endl; WSACleanup(); return -1; } // 3. 填充服务器地址信息 struct sockaddr_in serv_addr; memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERVER_PORT); // 转换服务器 IP 为网络字节序 if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) { std::cerr << "IP 地址转换失败(请检查服务器 IP 是否正确)" << std::endl; closesocket(sock_fd); WSACleanup(); return -1; } // 4. 连接 Linux 服务器(TCP 三次握手) if (connect(sock_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR) { std::cerr << "连接服务器失败,错误码:" << WSAGetLastError() << std::endl; closesocket(sock_fd); WSACleanup(); return -1; } std::cout << "已成功连接到 Linux TCP 服务器(" << SERVER_IP << ":" << SERVER_PORT << ")" << std::endl; // 5. 与服务器通信(发送消息 + 接收回复) char send_buf[BUF_SIZE], recv_buf[BUF_SIZE]; while (true) { // 输入要发送的消息 std::cout << "\n请输入要发送的消息(输入 exit 退出):"; std::cin.getline(send_buf, BUF_SIZE); // 退出条件 if (strcmp(send_buf, "exit") == 0) { std::cout << "主动断开与服务器的连接" << std::endl; break; } // 发送消息到服务器 int send_len = send(sock_fd, send_buf, (int)strlen(send_buf), 0); if (send_len == SOCKET_ERROR) { std::cerr << "send 发送失败,错误码:" << WSAGetLastError() << std::endl; break; } std::cout << "【发送】已发送:" << send_buf << std::endl; // 接收服务器回复 memset(recv_buf, 0, BUF_SIZE); int recv_len = recv(sock_fd, recv_buf, BUF_SIZE, 0); if (recv_len == SOCKET_ERROR) { std::cerr << "recv 接收失败,错误码:" << WSAGetLastError() << std::endl; break; } else if (recv_len == 0) { std::cerr << "服务器已断开连接" << std::endl; break; } std::cout << "【收到】服务器回复:" << recv_buf << std::endl; } // 6. 清理资源(Windows 必须) closesocket(sock_fd); WSACleanup(); return 0; }3. 编译 & 运行步骤
1. 编译 Linux 服务器
# 进入服务器代码目录 g++ tcp_server_linux.cpp -o tcp_server # 运行服务器(后台运行可加 &:./tcp_server &) ./tcp_server2. 编译 Windows 客户端
方式 1:MinGW 编译(推荐,轻量)
# 进入客户端代码目录 g++ tcp_client_windows.cpp -o tcp_client.exe -lws2_32 # 运行客户端 tcp_client.exe方式 2:Visual Studio 编译
- 新建「空项目」,将
tcp_client_windows.cpp添加到项目;- 直接点击「运行」(
#pragma comment(lib, "ws2_32.lib")会自动链接 Winsock 库)。4. 关键注意事项(必看)
1. 服务器 IP 配置
- Windows 客户端的
SERVER_IP必须替换为 Linux 服务器的实际局域网 IP(如192.168.1.100),绝对不能用 127.0.0.1(仅本机回环,跨机器无法通信);- 查看 Linux 服务器 IP:执行
ifconfig或ip addr,找到eth0(有线)/wlan0(无线)的inet字段。2. 防火墙放行
- Linux 服务器:放行 TCP 8080 端口
# Ubuntu/Debian 系统 sudo ufw allow 8080/tcp # CentOS/RHEL 系统 sudo firewall-cmd --add-port=8080/tcp --permanent sudo firewall-cmd --reload- Windows 客户端:首次运行客户端时,防火墙弹窗选择「允许访问」(专用网络 + 公用网络)。
3. 跨平台 API 核心差异
功能 Linux 系统 Windows 系统 核心头文件 <sys/socket.h>等<winsock2.h><ws2tcpip.h>套接字类型 intSOCKET(unsigned int)无效套接字标识 -1INVALID_SOCKET关闭套接字 close()closesocket()库初始化 / 清理 无需 WSAStartup()/WSACleanup()错误码获取 perror()/errnoWSAGetLastError()数据收发 read()/write()recv()/send()5. 运行效果示例
Linux 服务器终端
TCP 服务器(Linux)已启动,监听端口 8080... 【新连接】客户端 192.168.1.105:54321 已连接 【收到】客户端消息:Hello Linux TCP Server! 【回复】已发送:Linux 服务器已收到:Hello Linux TCP Server! 【收到】客户端消息:This is Windows Client 【回复】已发送:Linux 服务器已收到:This is Windows Client 【断开连接】客户端 192.168.1.105:54321 主动断开Windows 客户端终端
已成功连接到 Linux TCP 服务器(192.168.1.100:8080) 请输入要发送的消息(输入 exit 退出):Hello Linux TCP Server! 【发送】已发送:Hello Linux TCP Server! 【收到】服务器回复:Linux 服务器已收到:Hello Linux TCP Server! 请输入要发送的消息(输入 exit 退出):This is Windows Client 【发送】已发送:This is Windows Client 【收到】服务器回复:Linux 服务器已收到:This is Windows Client 请输入要发送的消息(输入 exit 退出):exit 主动断开与服务器的连接
第二种:Linux服务器+Linux客户端
UDP-L-L------------------------------------
一、UDP 服务器(Linux 端)
服务端是要写的代码主要是:
初始化阶段:
1.创建一个套接字
2.为服务器定义并且配置地址结构体, 必须转成网络序, 这是协议的规定, 配置内容是主机的某个网卡的IP/所有网卡的IP, 某一个端口;通常我们会选择监听所有的网卡的IP: INADDR_ANY
3.绑定套接字和其监听的地址结构体
业务逻辑代码:
1.持续获取接受来自外部网络的发送过来的数据(包含了外部网络的地址结构体)
2.处理数据, (利用外部网络的地址结构体)向外部网络回复反馈
udp_server_linux.cp#include <iostream> #include <cstring> #include <unistd.h> #include <sys/socket.h>// 套接字核心头文件:提供 socket()、bind()、recvfrom()、sendto() 等网络函数 #include <netinet/in.h>// 网络地址结构体头文件:定义 sockaddr_in(IPv4 地址结构体) #include <arpa/inet.h>// IP 地址转换函数:提供 inet_ntoa()(将网络字节序 IP 转字符串) #define PORT 8080 // 服务器监听的端口号(自定义,建议 1024 以上避免系统端口) #define BUF_SIZE 1024 // 数据缓冲区大小:单次最多接收/发送 1024 字节数据 int main() { // ===================== 步骤1:创建 UDP 套接字(socket 描述符) ===================== // socket() 函数作用:创建一个用于网络通信的"文件描述符"(Linux 一切皆文件) // 参数说明: // AF_INET:地址族,指定使用 IPv4 协议 // SOCK_DGRAM:套接字类型,DGRAM = Datagram(数据报),表示 UDP 协议(无连接、不可靠) // 0:协议编号,0 表示根据前两个参数自动匹配(UDP 对应 IPPROTO_UDP) int sock_fd = socket(AF_INET, SOCK_DGRAM, 0); if (sock_fd < 0) { perror("socket 创建失败"); return -1;// 程序异常退出 } // ===================== 步骤2:设置套接字选项(端口复用) ===================== // 问题背景:服务器重启时,若端口还处于 TIME_WAIT 状态,直接绑定会报错 "Address already in use" // 解决方案:设置 SO_REUSEADDR + SO_REUSEPORT 选项,允许端口复用 int opt = 1; // 选项值:1 表示启用该选项 // setsockopt() 函数:设置套接字的属性 // 参数说明: // sock_fd:要设置的套接字描述符 // SOL_SOCKET:级别,SOL_SOCKET 表示操作套接字本身的通用选项 // SO_REUSEADDR | SO_REUSEPORT:要启用的选项(按位或表示同时启用) // &opt:选项值的地址(输入参数) // sizeof(opt):选项值的长度 if (setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) { perror("setsockopt 失败"); close(sock_fd); // 失败时必须关闭已创建的套接字,避免资源泄漏 return -1; } // ===================== 步骤3:填充服务器地址结构体 ===================== // 定义两个地址结构体: // serv_addr:服务器自身的地址(用于绑定) struct sockaddr_in serv_addr; // cli_addr:客户端的地址(接收数据时自动填充) struct sockaddr_in cli_addr; // cli_len:客户端地址结构体的长度(recvfrom 的输入输出参数,必须初始化) socklen_t cli_len = sizeof(cli_addr); // memset:将 serv_addr 内存全部置 0,避免脏数据影响 memset(&serv_addr, 0, sizeof(serv_addr)); // 填充 serv_addr 字段(IPv4 地址结构体固定格式) serv_addr.sin_family = AF_INET; // 地址族:必须和 socket() 的 AF_INET 一致(IPv4) // htons():Host to Network Short(主机字节序转网络字节序,短整型) // 原因:不同系统主机字节序可能不同(大端/小端),网络协议强制使用大端序,必须转换 serv_addr.sin_port = htons(PORT); // INADDR_ANY:表示监听本机所有网卡的 IP(如 127.0.0.1、内网 IP、公网 IP) // 等价于 inet_addr("0.0.0.0"),无需手动指定具体 IP serv_addr.sin_addr.s_addr = INADDR_ANY; // ===================== 步骤4:绑定套接字到 IP + 端口 ===================== // bind() 函数:将套接字与指定的 IP 地址和端口绑定(UDP 服务器必须绑定,客户端无需) // 参数说明: // sock_fd:套接字描述符 // (struct sockaddr*)&serv_addr:通用地址结构体(强制类型转换,兼容 IPv4/IPv6) // sizeof(serv_addr):地址结构体长度 if (bind(sock_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) { perror("bind 绑定失败"); close(sock_fd); // 释放资源 return -1; } // 打印启动信息,告知用户服务器已就绪 std::cout << "=== UDP 服务器(Linux)已启动 ===" << std::endl; std::cout << "监听端口:" << PORT << ",等待客户端消息..." << std::endl; // ===================== 步骤5:循环接收客户端数据并回复 ===================== char buffer[BUF_SIZE]; // 定义数据缓冲区,用于存储接收的客户端数据 while (true) { // 无限循环:服务器持续运行,直到手动终止(Ctrl+C) memset(buffer, 0, BUF_SIZE); // 每次接收前清空缓冲区,避免残留上一次的数据 // recvfrom():UDP 专用接收函数(阻塞式,直到收到数据才返回) // 核心特点:无需提前建立连接,接收数据时,自动获取客户端的 IP + 端口 ssize_t recv_len = recvfrom( sock_fd, // 套接字描述符 buffer, // 接收数据的缓冲区(输出) BUF_SIZE, // 缓冲区最大长度(避免溢出) 0, // 标志位(0 表示无特殊行为) (struct sockaddr*)&cli_addr, // 输出参数,存储发送方(客户端)的地址信息 &cli_len // 输入输出参数,输入时是地址结构体长度,输出时是实际长度 ); // 错误处理:接收失败时打印错误,继续循环(不退出服务器) if (recv_len < 0) { perror("recvfrom 接收数据失败"); continue; } // 打印客户端信息 + 收到的消息 std::cout << "\n【收到】来自 " << inet_ntoa(cli_addr.sin_addr) // inet_ntoa:将网络字节序的 IP 转字符串(如 192.168.1.1) << ":" << ntohs(cli_addr.sin_port) // ntohs:Network to Host Short(网络字节序转主机字节序) << " 的消息:" << buffer << std::endl; // 构造回复消息:拼接客户端发送的内容,告知已收到 std::string reply = "服务器已收到:" + std::string(buffer); // sendto():UDP 专用发送函数(无连接,需指定目标地址) sendto( sock_fd, // 套接字描述符 reply.c_str(), // 要发送的字符串(转 C 风格字符串) reply.length(), // 发送数据的长度 0, // 标志位 (struct sockaddr*)&cli_addr, // 目标地址(客户端地址) cli_len // 目标地址长度 ); std::cout << "【回复】已发送:" << reply << std::endl; } // ===================== 步骤6:释放资源(理论上不会执行) ===================== // 因为服务器是无限循环,只有手动终止(Ctrl+C)才会退出,所以这里代码不会执行 close(sock_fd); // 关闭套接字,释放文件描述符资源 return 0; }
sendto()发送字符串时不需要转换字节序,本质是因为字节序问题只存在于多字节数值类型(如int/short),而字符串是单字节序列,不存在字节序差异。
数值类型主机序(小端)内存存储网络序(大端)内存存储转换函数8888(端口)16 位0x38 0x220x22 0x38htons()1024(整数)32 位0x00 0x04 0x00 0x000x00 0x00 0x04 0x00htonl()二、UDP 客户端(Linux 端)
代码功能:
初始化逻辑:
1. 创建套接字2. 为服务器创建地址结构体, 并且填充目标服务器的ip和端口
业务逻辑:
udp_client_linux.cpp#include <iostream> #include <cstring> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define SERVER_IP "127.0.0.1" // 服务器 IP 地址:127.0.0.1 是本机回环地址(仅本机测试用),跨机器测试需改为服务器实际 IP #define SERVER_PORT 8080 // 服务器监听端口:必须与 UDP 服务器的 PORT 保持一致,否则无法通信 #define BUF_SIZE 1024 // 数据缓冲区大小:单次最多发送/接收 1024 字节数据 int main() { // ===================== 步骤1:创建 UDP 套接字(socket 描述符) ===================== // socket() 函数:创建用于 UDP 通信的套接字描述符 // 参数说明: // AF_INET:地址族,指定使用 IPv4 协议(与服务器保持一致) // SOCK_DGRAM:套接字类型,DGRAM = Datagram(数据报),表示 UDP 协议(无连接、不可靠) // 0:协议编号,0 表示根据前两个参数自动匹配 UDP 协议(等价于 IPPROTO_UDP) int sock_fd = socket(AF_INET, SOCK_DGRAM, 0); if (sock_fd < 0) { perror("socket 创建失败"); return -1; } // ===================== 步骤2:填充服务器地址结构体 ===================== // 定义服务器地址结构体:用于指定消息发送的目标(服务器)地址 struct sockaddr_in serv_addr; // memset:将 serv_addr 内存全部置 0,避免未初始化的脏数据影响通信 memset(&serv_addr, 0, sizeof(serv_addr)); // 填充服务器地址结构体的核心字段(IPv4 固定格式) serv_addr.sin_family = AF_INET; // 地址族:必须为 AF_INET(IPv4),与服务器一致 serv_addr.sin_port = htons(SERVER_PORT); // 服务器端口:转换为网络字节序(大端),UDP 协议要求 // inet_pton():将字符串格式的 IP 地址(如 "127.0.0.1")转换为网络字节序的二进制格式 // 参数说明: // AF_INET:地址族(IPv4) // SERVER_IP:待转换的字符串 IP // &serv_addr.sin_addr:输出参数,存储转换后的二进制 IP // 返回值:成功返回 1;无效 IP 返回 0;失败返回 -1 if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) { perror("IP 地址转换失败(请检查服务器 IP 是否正确)"); close(sock_fd); // 失败时关闭已创建的套接字,避免资源泄漏 return -1; } // 打印客户端启动信息,告知用户服务器地址和端口 std::cout << "=== UDP 客户端(Linux)已启动 ===" << std::endl; std::cout << "服务器地址:" << SERVER_IP << ":" << SERVER_PORT << std::endl; // ===================== 步骤3:循环发送消息到服务器并接收回复 ===================== // 定义两个缓冲区:send_buf 存储用户输入的待发送消息,recv_buf 存储服务器的回复 char send_buf[BUF_SIZE], recv_buf[BUF_SIZE]; // 无限循环:直到用户输入 "exit" 才退出 while (true) { // 提示用户输入要发送的消息 std::cout << "\n请输入要发送的消息(输入 exit 退出):"; // cin.getline():读取用户输入的一行字符串(包含空格),存入 send_buf,最多读取 BUF_SIZE 字节 // 区别于 cin >> send_buf:cin >> 会以空格/换行为分隔符,getline 能读取整行 std::cin.getline(send_buf, BUF_SIZE); // 退出逻辑:若用户输入 "exit",则终止循环并退出客户端 // strcmp():比较两个字符串,相等返回 0,不等返回非 0 if (strcmp(send_buf, "exit") == 0) { std::cout << "客户端退出" << std::endl; break; // 跳出 while 循环,执行后续的资源释放逻辑 } // ===================== 发送消息到服务器 ===================== // sendto():UDP 专用发送函数(无连接,需明确指定目标服务器地址) ssize_t send_len = sendto( sock_fd, // 套接字描述符 send_buf, // 待发送的消息缓冲区 strlen(send_buf), // 发送数据的实际长度(strlen 不计算字符串末尾的 '\0') 0, // 标志位 (struct sockaddr*)&serv_addr, // 目标:目标地址(服务器的 IPv4 地址结构体,强制类型转换) sizeof(serv_addr) // 目标地址结构体的长度 ); // 错误处理:发送失败时打印错误,跳出循环(退出客户端) if (send_len < 0) { perror("sendto 发送数据失败"); break; } // 打印发送成功的日志,确认消息已发出 std::cout << "【发送】已发送:" << send_buf << std::endl; // ===================== 接收服务器回复 ===================== // 接收前清空回复缓冲区,避免残留上一次的回复数据 memset(recv_buf, 0, BUF_SIZE); // recvfrom():UDP 专用接收函数(阻塞式,直到收到服务器回复才返回) ssize_t recv_len = recvfrom( sock_fd, // 套接字描述符 recv_buf, // 存储服务器回复的缓冲区 BUF_SIZE, // 缓冲区最大长度(避免溢出) 0, // 标志位,0 表示阻塞接收 NULL, // 无需获取发送方(服务器)的地址,填 NULL 即可(客户端已知服务器地址) NULL // 地址长度参数也填 NULL(与上一个参数对应) ); // 错误处理:接收回复失败时打印错误,跳出循环(退出客户端) if (recv_len < 0) { perror("recvfrom 接收回复失败"); break; } // 打印服务器的回复,告知用户 std::cout << "【收到】服务器回复:" << recv_buf << std::endl; } // ===================== 步骤4:释放资源 ===================== // 关闭套接字描述符,释放系统分配的网络资源(即使异常退出也会执行) close(sock_fd); return 0; // 程序正常退出 }由于客户端有很多个, 且是动态的, 系统在客户端发送信息时会自动附加客户端的动态IP和端口;
不建议你为客户端绑死一个IP和端口, 这样可能会导致冲突, 因为对于同设备同IP, 不同应用的客户端必须使用不同端口, 系统会自动分配空闲的端口;服务端必须要固定端口和IP, 并且客户端会直接内置服务端的IP和端口号, 不能随便改变, 否则客户端无法正确访问服务端;
你想知道为什么 UDP 客户端调用
sendto()时不用显式发送自己的 IP / 端口,而服务端却能准确知道客户端地址 —— 核心原因是:UDP 数据包在网络传输时,底层协议栈(操作系统内核)会自动填充客户端的源 IP 和源端口,服务端通过recvfrom()的参数就能从数据包中提取这些信息,无需客户端手动传递。UDP 客户端 默认完全不需要手动
bind()—— 操作系统会在你第一次调用sendto()时,自动帮你完成「本地 IP + 端口」的绑定,这是 UDP 无连接特性的核心设计之一。
bind()的本质是「显式指定套接字关联的本地 IP + 端口」,而 UDP 客户端的核心诉求是「给服务器发消息」,不是「让别人找自己」,所以操作系统会替你做 3 件事:表格
操作时机 操作系统自动行为 咱们聊天客户端的例子 首次调用 sendto()时1. 选本地 IP:根据路由表选「能到达服务器的出口网卡 IP」(比如连本地服务器选 127.0.0.1,连公网服务器选网卡公网 IP)2. 选本地端口:从系统临时端口范围(如 32768-61000)随机选一个未被占用的端口3. 隐式绑定:把这个 IP + 端口和客户端套接字绑定 你客户端代码里 InitClient只调用了socket(),没bind(),但第一次执行sendto(服务器IP:8080)时,系统会自动给客户端绑定「127.0.0.1 + 随机端口(比如 54321)」后续收发 客户端所有的 recvfrom()都会监听这个「自动绑定的 IP + 端口」,服务器转发的消息也会精准发到这个地址服务器广播消息时,目标地址就是客户端这个随机端口,所以客户端 recver线程能收到你客户端的
InitClient函数只有一行创建 socket 的代码:void InitClient(const std::string &serverip, uint16_t serverport) { sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 只创建socket,无bind if (sockfd < 0) { /* 错误处理 */ } }但你执行
sendto(sockfd, line.c_str(), ..., 服务器地址)时,系统会:
- 自动给
sockfd绑定「IP(比如192.168.1.100)+ 随机端口(比如 56789)」;- 把消息从这个 IP + 端口发给服务器的 8080 端口;
- 服务器收到后,能从
recvfrom的peer参数拿到这个「192.168.1.100:56789」,所以广播消息时能精准发回给客户端。三、编译 & 运行步骤
1. 编译代码
打开 Linux 终端,分别编译服务器和客户端:
# 编译服务器 g++ udp_server_linux.cpp -o udp_server # 编译客户端 g++ udp_client_linux.cpp -o udp_client2. 运行程序
第一步:启动服务器(先运行)
./udp_server服务器启动后输出:
=== UDP 服务器(Linux)已启动 === 监听端口:8080,等待客户端消息...第二步:启动客户端(新开一个终端)
./udp_client客户端启动后输出:
=== UDP 客户端(Linux)已启动 === 服务器地址:127.0.0.1:8080四、关键注意事项
1. 跨机器通信配置
如果服务器和客户端不在同一台 Linux 机器,需要:
- 修改客户端代码中的
SERVER_IP为服务器的实际局域网 IP(如192.168.1.100);- 服务器放行 8080 端口(UDP):
# Ubuntu/Debian 系统 sudo ufw allow 8080/udp # CentOS/RHEL 系统 sudo firewall-cmd --add-port=8080/udp --permanent sudo firewall-cmd --reload2. 端口占用问题
如果启动服务器时报
bind 绑定失败: Address already in use:
- 查看占用 8080 端口的进程:
lsof -i :8080;- 杀死该进程:
kill -9 进程ID;- 或修改代码中的
PORT为其他未占用端口(如 8081)。3. UDP 核心特点(与 TCP 对比)
特性 UDP TCP 连接性 无连接(无需建立 / 断开连接) 面向连接(三次握手 / 四次挥手) 可靠性 不可靠(数据可能丢失 / 乱序) 可靠(重传、确认、有序) 核心 API recvfrom()/sendto()read()/write()/accept()服务器绑定 必须 bind()必须 bind()客户端绑定 无需(系统自动分配临时端口) 无需(系统自动分配临时端口) 五、运行效果示例
服务器终端输出
=== UDP 服务器(Linux)已启动 === 监听端口:8080,等待客户端消息... 【收到】来自 127.0.0.1:54321 的消息:Hello UDP Server! 【回复】已发送:服务器已收到:Hello UDP Server! 【收到】来自 127.0.0.1:54321 的消息:This is Linux Client 【回复】已发送:服务器已收到:This is Linux Client客户端终端输出
=== UDP 客户端(Linux)已启动 === 服务器地址:127.0.0.1:8080 请输入要发送的消息(输入 exit 退出):Hello UDP Server! 【发送】已发送:Hello UDP Server! 【收到】服务器回复:服务器已收到:Hello UDP Server! 请输入要发送的消息(输入 exit 退出):This is Linux Client 【发送】已发送:This is Linux Client 【收到】服务器回复:服务器已收到:This is Linux Client 请输入要发送的消息(输入 exit 退出):exit 客户端退出
封装后的代码(UDPServer 类)
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdexcept> // 用于异常处理
// 定义 UDP 服务器类:封装所有属性和行为
class UDPServer {
public:
// 构造函数:初始化端口和缓冲区大小(提供默认值,增强灵活性)
// 入参:port - 监听端口,buf_size - 数据缓冲区大小
explicit UDPServer(int port = 8080, int buf_size = 1024)
: port_(port), buf_size_(buf_size), sock_fd_(-1) {
// 初始化服务器地址结构体(清零)
memset(&serv_addr_, 0, sizeof(serv_addr_));
}
// 析构函数:自动释放资源(核心优势:无需手动 close,防止泄漏)
~UDPServer() {
closeServer();
std::cout << "=== UDP 服务器资源已释放 ===" << std::endl;
}
// 禁用拷贝构造和赋值运算符(避免套接字描述符重复释放)
UDPServer(const UDPServer&) = delete;
UDPServer& operator=(const UDPServer&) = delete;
// 初始化服务器:创建套接字 + 设置端口复用 + 绑定端口
// 返回值:true-成功,false-失败
bool init() {
// 1. 创建 UDP 套接字
sock_fd_ = socket(AF_INET, SOCK_DGRAM, 0);
if (sock_fd_ < 0) {
std::cerr << "错误:创建套接字失败 - " << strerror(errno) << std::endl;
return false;
}
// 2. 设置端口复用选项
int opt = 1;
if (setsockopt(sock_fd_, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT,
&opt, sizeof(opt)) < 0) {
std::cerr << "错误:设置套接字选项失败 - " << strerror(errno) << std::endl;
closeServer();
return false;
}
// 3. 填充服务器地址结构体
serv_addr_.sin_family = AF_INET; // IPv4 协议族
serv_addr_.sin_port = htons(port_); // 端口转网络字节序
serv_addr_.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡
// 4. 绑定套接字到端口
if (bind(sock_fd_, (struct sockaddr*)&serv_addr_, sizeof(serv_addr_)) < 0) {
std::cerr << "错误:绑定端口失败 - " << strerror(errno) << std::endl;
closeServer();
return false;
}
std::cout << "=== UDP 服务器初始化成功 ===" << std::endl;
std::cout << "监听端口:" << port_ << ",缓冲区大小:" << buf_size_ << "字节" << std::endl;
return true;
}
// 运行服务器:循环接收客户端数据并回复(阻塞式)
void run() {
if (sock_fd_ < 0) {
std::cerr << "错误:服务器未初始化,请先调用 init()!" << std::endl;
return;
}
std::cout << "\n=== UDP 服务器已启动,等待客户端消息 ===" << std::endl;
char* buffer = new char[buf_size_]; // 动态分配缓冲区(避免栈溢出)
while (true) {
// 清空缓冲区
memset(buffer, 0, buf_size_);
// 客户端地址信息(接收数据时自动填充)
struct sockaddr_in cli_addr;
socklen_t cli_len = sizeof(cli_addr);
// 接收客户端数据
ssize_t recv_len = recvfrom(
sock_fd_, buffer, buf_size_, 0,
(struct sockaddr*)&cli_addr, &cli_len
);
// 接收失败:打印错误,继续循环
if (recv_len < 0) {
std::cerr << "错误:接收数据失败 - " << strerror(errno) << std::endl;
continue;
}
// 解析客户端信息
std::string client_ip = inet_ntoa(cli_addr.sin_addr);
int client_port = ntohs(cli_addr.sin_port);
std::string recv_msg(buffer, recv_len); // 转换为 C++ 字符串
// 打印接收信息
std::cout << "\n【收到】来自 " << client_ip << ":" << client_port
<< " 的消息:" << recv_msg << std::endl;
// 构造回复消息并发送
std::string reply_msg = "服务器已收到:" + recv_msg;
sendTo(reply_msg, client_ip, client_port);
std::cout << "【回复】已发送:" << reply_msg << std::endl;
}
// 释放缓冲区(理论上不会执行,因无限循环)
delete[] buffer;
}
// 主动发送数据给指定客户端(对外暴露的接口,可单独调用)
// 入参:msg-发送内容,client_ip-客户端IP,client_port-客户端端口
// 返回值:发送的字节数(-1 表示失败)
ssize_t sendTo(const std::string& msg, const std::string& client_ip, int client_port) {
if (sock_fd_ < 0) {
std::cerr << "错误:套接字未初始化!" << std::endl;
return -1;
}
// 填充客户端地址结构体
struct sockaddr_in cli_addr;
memset(&cli_addr, 0, sizeof(cli_addr));
cli_addr.sin_family = AF_INET;
cli_addr.sin_port = htons(client_port);
// 转换客户端 IP 为网络字节序
if (inet_pton(AF_INET, client_ip.c_str(), &cli_addr.sin_addr) <= 0) {
std::cerr << "错误:客户端 IP 格式错误 - " << client_ip << std::endl;
return -1;
}
// 发送数据
ssize_t send_len = sendto(
sock_fd_, msg.c_str(), msg.length(), 0,
(struct sockaddr*)&cli_addr, sizeof(cli_addr)
);
if (send_len < 0) {
std::cerr << "错误:发送数据失败 - " << strerror(errno) << std::endl;
}
return send_len;
}
// 关闭服务器:释放套接字资源
void closeServer() {
if (sock_fd_ >= 0) {
close(sock_fd_);
sock_fd_ = -1; // 标记为已关闭,防止重复关闭
}
}
private:
// 私有成员变量:对外隐藏,仅类内部访问(封装核心)
int sock_fd_; // 套接字描述符(核心句柄)
int port_; // 监听端口
int buf_size_; // 数据缓冲区大小
struct sockaddr_in serv_addr_; // 服务器地址结构体
};
// 测试主函数:演示如何使用 UDPServer 类
int main() {
try {
// 1. 创建 UDP 服务器对象(指定端口 8080,缓冲区 1024)
UDPServer server(8080, 1024);
// 2. 初始化服务器
if (!server.init()) {
std::cerr << "服务器初始化失败,程序退出!" << std::endl;
return -1;
}
// 3. 运行服务器(阻塞式,直到手动 Ctrl+C 终止)
server.run();
} catch (const std::exception& e) {
// 异常捕获:增强程序健壮性
std::cerr << "服务器运行异常:" << e.what() << std::endl;
return -1;
}
return 0;
}
封装后的代码(UDPClient 类)
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cerrno> // 用于获取错误码描述
#include <stdexcept>
// UDP 客户端类:封装所有属性和行为,对外暴露简洁接口
class UDPClient {
public:
// 构造函数:初始化服务器地址、端口、缓冲区大小(提供默认值,增强灵活性)
// 入参:server_ip - 服务器IP,server_port - 服务器端口,buf_size - 缓冲区大小
UDPClient(const std::string& server_ip = "127.0.0.1",
int server_port = 8080,
int buf_size = 1024)
: server_ip_(server_ip),
server_port_(server_port),
buf_size_(buf_size),
sock_fd_(-1) {
// 初始化服务器地址结构体(清零,避免脏数据)
memset(&serv_addr_, 0, sizeof(serv_addr_));
}
// 析构函数:自动释放套接字资源(RAII 核心,防止资源泄漏)
~UDPClient() {
closeClient();
std::cout << "=== UDP 客户端资源已释放 ===" << std::endl;
}
// 禁用拷贝构造和赋值运算符(避免套接字描述符重复释放/操作)
UDPClient(const UDPClient&) = delete;
UDPClient& operator=(const UDPClient&) = delete;
// 初始化客户端:创建套接字 + 填充服务器地址结构体
// 返回值:true-成功,false-失败
bool init() {
// 1. 创建 UDP 套接字
sock_fd_ = socket(AF_INET, SOCK_DGRAM, 0);
if (sock_fd_ < 0) {
std::cerr << "错误:创建套接字失败 - " << strerror(errno) << std::endl;
return false;
}
// 2. 填充服务器地址结构体
serv_addr_.sin_family = AF_INET; // IPv4 协议族
serv_addr_.sin_port = htons(server_port_); // 端口转网络字节序
// 转换服务器 IP 为网络字节序(替代原 inet_pton,增加错误提示)
if (inet_pton(AF_INET, server_ip_.c_str(), &serv_addr_.sin_addr) <= 0) {
std::cerr << "错误:服务器 IP 格式错误(" << server_ip_ << ")- "
<< strerror(errno) << std::endl;
closeClient();
return false;
}
std::cout << "=== UDP 客户端初始化成功 ===" << std::endl;
std::cout << "服务器地址:" << server_ip_ << ":" << server_port_ << std::endl;
return true;
}
// 运行客户端:循环读取用户输入 → 发送消息 → 接收回复(阻塞式)
void run() {
if (sock_fd_ < 0) {
std::cerr << "错误:客户端未初始化,请先调用 init()!" << std::endl;
return;
}
std::cout << "\n=== UDP 客户端已启动(输入 exit 退出)===" << std::endl;
std::string input_msg; // 用 C++ string 替代字符数组,更安全
while (true) {
// 提示用户输入消息
std::cout << "\n请输入要发送的消息:";
std::getline(std::cin, input_msg); // 读取整行输入(支持空格)
// 退出逻辑:输入 exit 终止循环
if (input_msg == "exit") {
std::cout << "客户端准备退出..." << std::endl;
break;
}
// 空消息过滤:避免发送空字符串
if (input_msg.empty()) {
std::cout << "提示:消息不能为空,请重新输入!" << std::endl;
continue;
}
// 1. 发送消息到服务器
ssize_t send_len = sendMessage(input_msg);
if (send_len < 0) {
std::cerr << "发送失败,退出客户端..." << std::endl;
break;
}
std::cout << "【发送】已发送:" << input_msg << std::endl;
// 2. 接收服务器回复
std::string reply_msg = receiveReply();
if (reply_msg.empty()) {
std::cerr << "接收回复失败,退出客户端..." << std::endl;
break;
}
std::cout << "【收到】服务器回复:" << reply_msg << std::endl;
}
}
// 发送消息到服务器(对外暴露的独立接口,可单独调用)
// 入参:msg - 要发送的字符串
// 返回值:发送的字节数(-1 表示失败)
ssize_t sendMessage(const std::string& msg) {
if (sock_fd_ < 0) {
std::cerr << "错误:套接字未初始化!" << std::endl;
return -1;
}
ssize_t send_len = sendto(
sock_fd_, // 套接字描述符
msg.c_str(), // 待发送数据(C风格字符串)
msg.length(), // 数据长度(不含 '\0')
0, // 标志位
(struct sockaddr*)&serv_addr_, // 目标服务器地址
sizeof(serv_addr_) // 地址结构体长度
);
if (send_len < 0) {
std::cerr << "错误:发送消息失败 - " << strerror(errno) << std::endl;
}
return send_len;
}
// 接收服务器回复(对外暴露的独立接口,可单独调用)
// 返回值:服务器回复的字符串(空字符串表示失败)
std::string receiveReply() {
if (sock_fd_ < 0) {
std::cerr << "错误:套接字未初始化!" << std::endl;
return "";
}
// 动态分配缓冲区(避免栈溢出,适配自定义 buf_size_)
char* recv_buf = new char[buf_size_];
memset(recv_buf, 0, buf_size_); // 清空缓冲区
// 接收回复(无需获取服务器地址,填 NULL)
ssize_t recv_len = recvfrom(
sock_fd_, // 套接字描述符
recv_buf, // 接收缓冲区
buf_size_, // 缓冲区大小
0, // 阻塞接收
NULL, // 不获取发送方地址
NULL // 地址长度参数置空
);
std::string reply;
if (recv_len > 0) {
reply = std::string(recv_buf, recv_len); // 转换为 C++ string
} else if (recv_len < 0) {
std::cerr << "错误:接收回复失败 - " << strerror(errno) << std::endl;
}
delete[] recv_buf; // 释放缓冲区
return reply;
}
// 关闭客户端:释放套接字资源
void closeClient() {
if (sock_fd_ >= 0) {
close(sock_fd_);
sock_fd_ = -1; // 标记为已关闭,防止重复关闭
}
}
private:
// 私有成员变量:对外隐藏,仅类内部访问(封装核心)
int sock_fd_; // 套接字描述符(核心句柄)
std::string server_ip_; // 服务器 IP 地址(C++ string 更安全)
int server_port_; // 服务器端口
int buf_size_; // 数据缓冲区大小
struct sockaddr_in serv_addr_; // 服务器地址结构体
};
// 测试主函数:演示 UDPClient 类的使用
int main() {
try {
// 1. 创建 UDP 客户端对象(指定服务器 IP、端口、缓冲区)
// 跨机器测试时,将 "127.0.0.1" 改为服务器实际 IP
UDPClient client("127.0.0.1", 8080, 1024);
// 2. 初始化客户端
if (!client.init()) {
std::cerr << "客户端初始化失败,程序退出!" << std::endl;
return -1;
}
// 3. 运行客户端(循环交互,直到输入 exit)
client.run();
} catch (const std::exception& e) {
// 异常捕获:增强程序健壮性
std::cerr << "客户端运行异常:" << e.what() << std::endl;
return -1;
}
return 0;
}
二、封装思路与核心设计亮点
TCL-L-L----------------------------------
一、TCP 服务器(Linux 端)
tcp_server_linux.cpp#include <iostream> #include <cstring> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define PORT 8080 // 监听端口 #define BUF_SIZE 1024 // 数据缓冲区大小 int main() { // 1. 创建 TCP 套接字(SOCK_STREAM 表示 TCP 协议) int listen_fd = socket(AF_INET, SOCK_STREAM, 0); if (listen_fd < 0) { perror("socket 创建失败"); return -1; } // 2. 设置套接字选项:允许端口复用(避免重启服务器时端口占用报错) int opt = 1; if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) { perror("setsockopt 失败"); close(listen_fd); return -1; } // 3. 填充服务器地址结构体(IPv4) struct sockaddr_in serv_addr; memset(&serv_addr, 0, sizeof(serv_addr)); // 清空结构体 serv_addr.sin_family = AF_INET; // 地址族:IPv4 serv_addr.sin_port = htons(PORT); // 端口号:转网络字节序(大端) serv_addr.sin_addr.s_addr = INADDR_ANY; // 监听本机所有网卡 IP // 4. 绑定套接字到指定 IP + 端口 if (bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) { perror("bind 绑定失败"); close(listen_fd); return -1; } // 5. 开启监听(转为被动套接字,等待客户端连接) // 第二个参数:监听队列长度(最多同时等待 5 个连接) if (listen(listen_fd, 5) < 0) { perror("listen 监听失败"); close(listen_fd); return -1; } std::cout << "=== TCP 服务器(Linux)已启动 ===" << std::endl; std::cout << "监听端口:" << PORT << ",等待客户端连接..." << std::endl; // 循环监听客户端连接(支持多客户端依次连接) while (true) { // 6. 阻塞等待客户端连接,成功后返回新的通信套接字 struct sockaddr_in cli_addr; socklen_t cli_len = sizeof(cli_addr); int conn_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, &cli_len); if (conn_fd < 0) { perror("accept 接受连接失败"); continue; } // 打印客户端连接信息(IP + 端口) std::cout << "\n【新连接】客户端 " << inet_ntoa(cli_addr.sin_addr) // 客户端 IP(转字符串) << ":" << ntohs(cli_addr.sin_port) // 客户端端口(转主机字节序) << " 已连接" << std::endl; // 7. 与当前客户端循环通信 char buffer[BUF_SIZE]; while (true) { memset(buffer, 0, BUF_SIZE); // 清空缓冲区 // 接收客户端数据(阻塞,直到收到数据/客户端断开) ssize_t recv_len = read(conn_fd, buffer, BUF_SIZE); // 处理接收结果:客户端断开(recv_len=0)或接收失败(recv_len<0) if (recv_len <= 0) { if (recv_len == 0) { std::cout << "【断开连接】客户端 " << inet_ntoa(cli_addr.sin_addr) << ":" << ntohs(cli_addr.sin_port) << " 主动断开" << std::endl; } else { perror("read 接收数据失败"); } close(conn_fd); // 关闭与该客户端的通信套接字 break; // 回到外层循环,等待新客户端连接 } // 打印客户端发送的消息 std::cout << "【收到】客户端消息:" << buffer << std::endl; // 回复客户端(拼接收到的消息) std::string reply = "服务器已收到:" + std::string(buffer); write(conn_fd, reply.c_str(), reply.length()); std::cout << "【回复】已发送:" << reply << std::endl; } } // 理论上服务器不会执行到这里(无限循环监听) close(listen_fd); return 0; }二、TCP 客户端(Linux 端)
tcp_client_linux.cpp#include <iostream> #include <cstring> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define SERVER_IP "127.0.0.1" // 服务器 IP(本机测试用回环地址,跨机器改实际 IP) #define SERVER_PORT 8080 // 服务器端口(与服务器保持一致) #define BUF_SIZE 1024 // 数据缓冲区大小 int main() { // 1. 创建 TCP 套接字 int sock_fd = socket(AF_INET, SOCK_STREAM, 0); if (sock_fd < 0) { perror("socket 创建失败"); return -1; } // 2. 填充服务器地址结构体 struct sockaddr_in serv_addr; memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERVER_PORT); // 将服务器 IP 字符串转为网络字节序的二进制格式 if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) { perror("IP 地址转换失败(请检查服务器 IP 是否正确)"); close(sock_fd); return -1; } // 3. 连接服务器(TCP 三次握手) if (connect(sock_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) { perror("连接服务器失败"); close(sock_fd); return -1; } std::cout << "=== TCP 客户端(Linux)已启动 ===" << std::endl; std::cout << "已成功连接到服务器:" << SERVER_IP << ":" << SERVER_PORT << std::endl; // 4. 与服务器循环通信 char send_buf[BUF_SIZE], recv_buf[BUF_SIZE]; while (true) { // 输入要发送的消息 std::cout << "\n请输入要发送的消息(输入 exit 退出):"; std::cin.getline(send_buf, BUF_SIZE); // 输入 exit 则主动断开连接 if (strcmp(send_buf, "exit") == 0) { std::cout << "主动断开与服务器的连接" << std::endl; break; } // 发送消息到服务器 ssize_t send_len = write(sock_fd, send_buf, strlen(send_buf)); if (send_len < 0) { perror("write 发送数据失败"); break; } std::cout << "【发送】已发送:" << send_buf << std::endl; // 接收服务器回复 memset(recv_buf, 0, BUF_SIZE); ssize_t recv_len = read(sock_fd, recv_buf, BUF_SIZE); if (recv_len <= 0) { if (recv_len == 0) { std::cerr << "服务器已断开连接" << std::endl; } else { perror("read 接收回复失败"); } break; } std::cout << "【收到】服务器回复:" << recv_buf << std::endl; } // 5. 关闭套接字,释放资源 close(sock_fd); std::cout << "客户端已退出" << std::endl; return 0; }三、编译 & 运行步骤
1. 编译代码
打开 Linux 终端,分别编译服务器和客户端:
# 编译服务器 g++ tcp_server_linux.cpp -o tcp_server # 编译客户端 g++ tcp_client_linux.cpp -o tcp_client2. 运行程序
第一步:启动服务器(先运行)
./tcp_server服务器启动后会输出:
=== TCP 服务器(Linux)已启动 === 监听端口:8080,等待客户端连接...第二步:启动客户端(新开一个终端)
./tcp_client客户端启动后会输出:
=== TCP 客户端(Linux)已启动 === 已成功连接到服务器:127.0.0.1:8080四、关键注意事项
1. 跨机器通信配置
如果服务器和客户端不在同一台 Linux 机器,需要:
- 修改客户端代码中的
SERVER_IP为服务器的实际局域网 IP(如192.168.1.100);- 服务器放行 8080 端口(TCP):
# Ubuntu/Debian 系统 sudo ufw allow 8080/tcp # CentOS/RHEL 系统 sudo firewall-cmd --add-port=8080/tcp --permanent sudo firewall-cmd --reload2. 端口占用问题
如果启动服务器时报
bind 绑定失败: Address already in use:
- 查看占用 8080 端口的进程:
lsof -i :8080;- 杀死该进程:
kill -9 进程ID;- 或修改代码中的
PORT为其他未占用端口(如 8081)。3. 核心 API 说明
API 函数 作用 socket()创建套接字(通信句柄) setsockopt()设置套接字选项(如端口复用) bind()绑定 IP + 端口(服务器专用) listen()开启监听(TCP 服务器专用) accept()接受客户端连接(TCP 服务器专用) connect()连接服务器(TCP 客户端专用) read()/write()收发数据(替代 recv()/send())五、运行效果示例
服务器终端输出
=== TCP 服务器(Linux)已启动 === 监听端口:8080,等待客户端连接... 【新连接】客户端 127.0.0.1:54321 已连接 【收到】客户端消息:Hello TCP Server! 【回复】已发送:服务器已收到:Hello TCP Server! 【收到】客户端消息:This is Linux Client 【回复】已发送:服务器已收到:This is Linux Client 【断开连接】客户端 127.0.0.1:54321 主动断开客户端终端输出
=== TCP 客户端(Linux)已启动 === 已成功连接到服务器:127.0.0.1:8080 请输入要发送的消息(输入 exit 退出):Hello TCP Server! 【发送】已发送:Hello TCP Server! 【收到】服务器回复:服务器已收到:Hello TCP Server! 请输入要发送的消息(输入 exit 退出):This is Linux Client 【发送】已发送:This is Linux Client 【收到】服务器回复:服务器已收到:This is Linux Client 请输入要发送的消息(输入 exit 退出):exit 主动断开与服务器的连接 客户端已退出
6262

被折叠的 条评论
为什么被折叠?



