第一种:Linux服务器+Windows客户端
UDP-L-W------------------------------------
1. UDP 服务器(Linux 端)
udp_server_linux.cpp
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
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 服务器
bash# 进入服务器代码目录 g++ udp_server_linux.cpp -o udp_server # 运行服务器 ./udp_server2. 编译 Windows 客户端
方式 1:MinGW 编译(推荐)
bash# 进入客户端代码目录 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 --reloadWindows 客户端:允许程序通过防火墙(首次运行时弹窗选择「允许」)。
跨平台 API 差异
功能 Linux Windows 头文件 <sys/socket.h><netinet/in.h><winsock2.h><ws2tcpip.h>套接字类型 intSOCKET关闭套接字 close()closesocket()库初始化 无需 WSAStartup()库清理 无需 WSACleanup()错误处理 perror()WSAGetLastError()5. 运行效果示例
Linux 服务器终端
bashUDP 服务器(Linux)已启动,监听端口 8080... 【收到】来自 192.168.1.105:54321 的消息:Hello Linux UDP Server! 我是 Windows 客户端~ 【回复】已发送给客户端Windows 客户端终端
bash【发送】给 Linux 服务器:Hello Linux UDP Server! 我是 Windows 客户端~ 【收到】Linux 服务器回复:Linux 服务器已收到你的 UDP 消息!
TCP-L-W--------------------------------------
1. TCP 服务器(Linux 端)
tcp_server_linux.cpp
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
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 服务器
bash# 进入服务器代码目录 g++ tcp_server_linux.cpp -o tcp_server # 运行服务器(后台运行可加 &:./tcp_server &) ./tcp_server2. 编译 Windows 客户端
方式 1:MinGW 编译(推荐,轻量)
bash# 进入客户端代码目录 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 端口
bash# Ubuntu/Debian 系统 sudo ufw allow 8080/tcp # CentOS/RHEL 系统 sudo firewall-cmd --add-port=8080/tcp --permanent sudo firewall-cmd --reloadWindows 客户端:首次运行客户端时,防火墙弹窗选择「允许访问」(专用网络 + 公用网络)。
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 服务器终端
bashTCP 服务器(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 客户端终端
bash已成功连接到 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 端)
udp_server_linux.cp
cpp#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:服务器自身的地址(用于绑定) // cli_addr:客户端的地址(接收数据时自动填充) struct sockaddr_in serv_addr, 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; }二、UDP 客户端(Linux 端)
udp_client_linux.cpp
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和端口号, 不能随便改变, 否则客户端无法正确访问服务端;
三、编译 & 运行步骤
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):
bash# 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()客户端绑定 无需(系统自动分配临时端口) 无需(系统自动分配临时端口) 五、运行效果示例
服务器终端输出
bash=== 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客户端终端输出
bash=== 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 客户端退出
TCL-L-L----------------------------------
一、TCP 服务器(Linux 端)
tcp_server_linux.cpp
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
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 终端,分别编译服务器和客户端:
bash# 编译服务器 g++ tcp_server_linux.cpp -o tcp_server # 编译客户端 g++ tcp_client_linux.cpp -o tcp_client2. 运行程序
第一步:启动服务器(先运行)
bash./tcp_server服务器启动后会输出:
=== TCP 服务器(Linux)已启动 === 监听端口:8080,等待客户端连接...第二步:启动客户端(新开一个终端)
bash./tcp_client客户端启动后会输出:
bash=== TCP 客户端(Linux)已启动 === 已成功连接到服务器:127.0.0.1:8080四、关键注意事项
1. 跨机器通信配置
如果服务器和客户端不在同一台 Linux 机器,需要:
修改客户端代码中的
SERVER_IP为服务器的实际局域网 IP (如192.168.1.100);服务器放行 8080 端口(TCP):
bash# 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())五、运行效果示例
服务器终端输出
bash=== 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 主动断开客户端终端输出
bash=== 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 主动断开与服务器的连接 客户端已退出