欢迎光临
我们一直在努力

简易回声服务器实现与网络测试指南

目录

一、问题背景与解决方案

二、服务端实现

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 <iostream>
    #include <cstring>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <unistd.h>

    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] = '\\0';
    // 打印客户端信息
    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 <iostream> // 标准输入输出
    #include <cstring> // 字符串操作
    #include <sys/socket.h> // 套接字编程
    #include <netinet/in.h> // 互联网地址族
    #include <arpa/inet.h> // IP地址转换
    #include <unistd.h> // 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] = '\\0'; // 确保字符串以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,但内存中存储的是二进制值,必须通过转换才能正确显示。

    关键点总结

    函数

    方向

    数据大小

    典型用途

    inet_ntop

    网络二进制 → 字符串

    IPv4

    打印或记录IP地址

    ntohs

    网络字节序 → 主机字节序

    16位

    读取端口号或短整型数据

    这两个函数是网络编程中处理地址和端口号可读性的基础工具,确保数据在不同字节序的机器间正确解析。

    3. INET_ADDRSTRLEN 宏

    它用于定义存储 IPv4 地址字符串表示(点分十进制格式)所需的最大缓冲区长度。以下是具体说明:

    宏定义与用途

    • 定义位置:通常在 <netinet/in.h> 或 <arpa/inet.h> 头文件中定义。

    • 值:#define INET_ADDRSTRLEN 16。

    • 用途:指定存储 IPv4 地址字符串(如 "192.168.1.1")所需的缓冲区大小,包括终止符 \\0。

    长度计算依据

    • IPv4 地址由 4 组十进制数(每组 0-255)和 3 个点(.)组成,格式为 A.B.C.D。

    • 每组十进制数最多占 3 字节(如 255),加上 3 个点和 1 个终止符,总长度为:4组 × 3字节 + 3个点 + 1个终止符 = 16字节

    • 示例:"255.255.255.255" 是最长的合法 IPv4 地址字符串,占用 16 字节。

    相关宏:INET6_ADDRSTRLEN

    • 用于 IPv6 地址的字符串表示(十六进制格式,如 "2001:0db8::1")。

    • 值:通常为 46(兼容格式,包括冒号和可能的缩略表示)。

    • 用途:确保缓冲区足够存储最长的 IPv6 地址字符串。

    使用场景

    • 函数参数:如 inet_ntop() 的第四个参数需指定目标缓冲区大小,此时可直接使用 INET_ADDRSTRLEN:

      char ip_str[INET_ADDRSTRLEN];
      inet_ntop(AF_INET, &addr.sin_addr, ip_str, INET_ADDRSTRLEN);

    • 动态分配:若动态分配缓冲区,可基于该宏确保安全:

      char *ip_str = malloc(INET_ADDRSTRLEN);

    注意事项

    • 平台差异:某些系统(如 Windows 的 Winsock)可能定义不同的值(如 22),需参考具体文档。

    • IPv6 兼容性:处理 IPv6 时需使用 INET6_ADDRSTRLEN,避免缓冲区溢出。

    示例代码

    #include <stdio.h>
    #include <arpa/inet.h>

    int main() {
        struct in_addr addr = { .s_addr = htonl(0xC0A80101) }; // 192.168.1.1
        char ip_str[INET_ADDRSTRLEN];

        if (inet_ntop(AF_INET, &addr, ip_str, INET_ADDRSTRLEN) == NULL) {
            perror("inet_ntop failed");
            return 1;
        }

        printf("IPv4 Address: %s\\n", ip_str); // 输出: IPv4 Address: 192.168.1.1
        return 0;
    }

    总结:INET_ADDRSTRLEN 是一个预定义的宏,值为 16,用于确保 IPv4 地址字符串转换时有足够的缓冲区空间。它是网络编程中处理地址字符串时的关键常量。

    2.3 析构函数

    ~EchoServer() {
    close(_sockfd); // 关闭套接字
    }

    确保在服务器对象销毁时关闭套接字,释放资源。

    3. main函数

    int main() {
    EchoServer server(8888); // 创建服务器实例,监听8888端口
    server.Start(); // 启动服务器
    return 0;
    }

    创建服务器实例并启动主循环。

    4. 程序工作流程

  • 创建UDP套接字

  • 绑定到指定端口(8888)

  • 进入无限循环:

    • 等待接收客户端消息

    • 打印客户端地址和消息内容

    • 构造响应消息并发送回客户端

  • 如果接收失败,打印错误信息

  • 5. 关键系统调用说明

    • socket(): 创建通信端点

    • bind(): 将地址与套接字关联

    • recvfrom(): 从套接字接收数据(UDP)

    • sendto(): 通过套接字发送数据(UDP)

    • close(): 关闭文件描述符

    6. 特点

    • UDP协议:无连接,不可靠但高效

    • 多客户端支持:每次接收都获取客户端地址,可以服务多个客户端

    • 简单回声服务:将接收到的消息原样返回(添加前缀)

    • 错误处理:基本的错误检测和退出机制

    7. 可能的改进(后面会额外补充)

  • 添加信号处理(如SIGINT)以优雅关闭服务器

  • 实现超时机制,避免无限阻塞

  • 添加日志记录功能

  • 支持更大的消息(当前限制为127字节)

  • 添加配置选项(如绑定地址、缓冲区大小等)

  • 这个服务器适合用于学习基本的UDP网络编程概念,可以作为更复杂网络应用的基础。


    三、客户端实现

    1、核心逻辑

  • 连接指定服务端

  • 发送用户输入的数据

  • 接收并打印服务端的回声响应

  •         修改服务端代码后,客户端代码也需要相应调整。客户端发送数据至服务端后,由于服务端会将数据回传,因此客户端需要通过recvfrom函数接收响应数据。

            在调用recvfrom接收服务端响应时,客户端虽然已获知服务端网络信息,但仍需获取这些参数。建议使用临时变量存储这些信息,避免因参数设置为空而引发问题。

            客户端收到服务端回传的数据后,直接输出原始内容即可。需要注意的是,客户端发送给服务端的数据不仅会在服务端打印显示,还会被服务端重新发回客户端,最终由客户端打印输出。

    2、完整代码实现

    #include <iostream>
    #include <cstring>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <unistd.h>

    class EchoClient {
    private:
    int _sockfd;
    std::string _server_ip;
    int _server_port;

    public:
    EchoClient(const std::string& ip, int port)
    : _server_ip(ip), _server_port(port) {
    _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (_sockfd < 0) {
    perror("socket creation failed");
    exit(EXIT_FAILURE);
    }
    }

    void Start() {
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(_server_port);
    inet_pton(AF_INET, _server_ip.c_str(), &server_addr.sin_addr);

    const int BUFFER_SIZE = 128;
    char buffer[BUFFER_SIZE];

    for (;;) {
    std::string msg;
    std::cout << "Please Enter# ";
    std::getline(std::cin, msg);

    // 发送数据到服务端
    sendto(_sockfd, msg.c_str(), msg.size(), 0,
    (const struct sockaddr*)&server_addr, sizeof(server_addr));

    // 接收服务端响应
    struct sockaddr_in tmp_addr;
    socklen_t addr_len = sizeof(tmp_addr);
    ssize_t size = recvfrom(_sockfd, buffer, BUFFER_SIZE – 1, 0,
    (struct sockaddr*)&tmp_addr, &addr_len);

    if (size > 0) {
    buffer[size] = '\\0';
    std::cout << "Server response: " << buffer << std::endl;
    } else {
    std::cerr << "recvfrom error" << std::endl;
    }
    }
    }

    ~EchoClient() {
    close(_sockfd);
    }
    };

    int main(int argc, char* argv[]) {
    if (argc != 3) {
    std::cerr << "Usage: " << argv[0] << " <server_ip> <server_port>" << std::endl;
    return 1;
    }

    EchoClient client(argv[1], std::stoi(argv[2]));
    client.Start();
    return 0;
    }

    3、详细讲解:EchoClient UDP 客户端代码

            这段代码实现了一个基于 UDP 协议的简单回声(Echo)客户端,可以与服务器进行交互式通信。下面我将从整体架构、关键函数、网络通信流程等方面进行详细分析。

    1. 代码整体架构

    类设计

    class EchoClient {
    private:
    int _sockfd; // 套接字文件描述符
    std::string _server_ip; // 服务器IP地址
    int _server_port; // 服务器端口号

    public:
    // 构造函数
    EchoClient(const std::string& ip, int port);

    // 启动客户端
    void Start();

    // 析构函数
    ~EchoClient();
    };

    • 使用面向对象方式封装UDP客户端

    • 私有成员保存连接信息

    • 公共接口提供初始化和启动功能

    主程序

    int main(int argc, char* argv[]) {
    if (argc != 3) {
    std::cerr << "Usage: " << argv[0] << " <server_ip> <server_port>" << std::endl;
    return 1;
    }

    EchoClient client(argv[1], std::stoi(argv[2]));
    client.Start();
    return 0;
    }

    • 处理命令行参数(服务器IP和端口)

    • 创建客户端对象并启动

    2. 构造函数详解

    EchoClient(const std::string& ip, int port)
    : _server_ip(ip), _server_port(port) {
    // 创建UDP套接字
    _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (_sockfd < 0) {
    perror("socket creation failed");
    exit(EXIT_FAILURE);
    }
    }

    关键点:
    • 套接字创建:

      • socket(AF_INET, SOCK_DGRAM, 0) 创建UDP套接字

      • AF_INET 表示IPv4地址族

      • SOCK_DGRAM 表示无连接的数据报服务(UDP)

      • 第三个参数0表示使用默认协议(UDP)

    • 错误处理:

      • 检查套接字是否创建成功

      • 失败时打印错误信息并退出程序

    3. Start() 方法详解

    这是客户端的核心功能实现,包含与服务器交互的完整流程。

    3.1 服务器地址设置

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr)); // 清零结构体
    server_addr.sin_family = AF_INET; // IPv4地址族
    server_addr.sin_port = htons(_server_port); // 端口号(网络字节序)
    inet_pton(AF_INET, _server_ip.c_str(), &server_addr.sin_addr); // IP地址转换

    关键函数:

    • htons():主机字节序转网络字节序(16位)

    • inet_pton():将点分十进制IP字符串转换为二进制格式

    3.2 主循环

    const int BUFFER_SIZE = 128;
    char buffer[BUFFER_SIZE];

    for (;;) {
    // 获取用户输入
    std::string msg;
    std::cout << "Please Enter# ";
    std::getline(std::cin, msg);

    // 发送数据到服务端
    sendto(_sockfd, msg.c_str(), msg.size(), 0,
    (const struct sockaddr*)&server_addr, sizeof(server_addr));

    // 接收服务端响应
    struct sockaddr_in tmp_addr;
    socklen_t addr_len = sizeof(tmp_addr);
    ssize_t size = recvfrom(_sockfd, buffer, BUFFER_SIZE – 1, 0,
    (struct sockaddr*)&tmp_addr, &addr_len);

    // 处理响应
    if (size > 0) {
    buffer[size] = '\\0'; // 添加字符串终止符
    std::cout << "Server response: " << buffer << std::endl;
    } else {
    std::cerr << "recvfrom error" << std::endl;
    }
    }

    关键网络函数:

    第一:sendto() 函数

        sendto() 是 Unix/Linux 系统中的一个核心系统调用,用于在 无连接 的网络通信中发送数据(主要是 UDP 协议)。下面我将从多个方面详细讲解这个函数。

    1. 函数原型

    #include <sys/socket.h>

    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                   const struct sockaddr *dest_addr, socklen_t addrlen);

    2. 参数详解

    2.1 sockfd (套接字描述符)

    • 作用:标识要使用的套接字

    • 类型:整数文件描述符

    • 获取方式:通常通过 socket() 系统调用创建

    • 注意:必须是支持发送操作的套接字(如 UDP 套接字)

    2.2 buf (数据缓冲区)

    • 作用:指向要发送数据的缓冲区

    • 类型:const void*(通用指针)

    • 特点:

      • 可以是任何类型的数据(字符、结构体等)

      • 数据不会被 sendto() 修改(因为标记为 const)

    2.3 len (数据长度)

    • 作用:指定要发送的字节数

    • 类型:size_t(无符号整数)

    • 重要提示:

      • 不是缓冲区大小,而是实际要发送的数据长度

      • 如果 len 为 0,通常不会发送数据(但可能发送 0 长度数据报)

    2.4 flags (控制标志)

    • 作用:控制发送行为的标志位

    • 常用选项(通过位或 | 组合):

      • 0:默认行为

      • MSG_DONTWAIT:非阻塞发送(如果不可立即发送则返回 EAGAIN)

      • MSG_MORE:告诉内核还有更多数据要发送(TCP 优化)

      • MSG_CONFIRM:通知链路层邻居缓存有效(某些系统)

    • UDP 常用:通常设为 0

    2.5 dest_addr (目标地址)

    • 作用:指定数据报的目标地址

    • 类型:const struct sockaddr*

    • 特点:

      • 对于 UDP 这样的无连接协议必须指定

      • 对于已连接的套接字可以设为 NULL

    • 地址结构体:

      • IPv4:struct sockaddr_in

      • IPv6:struct sockaddr_in6

      • 通用:struct sockaddr(使用时需要类型转换)

    2.6 addrlen (地址长度)

    • 作用:指定地址结构体的长度

    • 类型:socklen_t(通常是 32 位无符号整数)

    • 为什么需要:

      • 不同地址族(IPv4/IPv6)的结构体大小不同

      • 系统需要知道地址结构的准确大小才能正确解析

    3. 返回值

    • 成功:返回实际发送的字节数

      • 对于 UDP 通常应该等于 len(因为 UDP 是数据报协议)

      • 如果返回值 < len,可能是信号中断或缓冲区满

    • 失败:返回 -1 并设置 errno

      • 常见错误:

        • EACCES:无权限(如广播地址未设置权限)

        • EAGAIN/EWOULDBLOCK:非阻塞模式下套接字不可写

        • ECONNRESET:连接被对方重置(对 UDP 来说可能是 ICMP 错误)

        • EDESTADDRREQ:未指定目标地址(对无连接套接字)

        • EINTR:被信号中断

        • EINVAL:无效参数

        • EMSGSIZE:消息太大(超过 MTU 或协议限制)

        • ENOBUFS:系统缓冲区不足

        • ENOTCONN:套接字未连接(但对 UDP 通常不需要连接)

    4. 使用示例

    基本 UDP 发送

    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);

    const char *message = "Hello, Server!";
    ssize_t sent = sendto(sockfd, message, strlen(message), 0,
                         (struct sockaddr*)&server_addr, sizeof(server_addr));

    if (sent == -1) {
        perror("sendto failed");
    } else {
        printf("Sent %zd bytes to server\\n", sent);
    }

    发送二进制数据

    struct {
        int id;
        float value;
    } data = {42, 3.14f};

    sendto(sockfd, &data, sizeof(data), 0,
          (struct sockaddr*)&server_addr, sizeof(server_addr));

    5. 重要特性

    5.1 无连接性

    • 每次调用 sendto() 都需要指定目标地址

    • 不同于 TCP 的 send()(连接后无需指定地址)

    • 适合 UDP 这样的无连接协议

    5.2 数据报边界

    • UDP 是面向消息的协议,每次 sendto() 对应一个独立的数据报

    • 接收方会完整收到这个数据报(除非被截断或丢失)

    5.3 长度限制

    • UDP 数据报最大长度受限于 IP 层 MTU(通常约 1500 字节)

    • 实际可用载荷大小:

      • IPv4:65507 字节(65535 – 8 字节 UDP 头 – 20 字节 IP 头)

      • 但建议保持在路径 MTU 以下(通常 1472 字节或更小)

    6. 与相关函数比较

    函数

    协议

    连接性

    地址参数

    适用场景

    sendto()

    UDP/其他

    无连接

    必须提供

    通用发送

    send()

    TCP/UDP

    可连接

    可选

    已连接套接字

    sendmsg()

    所有

    任意

    通过消息头

    高级功能(多缓冲区、控制信息)

    7. 常见问题与解决方案

    7.1 发送失败(返回 -1)

    if (sendto(sockfd, buf, len, 0, addr, addrlen) == -1) {
        switch(errno) {
            case EMSGSIZE:
                // 数据太大,需要分片或减少大小
                break;
            case EACCES:
                // 检查权限(如广播权限)
                break;
            default:
                perror("sendto error");
        }
    }

    7.2 部分发送

    • UDP 理论上应该全部发送或失败

    • 如果返回小于 len:

      • 可能是信号中断(检查 errno 是否为 EINTR)

      • 可能是系统实现问题(罕见)

    7.3 性能优化

    • 对于高频小数据报:

      • 考虑使用 sendmmsg()(Linux 特有)批量发送

      • 或使用已连接套接字 + send() 减少地址处理开销

    8. 底层实现原理

  • 内核处理流程:

    • 从用户空间复制数据到内核缓冲区

    • 添加 UDP 头(源端口、目的端口、长度、校验和)

    • 添加 IP 头并路由

    • 交给网络设备驱动发送

  • 校验和计算:

    • 如果校验和字段为 0,可能不计算(取决于实现)

    • 否则计算 UDP 伪首部 + UDP 头 + 数据的校验和

  • 缓冲区管理:

    • 使用套接字发送缓冲区

    • 如果缓冲区满,可能阻塞或返回 EAGAIN(取决于阻塞/非阻塞模式)

  • 9. 完整代码示例(带错误处理)

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <errno.h>

    int main() {
        // 创建UDP套接字
        int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd == -1) {
            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_port = htons(8080);

        if (inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr) <= 0) {
            perror("invalid address");
            close(sockfd);
            exit(EXIT_FAILURE);
        }

        // 发送数据
        const char *message = "Hello from UDP client!";
        ssize_t bytes_sent = sendto(sockfd, message, strlen(message), 0,
                                   (struct sockaddr*)&server_addr, sizeof(server_addr));

        if (bytes_sent == -1) {
            perror("sendto failed");
        } else {
            printf("Successfully sent %zd bytes\\n", bytes_sent);
        }

        close(sockfd);
        return 0;
    }

    10. 总结

    sendto() 是 UDP 网络编程的核心函数,具有以下关键特性:

  • 适用于无连接协议(主要是 UDP)

  • 每次发送都需要指定目标地址

  • 保持数据报边界(每次调用发送一个独立数据报)

  • 需要处理各种错误情况(特别是 EMSGSIZE 和权限问题)

  • 性能受限于系统调用和缓冲区管理

  •         理解 sendto() 的工作原理对于编写高效可靠的网络应用程序至关重要,特别是在处理 UDP 协议时需要特别注意数据报大小和错误处理。

    第二:recvfrom() 函数

        recvfrom() 是 Unix/Linux 系统中用于从无连接套接字(主要是 UDP)接收数据的核心系统调用。下面我将从多个方面详细讲解这个函数。

    1. 函数原型

    #include <sys/socket.h>

    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                    struct sockaddr *src_addr, socklen_t *addrlen);

    2. 参数详解

    2.1 sockfd (套接字描述符)

    • 作用:标识要使用的套接字

    • 类型:整数文件描述符

    • 获取方式:通过 socket() 系统调用创建

    • 注意:必须是支持接收操作的套接字

    2.2 buf (接收缓冲区)

    • 作用:指向存储接收数据的缓冲区

    • 类型:void*(通用指针)

    • 特点:

      • 必须是已分配的内存区域

      • 缓冲区大小应足够容纳预期数据

    2.3 len (缓冲区长度)

    • 作用:指定缓冲区的最大容量

    • 类型:size_t(无符号整数)

    • 重要提示:

      • 不是要接收的数据量,而是缓冲区能容纳的最大字节数

      • 防止缓冲区溢出

    2.4 flags (控制标志)

    • 作用:控制接收行为的标志位

    • 常用选项(通过位或 | 组合):

      • 0:默认行为(阻塞直到收到数据)

      • MSG_DONTWAIT:非阻塞接收(如果没有数据立即返回 EAGAIN)

      • MSG_WAITALL:等待直到收到请求的全部字节(对 UDP 通常无效)

      • MSG_PEEK:查看数据但不从接收队列中移除

      • MSG_TRUNC:返回数据报实际长度(即使比缓冲区大)

    2.5 src_addr (源地址)

    • 作用:返回发送方的地址信息

    • 类型:struct sockaddr*

    • 特点:

      • 可以是 NULL(如果不关心发送方地址)

      • 通常需要转换为特定地址族的结构体(如 sockaddr_in)

    2.6 addrlen (地址长度指针)

    • 作用:输入输出参数

      • 输入:指定 src_addr 缓冲区的大小

      • 输出:返回实际写入的地址结构大小

    • 类型:socklen_t*(指向 socklen_t 的指针)

    • 为什么需要:

      • 处理不同地址族(IPv4/IPv6)的结构体大小不同

      • 系统需要知道可以写入多少地址数据

    3. 返回值

    • 成功:返回实际接收的字节数

      • 对于 UDP:应该是完整数据报的大小(除非被截断)

      • 返回 0:对于 UDP 表示接收到 0 长度数据报(合法但罕见)

    • 失败:返回 -1 并设置 errno

      • 常见错误:

        • EAGAIN/EWOULDBLOCK:非阻塞模式下无数据可读

        • ECONNREFUSED:收到 ICMP 端口不可达(对 UDP)

        • EINTR:被信号中断

        • EINVAL:无效参数

        • ENOTCONN:套接字未连接(但对 UDP 通常不需要连接)

    4. 使用示例

    基本 UDP 接收

    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    struct sockaddr_in server_addr, client_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(8080);
    bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));

    char buffer[1024];
    socklen_t client_len = sizeof(client_addr);
    ssize_t recv_len = recvfrom(sockfd, buffer, sizeof(buffer), 0,
                               (struct sockaddr*)&client_addr, &client_len);

    if (recv_len == -1) {
        perror("recvfrom failed");
    } else {
        buffer[recv_len] = '\\0'; // 添加字符串结束符
        printf("Received %zd bytes from %s:%d\\n", recv_len,
               inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
        printf("Data: %s\\n", buffer);
    }

    处理大数据报(使用 MSG_TRUNC)

    char buffer[2000];
    socklen_t temp_len = sizeof(client_addr);
    int flags = 0;
    ssize_t recv_len = recvfrom(sockfd, buffer, sizeof(buffer), flags | MSG_TRUNC,
                               (struct sockaddr*)&client_addr, &temp_len);

    if (recv_len > sizeof(buffer)) {
        printf("Partial read: got %zu bytes of %zd byte datagram\\n",
               sizeof(buffer), recv_len);
    }

    5. 重要特性

    5.1 无连接性

    • 每次调用 recvfrom() 都会返回发送方的地址

    • 不同于 TCP 的 recv()(连接套接字不提供对端地址)

    • 适合 UDP 这样的无连接协议

    5.2 数据报边界

    • UDP 是面向消息的协议,每次 recvfrom() 返回一个完整的数据报

    • 除非缓冲区太小,否则不会拆分数据报

    5.3 缓冲区管理

    • 如果数据报大于缓冲区:

      • 默认情况下:数据被截断,只返回缓冲区能容纳的部分

      • 使用 MSG_TRUNC 标志:可以获取实际数据报大小(即使被截断)

    • 建议缓冲区至少为可能的最大 UDP 数据报大小(通常 64KB)

    6. 与相关函数比较

    函数

    协议

    连接性

    地址参数

    适用场景

    recvfrom()

    UDP/其他

    无连接

    返回发送方地址

    通用接收

    recv()

    TCP/UDP

    可连接

    不提供地址

    已连接套接字

    recvmsg()

    所有

    任意

    通过消息头

    高级功能(多缓冲区、控制信息)

    7. 常见问题与解决方案

    7.1 接收失败(返回 -1)

    ssize_t recv_len = recvfrom(sockfd, buf, len, 0, addr, &addrlen);
    if (recv_len == -1) {
        switch(errno) {
            case EAGAIN:
                // 非阻塞模式下无数据,可以稍后重试
                break;
            case ECONNREFUSED:
                // 可能收到ICMP端口不可达,忽略或记录
                break;
            default:
                perror("recvfrom error");
        }
    }

    7.2 部分接收

    • UDP 理论上应该全部接收或失败

    • 如果返回小于实际数据报大小:

      • 缓冲区太小导致截断

      • 使用 MSG_TRUNC 标志检测这种情况

    7.3 性能优化

    • 对于高频接收:

      • 考虑使用 recvmmsg()(Linux 特有)批量接收

      • 或使用已连接套接字 + recv() 减少地址处理开销

      • 调整套接字缓冲区大小(SO_RCVBUF)

    8. 底层实现原理

  • 内核处理流程:

    • 检查套接字接收队列是否有数据

    • 如果没有数据:

      • 阻塞模式:等待直到数据到达

      • 非阻塞模式:立即返回 EAGAIN

    • 将数据从内核缓冲区复制到用户缓冲区

    • 填充发送方地址信息(如果请求)

  • 地址处理:

    • 根据接收到的数据包确定发送方地址族

    • 转换为 sockaddr 通用格式返回给用户

  • 数据报完整性:

    • UDP 数据报要么完整接收,要么被截断

    • 不会出现 TCP 那样的部分数据流

  • 9. 完整代码示例(带错误处理)

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <errno.h>

    #define BUF_SIZE 2048

    int main() {
        // 创建UDP套接字
        int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd == -1) {
            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(8080);

        if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
            perror("bind failed");
            close(sockfd);
            exit(EXIT_FAILURE);
        }

        // 接收循环
        while (1) {
            char buffer[BUF_SIZE];
            struct sockaddr_in client_addr;
            socklen_t client_len = sizeof(client_addr);

            ssize_t recv_len = recvfrom(sockfd, buffer, BUF_SIZE, 0,
                                       (struct sockaddr*)&client_addr, &client_len);

            if (recv_len == -1) {
                if (errno == EINTR) continue; // 被信号中断,继续
                perror("recvfrom failed");
                break;
            }

            // 处理接收到的数据
            char client_ip[INET_ADDRSTRLEN];
            inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
            printf("Received %zd bytes from %s:%d\\n",
                   recv_len, client_ip, ntohs(client_addr.sin_port));

            // 简单回显
            sendto(sockfd, buffer, recv_len, 0,
                  (struct sockaddr*)&client_addr, client_len);
        }

        close(sockfd);
        return 0;
    }

    10. 高级主题

    10.1 匿名端口

    • 服务器可以使用 bind() 到端口 0 让系统分配临时端口

    • 然后通过 getsockname() 获取实际绑定的端口

    10.2 多播接收

    // 加入多播组
    struct ip_mreq mreq;
    mreq.imr_multiaddr.s_addr = inet_addr("224.0.0.1");
    mreq.imr_interface.s_addr = htonl(INADDR_ANY);
    setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));

    // 然后正常调用 recvfrom() 接收多播数据

    10.3 非阻塞接收

    // 设置非阻塞模式
    int flags = fcntl(sockfd, F_GETFL, 0);
    fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

    // 在循环中检查是否有数据
    char buf[1024];
    struct sockaddr_in addr;
    socklen_t addrlen = sizeof(addr);
    ssize_t n = recvfrom(sockfd, buf, sizeof(buf), MSG_DONTWAIT, &addr, &addrlen);

    if (n == -1) {
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            // 没有数据可读
        } else {
            perror("recvfrom");
        }
    }

    11. 总结

    recvfrom() 是 UDP 网络编程的核心函数,具有以下关键特性:

  • 适用于无连接协议(主要是 UDP)

  • 每次接收都返回发送方地址信息

  • 保持数据报边界(每次调用接收一个独立数据报)

  • 需要处理缓冲区截断和错误情况

  • 性能受限于系统调用和缓冲区管理

  •         理解 recvfrom() 的工作原理对于编写高效可靠的网络应用程序至关重要,特别是在处理 UDP 协议时需要特别注意数据报完整性和缓冲区大小管理。

    3.3 缓冲区处理

    buffer[size] = '\\0'; // 确保字符串正确终止

    • 手动添加终止符,确保可以安全作为字符串处理

    • 防止缓冲区溢出(虽然BUFFER_SIZE-1已经限制了接收长度)

    4. 析构函数

    ~EchoClient() {
    close(_sockfd); // 关闭套接字
    }

    • 资源释放,避免文件描述符泄漏

    • 在对象销毁时自动调用

    5. 网络通信流程总结

  • 初始化阶段:

    • 创建UDP套接字

    • 设置服务器地址信息

  • 交互循环:

    • 读取用户输入

    • 发送消息到服务器

    • 接收服务器响应

    • 显示响应内容

  • 终止:程序退出时自动关闭套接字

  • 6. 代码特点分析

  • UDP协议特性:

    • 无连接:每次发送都需指定目标地址

    • 不可靠:没有连接状态,不保证送达

    • 简单高效:适合简单请求-响应模式

  • 健壮性考虑:

    • 检查套接字创建是否成功

    • 处理接收错误情况

    • 使用固定大小缓冲区防止溢出

  • 可改进点:

    • 没有超时机制,recvfrom可能无限阻塞

    • 缓冲区大小固定,长消息会被截断

    • 没有验证服务器身份(任何服务器响应都会显示)

  • 7. 关键系统调用和函数

    函数作用
    socket() 创建套接字
    sendto() 发送UDP数据报
    recvfrom() 接收UDP数据报
    inet_pton() IP地址字符串转二进制
    htons() 主机到网络字节序转换(16位)
    close() 关闭套接字

    8. 编译运行说明

            首先需要先启动对应端口的Echo服务器,也就是编译和运行服务端,然后对客户端进行同样的操作,然后测试回声服务器时,服务端和客户端都能观察到相应现象,从而可以准确判断通信状态,如下:

    9. 完整工作流程示例

  • 比如用户输入消息:"Hello, Server!"

  • 客户端发送到指定服务器和端口

  • 服务器接收并返回相同消息

  • 客户端显示:"Server response: Hello, Server!"

  • 循环继续,等待下一条输入

  • 这个简单的回声客户端展示了UDP网络编程的基本模式,可以作为更复杂网络应用的基础。


    四、网络测试与部署指南

    1、静态编译客户端

            你可以将生成的可执行程序分享给朋友进行网络测试。为确保程序在各台设备上完全一致,建议在编译客户端时添加-static参数进行静态编译。所以为了在不同机器上运行相同的客户端程序,建议进行静态编译:

    g++ EchoClient.cc -o EchoClient -static -std=c++11

    静态编译后的程序会包含所有依赖库,因此体积较大,但能确保在不同环境下的兼容性。

    2、程序分发方法

            你可以先通过sz命令将客户端可执行程序下载到本地,再分享给朋友使用。这类似于我们日常下载PC软件的过程:下载的实际上是客户端程序,而对应的服务端则部署在Linux服务器上运行。

    1. 从服务器下载客户端:

    sz EchoClient # 使用lrzsz工具下载到本地

    将客户端的可执行程序发送给朋友后,对方可以通过以下两种方式将其上传到云服务器:

  • 使用rz命令上传

  • 直接拖放文件到服务器

  • 上传完成后,执行chmod命令赋予文件可执行权限即可。

    2. 上传到目标机器:

    • 方法1:使用rz命令上传

      rz # 选择客户端程序上传

    • 方法2:直接拖拽(如果使用图形界面SSH工具)

    3. 添加执行权限:

    chmod +x client


    五、代码优化说明

    1、错误处理增强:

    • 添加了socket创建和绑定的错误检查

    • 改进了网络地址转换的错误处理

    2、代码结构优化:

    • 使用类封装服务器和客户端逻辑

    • 将网络地址处理与业务逻辑分离

    3、可移植性改进:

    • 使用inet_pton替代已弃用的inet_addr

    • 添加了-std=c++11编译选项确保兼容性

    4、缓冲区安全:

    • 确保所有字符串操作都以null结尾

    • 限制接收缓冲区大小防止溢出

    这个简易回声服务器实现完整展示了UDP通信的基本模式,可以作为网络编程学习的基础示例,也可以根据需要扩展为更复杂的协议实现。

    赞(0)
    未经允许不得转载:171主机测评 » 简易回声服务器实现与网络测试指南
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址