简易回声服务器实现与网络测试指南
目录
一、问题背景与解决方案
二、服务端实现
1、核心逻辑
2、完整代码实现
3、代码详细讲解:UDP Echo 服务器
1. 头文件引入
2. EchoServer 类定义
2.1 构造函数
2.2 Start方法 - 服务器主循环
2.3 析构函数
3. main函数
4. 程序工作流程
5. 关键系统调用说明
6. 特点
7. 可能的改进(后面会额外补充)
三、客户端实现
1、核心逻辑
2、完整代码实现
3、详细讲解:EchoClient UDP 客户端代码
1. 代码整体架构
类设计
主程序
2. 构造函数详解
关键点:
3. Start() 方法详解
3.1 服务器地址设置
3.2 主循环
第一:sendto() 函数
第二:recvfrom() 函数
3.3 缓冲区处理
4. 析构函数
5. 网络通信流程总结
6. 代码特点分析
7. 关键系统调用和函数
8. 编译运行说明
9. 完整工作流程示例
四、网络测试与部署指南
1、静态编译客户端
2、程序分发方法
五、代码优化说明
一、问题背景与解决方案
在进行网络通信测试时,我们遇到一个典型问题:当客户端向服务端发送数据后,服务端能够打印接收到的数据(服务端可见),但客户端无法确认服务端是否成功接收(客户端不可见)。为了解决这个问题,我们可以将服务端改造为回声服务器,使其在接收数据后将相同内容返回给客户端,这样客户端就能通过接收响应来验证通信是否正常。
二、服务端实现
1、核心逻辑
-
使用UDP协议接收客户端数据
-
打印接收到的数据(包括客户端IP和端口)
-
将接收到的数据加上前缀后返回给客户端
2、完整代码实现
为此,我们可以将该服务器改造为一个简易回声服务器。当服务端接收到客户端发送的数据时,除了在服务端打印输出外,还会调用sendto函数将接收到的数据原样回传给对应的客户端。
需要特别说明的是,服务端在调用sendto函数时必须提供客户端的网络属性信息。实际上,这些信息在数据接收阶段就已经通过recvfrom函数获取并保存了,因此服务端完全知晓需要回应的客户端信息。
#include
#include
#include
#include
#include
#include
class EchoServer {
private:
int _sockfd;
public:
EchoServer(int port) {
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(port);
if (bind(_sockfd, (const struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
}
void Start() {
const int BUFFER_SIZE = 128;
char buffer[BUFFER_SIZE];
for (;;) {
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 接收客户端数据
ssize_t size = recvfrom(_sockfd, buffer, BUFFER_SIZE - 1, 0,
(struct sockaddr*)&peer, &len);
if (size > 0) {
buffer[size] = ' ';
// 打印客户端信息
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(peer.sin_addr), ip_str, INET_ADDRSTRLEN);
int port = ntohs(peer.sin_port);
std::cout << "Received from " << ip_str << ":" << port
<< " - " << buffer << std::endl;
// 构造回声消息
std::string echo_msg = "Echo from server: ";
echo_msg += buffer;
// 发送回声响应
sendto(_sockfd, echo_msg.c_str(), echo_msg.size(), 0,
(struct sockaddr*)&peer, len);
} else {
std::cerr << "recvfrom error" << std::endl;
}
}
}
~EchoServer() {
close(_sockfd);
}
};
int main() {
EchoServer server(8888);
server.Start();
return 0;
}
3、代码详细讲解:UDP Echo 服务器
这是一个使用C++实现的UDP Echo服务器,它会接收客户端发送的消息,并将相同的消息(添加了前缀)返回给客户端。
1. 头文件引入
#include // 标准输入输出
#include // 字符串操作
#include // 套接字编程
#include // 互联网地址族
#include // IP地址转换
#include // POSIX操作系统API
这些头文件提供了创建网络服务器所需的基本功能:套接字创建和管理、地址结构定义、输入输出功能、字符串处理
2. EchoServer 类定义
class EchoServer {
private:
int _sockfd; // 服务器套接字文件描述符
2.1 构造函数
public:
EchoServer(int port) {
// 创建UDP套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 设置服务器地址结构
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr)); // 清零结构体
server_addr.sin_family = AF_INET; // IPv4地址族
server_addr.sin_addr.s_addr = INADDR_ANY; // 接受所有接口的连接
server_addr.sin_port = htons(port); // 设置端口号(网络字节序)
// 绑定套接字到指定端口
if (bind(_sockfd, (const struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
}
关键点:
-
使用
socket()创建UDP套接字(SOCK_DGRAM) -
使用
sockaddr_in结构体设置服务器地址:-
AF_INET表示IPv4 -
INADDR_ANY表示接受来自任何网络接口的连接 -
htons()将主机字节序转换为网络字节序
-
-
使用
bind()将套接字绑定到指定端口
2.2 Start方法 - 服务器主循环
void Start() {
const int BUFFER_SIZE = 128; // 缓冲区大小
char buffer[BUFFER_SIZE]; // 接收缓冲区
for (;;) { // 无限循环
struct sockaddr_in peer; // 客户端地址结构
socklen_t len = sizeof(peer);
// 接收客户端数据
ssize_t size = recvfrom(_sockfd, buffer, BUFFER_SIZE - 1, 0,
(struct sockaddr*)&peer, &len);
if (size > 0) {
buffer[size] = ' '; // 确保字符串以null结尾
// 打印客户端信息
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(peer.sin_addr), ip_str, INET_ADDRSTRLEN);
int port = ntohs(peer.sin_port);
std::cout << "Received from " << ip_str << ":" << port
<< " - " << buffer << std::endl;
// 构造回声消息
std::string echo_msg = "Echo from server: ";
echo_msg += buffer;
// 发送回声响应
sendto(_sockfd, echo_msg.c_str(), echo_msg.size(), 0,
(struct sockaddr*)&peer, len);
} else {
std::cerr << "recvfrom error" << std::endl;
}
}
}
关键点:
-
使用
recvfrom()接收UDP数据报:-
它会阻塞直到收到数据
-
同时获取客户端地址信息(存储在
peer结构中)
-
-
处理接收到的数据:
-
添加null终止符确保是合法字符串
-
使用
inet_ntop()将二进制IP地址转换为可读字符串 -
使用
ntohs()将端口号转换为主机字节序
-
-
构造响应消息(添加前缀"Echo from server: ")
-
使用
sendto()将响应发送回客户端
1. inet_ntop(AF_INET, &(peer.sin_addr), ip_str, INET_ADDRSTRLEN)
功能:将 二进制格式的IPv4地址 转换为 点分十进制字符串格式(如 "192.168.1.1")。
参数解析:
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

-
af: 地址族(Address Family),这里用AF_INET表示IPv4。 -
src: 指向二进制格式IP地址的指针,这里是&(peer.sin_addr)。 -
dst: 存储转换结果的字符串缓冲区,这里是ip_str。 -
size: 目标缓冲区的大小,这里是INET_ADDRSTRLEN(定义为16,足够存放IPv4字符串)。
代码中的具体用法:
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(peer.sin_addr), ip_str, INET_ADDRSTRLEN);
-
peer.sin_addr是sockaddr_in结构体中的成员,类型为struct in_addr,存储了二进制格式的IPv4地址(如0xC0A80101对应192.168.1.1)。 -
&(peer.sin_addr)取地址,传递给inet_ntop。 -
转换结果存入
ip_str,例如:"192.168.1.1"。
为什么需要这个转换?
-
网络传输中IP地址以二进制形式存储(高效),但人类可读格式需要字符串。
-
类似函数:
inet_addr()(已废弃)、inet_aton()。
2. ntohs(peer.sin_port)
功能:将 网络字节序(大端)的16位端口号 转换为主机字节序(可能是小端或大端,取决于CPU架构)。
参数解析:
uint16_t ntohs(uint16_t netshort);

-
netshort: 网络字节序的16位值(这里是peer.sin_port)。 -
返回值:主机字节序的16位值。
代码中的具体用法:
int port = ntohs(peer.sin_port);
-
peer.sin_port是sockaddr_in结构体中的成员,类型为uint16_t,存储了网络字节序的端口号(如0x22B8对应8888)。 -
ntohs()将其转换为主机字节序:-
如果主机是小端(如x86),
0x22B8(大端)会被转换为0xB822(小端)。 -
如果主机是大端(如某些嵌入式系统),值保持不变。
-
为什么需要这个转换?
-
网络协议规定使用大端字节序(网络字节序),但不同CPU可能使用小端(如Intel)或大端。
-
类似函数:
-
htons():主机字节序 → 网络字节序(发送数据时用)。 -
ntohl()/htonl():处理32位值(如IPv4地址)。
-
结合代码的上下文
在 EchoServer 的 Start() 方法中:
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(peer.sin_addr), ip_str, INET_ADDRSTRLEN); // 二进制IP → 字符串
int port = ntohs(peer.sin_port); // 网络端口 → 主机端口
std::cout << "Received from " << ip_str << ":" << port << " - " << buffer << std::endl;
-
打印客户端地址时,需要将二进制IP和端口号转换为人类可读格式。
-
例如,客户端地址可能是
192.168.1.100:54321,但内存中存储的是二进制值,必须通过转换才能正确显示。
关键点总结
| 函数 | 方向 | 数据大小 | 典型用途 |
|---|---|---|---|
|
| 网络二进制 → 字符串 | IPv4 | 打印或记录IP地址 |
|
| 网络字节序 → 主机字节序 | 16位 | 读取端口号或短整型数据 |
这两个函数是网络编程中处理地址和端口号可读性的基础工具,确保数据在不同字节序的机器间正确解析。
3. INET_ADDRSTRLEN 宏
它用于定义存储 IPv4 地址字符串表示(点分十进制格式)所需的最大缓冲区长度。以下是具体说明:
宏定义与用途
-
定义位置:通常在
或头文件中定义。 -
值:
#define INET_ADDRSTRLEN 16。 -
用途:指定存储 IPv4 地址字符串(如
"192.168.1.1")所需的缓冲区大小,包括终止符

