【Linux】UDP

【Linux】UDP

一、UDP核心特性与编程基础

1.1 UDP协议关键特性

  • 无连接:通信前无需建立连接,直接发送数据,减少连接开销。
  • 不可靠传输:不保证数据的到达顺序、完整性,可能丢失或重复。
  • 面向数据报:数据以"数据包"为单位传输,每个数据包有明确边界,不会粘包。
  • 全双工:单个Socket可同时读写,支持双向通信。

1.2 UDP核心编程API

UDP编程依赖4个核心系统调用,与TCP不同,无需listenaccept等连接相关接口:

接口 功能描述
socket() 创建UDP Socket,类型指定为SOCK_DGRAM
bind() 绑定IP和端口(服务器必须显式绑定,客户端通常由系统自动绑定随机端口)。
recvfrom() 接收数据,同时获取发送方的IP和端口。
sendto() 发送数据,需指定接收方的IP和端口。

1.3 关键问题:客户端需要显式bind吗?

  • 结论 :不需要。客户端首次调用sendto()时,操作系统会自动分配一个随机端口并绑定到Socket,避免端口冲突(多客户端同时连接时,随机端口保证唯一性)。
  • 服务器必须显式bind:服务器端口是"众所周知"的(如8080),客户端需通过固定端口找到服务器,因此必须手动绑定。

二、V1版本:基础Echo服务器(数据回显)

Echo服务器是最基础的UDP应用,核心功能是"接收客户端数据并原样返回",适合入门UDP编程流程。

2.1 核心代码结构

2.1.1 工具类:禁止拷贝(nocopy.hpp)

避免Socket对象被拷贝导致的资源泄漏:

cpp 复制代码
#pragma once
class nocopy {
public:
    nocopy() = default;
    nocopy(const nocopy&) = delete;  // 禁用拷贝构造
    nocopy& operator=(const nocopy&) = delete;  // 禁用赋值运算符
    ~nocopy() = default;
};
2.1.2 地址封装类(InetAddr.hpp)

封装struct sockaddr_in,简化IP和端口的转换与打印:

cpp 复制代码
#pragma once
#include <string>
#include <netinet/in.h>
#include <arpa/inet.h>

class InetAddr {
public:
    InetAddr(struct sockaddr_in& addr) : _addr(addr) {
        _port = ntohs(addr.sin_port);  // 网络字节序→主机字节序
        _ip = inet_ntoa(addr.sin_addr);  // in_addr→点分十进制字符串
    }

    std::string Ip() const { return _ip; }
    uint16_t Port() const { return _port; }
    // 格式化输出:IP:端口(如127.0.0.1:8888)
    std::string PrintDebug() const {
        return _ip + ":" + std::to_string(_port);
    }

private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};
2.1.3 UDP服务器类(UdpServer.hpp)
cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "nocopy.hpp"
#include "InetAddr.hpp"

const static uint16_t default_port = 8888;
const static int default_fd = -1;
const static int buffer_size = 1024;

class UdpServer : public nocopy {
public:
    UdpServer(uint16_t port = default_port) : _port(port), _sockfd(default_fd) {}

    // 初始化:创建Socket + 绑定端口
    void Init() {
        // 1. 创建UDP Socket
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0) {
            std::cerr << "socket error: " << strerror(errno) << std::endl;
            exit(1);
        }
        std::cout << "socket created, fd: " << _sockfd << std::endl;

        // 2. 绑定IP和端口
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);  // 主机字节序→网络字节序
        local.sin_addr.s_addr = INADDR_ANY;  // 绑定所有网卡IP

        int ret = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if (ret != 0) {
            std::cerr << "bind error: " << strerror(errno) << std::endl;
            exit(2);
        }
        std::cout << "bind success, port: " << _port << std::endl;
    }

    // 启动服务器:循环接收并回显数据
    void Start() {
        char buffer[buffer_size];
        while (true) {
            struct sockaddr_in peer;  // 存储发送方地址
            socklen_t peer_len = sizeof(peer);

            // 3. 接收数据(阻塞等待)
            ssize_t n = recvfrom(_sockfd, buffer, buffer_size - 1, 0,
                                (struct sockaddr*)&peer, &peer_len);
            if (n > 0) {
                buffer[n] = '\0';  // 手动添加字符串结束符
                InetAddr client_addr(peer);
                std::cout << "[" << client_addr.PrintDebug() << "]# " << buffer << std::endl;

                // 4. 回显数据(原样返回)
                sendto(_sockfd, buffer, strlen(buffer), 0,
                       (struct sockaddr*)&peer, peer_len);
            }
        }
    }

private:
    uint16_t _port;  // 服务器端口
    int _sockfd;     // Socket文件描述符
};
2.1.4 UDP客户端(UdpClient.cpp)
cpp 复制代码
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

void Usage(const std::string& process) {
    std::cout << "Usage: " << process << " server_ip server_port" << std::endl;
}

int main(int argc, char* argv[]) {
    if (argc != 3) {
        Usage(argv[0]);
        return 1;
    }

    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

    // 1. 创建UDP Socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        std::cerr << "socket error: " << strerror(errno) << std::endl;
        return 2;
    }

    // 2. 填充服务器地址信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());  // 字符串IP→网络字节序

    // 3. 循环发送并接收数据
    std::string input;
    while (true) {
        std::cout << "Please Enter# ";
        std::getline(std::cin, input);

        // 发送数据到服务器
        sendto(sockfd, input.c_str(), input.size(), 0,
               (struct sockaddr*)&server, sizeof(server));

        // 接收服务器回显数据
        char buffer[1024];
        struct sockaddr_in temp;
        socklen_t temp_len = sizeof(temp);
        ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
                            (struct sockaddr*)&temp, &temp_len);
        if (n > 0) {
            buffer[n] = '\0';
            std::cout << "server echo# " << buffer << std::endl;
        }
    }

    close(sockfd);
    return 0;
}

2.2 编译运行与核心要点

编译命令
bash 复制代码
# 编译服务器
g++ -o udp_server UdpServer.cpp -std=c++11
# 编译客户端
g++ -o udp_client UdpClient.cpp -std=c++11
运行步骤
  1. 启动服务器:./udp_server 8888
  2. 启动客户端:./udp_client 127.0.0.1 8888
  3. 客户端输入数据,服务器会原样返回。
关键要点
  • INADDR_ANY:绑定所有网卡IP,服务器可接收来自任意网卡的请求(如本地回环127.0.0.1、局域网IP 192.168.0.100)。
  • 字节序转换:端口和IP需通过htons()inet_addr()等函数转换为网络字节序(大端)。
  • recvfrom()peer_len参数:必须传入有效长度,否则可能导致内存错误。

三、V2版本:字典服务器(业务解耦)

在Echo服务器基础上,扩展为"英译汉字典"功能,核心是实现"网络层"与"业务层"的解耦,提高代码可维护性。

3.1 核心设计思路

  • 网络层:负责数据收发、地址解析,不关心业务逻辑。
  • 业务层:负责字典查询,通过回调函数与网络层交互。
  • 配置文件:字典数据存储在dict.txt,启动时加载到内存。

3.2 核心代码实现

3.2.1 字典类(Dict.hpp)

加载配置文件并提供翻译接口:

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>

const std::string sep = ": ";  // 字典分隔符(如"apple: 苹果")

class Dict {
public:
    Dict(const std::string& conf_path) : _conf_path(conf_path) {
        LoadDict();  // 初始化时加载字典
    }

    // 翻译接口:输入英文单词,返回中文释义
    std::string Translate(const std::string& word) {
        auto iter = _dict.find(word);
        return iter != _dict.end() ? iter->second : "Unknown word";
    }

private:
    // 加载字典文件到哈希表
    void LoadDict() {
        std::ifstream in(_conf_path);
        if (!in.is_open()) {
            std::cerr << "open dict file failed: " << _conf_path << std::endl;
            return;
        }

        std::string line;
        while (std::getline(in, line)) {
            if (line.empty()) continue;
            size_t pos = line.find(sep);
            if (pos == std::string::npos) continue;  // 跳过格式错误行
            std::string key = line.substr(0, pos);
            std::string value = line.substr(pos + sep.size());
            _dict.emplace(key, value);
        }

        in.close();
        std::cout << "dict loaded, size: " << _dict.size() << std::endl;
    }

private:
    std::string _conf_path;  // 字典文件路径
    std::unordered_map<std::string, std::string> _dict;  // 存储单词-释义映射
};
3.2.2 解耦后的UDP服务器(UdpServer.hpp)

通过函数对象(std::function)实现业务回调:

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "nocopy.hpp"
#include "InetAddr.hpp"

// 定义回调函数类型:输入请求,输出响应
using FuncType = std::function<void(const std::string& req, std::string* resp)>;

const static uint16_t default_port = 8888;
const static int default_fd = -1;
const static int buffer_size = 1024;

class UdpServer : public nocopy {
public:
    // 构造时传入业务回调函数
    UdpServer(FuncType func, uint16_t port = default_port)
        : _func(func), _port(port), _sockfd(default_fd) {}

    void Init() {
        // 与V1版本一致:创建Socket + 绑定端口
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0) {
            std::cerr << "socket error: " << strerror(errno) << std::endl;
            exit(1);
        }

        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;

        if (bind(_sockfd, (struct sockaddr*)&local, sizeof(local)) != 0) {
            std::cerr << "bind error: " << strerror(errno) << std::endl;
            exit(2);
        }
    }

    void Start() {
        char buffer[buffer_size];
        while (true) {
            struct sockaddr_in peer;
            socklen_t peer_len = sizeof(peer);

            // 接收客户端请求
            ssize_t n = recvfrom(_sockfd, buffer, buffer_size - 1, 0,
                                (struct sockaddr*)&peer, &peer_len);
            if (n > 0) {
                buffer[n] = '\0';
                InetAddr client_addr(peer);
                std::cout << "[" << client_addr.PrintDebug() << "]# " << buffer << std::endl;

                // 调用业务回调函数处理请求
                std::string resp;
                _func(buffer, &resp);

                // 发送响应给客户端
                sendto(_sockfd, resp.c_str(), resp.size(), 0,
                       (struct sockaddr*)&peer, peer_len);
            }
        }
    }

private:
    FuncType _func;    // 业务回调函数
    uint16_t _port;   // 服务器端口
    int _sockfd;      // Socket文件描述符
};
3.2.3 服务器主函数(Main.cpp)

初始化字典和服务器,绑定业务逻辑:

cpp 复制代码
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
#include "Dict.hpp"

// 全局字典对象(启动时加载)
Dict g_dict("./dict.txt");

// 业务处理函数:调用字典翻译
void TranslateFunc(const std::string& req, std::string* resp) {
    *resp = g_dict.Translate(req);
}

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

    uint16_t port = std::stoi(argv[1]);
    // 创建服务器,绑定业务回调函数
    std::unique_ptr<UdpServer> server = std::make_unique<UdpServer>(TranslateFunc, port);
    server->Init();
    server->Start();

    return 0;
}
3.2.4 字典配置文件(dict.txt)
txt 复制代码
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
happy: 快乐的
hello: 你好
goodbye: 再见

3.3 核心要点与扩展

运行效果

客户端输入英文单词(如apple),服务器返回中文释义(苹果);输入未知单词,返回Unknown word

关键亮点
  • 业务解耦:网络层与业务层通过回调函数分离,若需替换业务(如改为天气查询),只需修改回调函数,无需改动服务器核心代码。
  • 配置化设计:字典数据存储在文件中,无需修改代码即可更新词汇。
扩展方向
  • 支持中文到英文的反向翻译。
  • 增加单词联想功能(前缀匹配)。
  • 优化字典加载(支持大文件分片加载)。

四、V3版本:多人聊天室(消息转发+多线程)

聊天室是UDP的典型应用,核心功能是"消息转发"------服务器接收客户端消息后,广播给所有在线用户。需解决用户管理、多线程通信、消息路由等问题。

4.1 核心设计架构

  • 用户管理:维护在线用户列表(存储IP和端口)。
  • 消息路由:接收客户端消息,转发给所有在线用户。
  • 多线程客户端:客户端分"读线程"(接收广播消息)和"写线程"(发送消息),支持同时读写。
  • 线程池优化:服务器用线程池处理消息转发,提高并发能力。

4.2 核心代码实现

4.2.1 路由类(Route.hpp)

管理在线用户和消息转发逻辑:

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include "InetAddr.hpp"

class Route {
public:
    // 检查用户是否已在线
    bool IsOnline(const InetAddr& user) {
        for (const auto& u : _online_users) {
            if (u.Ip() == user.Ip() && u.Port() == user.Port()) {
                return true;
            }
        }
        return false;
    }

    // 添加在线用户(首次发消息即登录)
    void AddUser(const InetAddr& user) {
        if (!IsOnline(user)) {
            _online_users.push_back(user);
            std::cout << "user login: " << user.PrintDebug() << std::endl;
        }
    }

    // 移除在线用户(发送"QUIT"退出)
    void RemoveUser(const InetAddr& user) {
        for (auto iter = _online_users.begin(); iter != _online_users.end(); ++iter) {
            if (iter->Ip() == user.Ip() && iter->Port() == user.Port()) {
                _online_users.erase(iter);
                std::cout << "user logout: " << user.PrintDebug() << std::endl;
                break;
            }
        }
    }

    // 消息转发:向所有在线用户发送消息
    void ForwardMessage(int sockfd, const std::string& msg, const InetAddr& sender) {
        AddUser(sender);  // 未在线则登录

        // 构建消息:发送方IP:端口# 消息内容
        std::string broadcast_msg = sender.PrintDebug() + "# " + msg;

        // 转发给所有在线用户
        for (const auto& user : _online_users) {
            struct sockaddr_in addr;
            addr.sin_family = AF_INET;
            addr.sin_port = htons(user.Port());
            addr.sin_addr.s_addr = inet_addr(user.Ip().c_str());

            sendto(sockfd, broadcast_msg.c_str(), broadcast_msg.size(), 0,
                   (struct sockaddr*)&addr, sizeof(addr));
        }

        // 发送"QUIT"则退出
        if (msg == "QUIT") {
            RemoveUser(sender);
        }
    }

private:
    std::vector<InetAddr> _online_users;  // 在线用户列表
};
4.2.2 多线程客户端(UdpClient.cpp)

分读、写线程,支持同时接收和发送消息:

cpp 复制代码
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "InetAddr.hpp"

// 线程参数:Socket fd + 服务器地址
struct ThreadData {
    int sockfd;
    InetAddr server_addr;
};

// 读线程:接收服务器广播消息
void* RecvThread(void* arg) {
    ThreadData* data = (ThreadData*)arg;
    char buffer[4096];
    while (true) {
        struct sockaddr_in temp;
        socklen_t temp_len = sizeof(temp);
        ssize_t n = recvfrom(data->sockfd, buffer, sizeof(buffer) - 1, 0,
                            (struct sockaddr*)&temp, &temp_len);
        if (n > 0) {
            buffer[n] = '\0';
            std::cout << "\n" << buffer << std::endl;  // 打印广播消息
            std::cout << "Please Enter# ";
            std::flush(std::cout);  // 刷新输入提示
        }
    }
    return nullptr;
}

// 写线程:从标准输入发送消息
void* SendThread(void* arg) {
    ThreadData* data = (ThreadData*)arg;
    std::string input;
    while (true) {
        std::cout << "Please Enter# ";
        std::getline(std::cin, input);

        // 发送消息到服务器
        struct sockaddr_in server_addr;
        server_addr.sin_family = AF_INET;
        server_addr.sin_port = htons(data->server_addr.Port());
        server_addr.sin_addr.s_addr = inet_addr(data->server_addr.Ip().c_str());

        sendto(data->sockfd, input.c_str(), input.size(), 0,
               (struct sockaddr*)&server_addr, sizeof(server_addr));

        // 输入"QUIT"退出
        if (input == "QUIT") {
            break;
        }
    }
    close(data->sockfd);
    return nullptr;
}

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

    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

    // 创建UDP Socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        std::cerr << "socket error: " << strerror(errno) << std::endl;
        return 2;
    }

    // 初始化服务器地址
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());
    InetAddr server_addr(server);

    // 初始化线程参数
    ThreadData data;
    data.sockfd = sockfd;
    data.server_addr = server_addr;

    // 创建读、写线程
    pthread_t recv_tid, send_tid;
    pthread_create(&recv_tid, nullptr, RecvThread, &data);
    pthread_create(&send_tid, nullptr, SendThread, &data);

    // 等待线程结束
    pthread_join(send_tid, nullptr);
    pthread_cancel(recv_tid);  // 写线程退出后,终止读线程

    return 0;
}
4.2.3 线程池优化的服务器(Main.cpp)

结合之前实现的线程池,处理消息转发任务:

cpp 复制代码
#include <iostream>
#include <memory>
#include <functional>
#include "UdpServer.hpp"
#include "Route.hpp"
#include "ThreadPool.hpp"  // 复用之前实现的线程池

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

    uint16_t port = std::stoi(argv[1]);
    auto route = std::make_unique<Route>();
    auto thread_pool = ThreadPool<std::function<void()>>::GetInstance();

    // 业务回调:将消息转发任务提交到线程池
    auto forward_func = [&](const std::string& msg, std::string* resp) {
        (void)resp;  // 忽略响应(聊天室无需单独回复)
        // 获取发送方地址(需修改UdpServer,传递InetAddr参数)
        // 此处简化:实际需在UdpServer中扩展,传递sender地址
        InetAddr sender;  // 实际应从recvfrom获取
        thread_pool->Enqueue(std::bind(&Route::ForwardMessage, route.get(), _sockfd, msg, sender));
    };

    // 启动服务器
    std::unique_ptr<UdpServer> server = std::make_unique<UdpServer>(forward_func, port);
    server->Init();
    server->Start();

    return 0;
}

4.3 核心要点与运行效果

运行步骤
  1. 启动聊天室服务器:./chat_server 8888
  2. 启动多个客户端:./chat_client 127.0.0.1 8888
  3. 任意客户端输入消息,所有客户端都会收到广播;输入QUIT退出。
关键技术点
  • UDP全双工:单个Socket同时支持读和写,多线程操作无冲突。
  • 用户管理 :通过IP+端口唯一标识用户,首次发消息自动登录,发送QUIT自动退出。
  • 线程池优化:消息转发任务提交到线程池,避免单线程阻塞,提高并发处理能力。

五、UDP编程关键知识点补充

5.1 地址转换函数(避免线程安全问题)

UDP编程中常用IP地址转换函数,需注意线程安全:

函数 功能描述 线程安全
inet_addr() 点分十进制字符串→in_addr_t(网络字节序)
inet_ntoa() in_addr→点分十进制字符串 否(静态缓冲区覆盖)
inet_pton() 支持IPv4/IPv6,字符串→二进制地址
inet_ntop() 支持IPv4/IPv6,二进制地址→字符串 是(用户提供缓冲区)

推荐使用线程安全函数

cpp 复制代码
// inet_pton + inet_ntop 示例
#include <arpa/inet.h>

int main() {
    const char* ip_str = "192.168.0.1";
    struct in_addr addr;

    // 字符串→二进制地址
    inet_pton(AF_INET, ip_str, &addr);

    // 二进制地址→字符串(线程安全)
    char buf[INET_ADDRSTRLEN];
    const char* res = inet_ntop(AF_INET, &addr, buf, sizeof(buf));
    std::cout << "ip: " << res << std::endl;
    return 0;
}

5.2 UDP常见问题与解决方案

问题 原因 解决方案
数据丢失 UDP不可靠传输,网络拥堵或接收缓冲区满 应用层添加确认机制(如ACK)、重传策略
端口冲突 服务器绑定已被占用的端口 选择未使用的端口,或检测端口占用后自动重试
消息乱序 数据包路由路径不同,到达顺序不一致 应用层添加序列号,接收端按序列号重组
缓冲区溢出 接收方处理速度慢,缓冲区被填满 增大接收缓冲区(setsockopt),优化处理逻辑

六、总结与进阶方向

UDP编程的核心是"简单高效",适合对实时性要求高、可容忍少量数据丢失的场景。

进阶学习方向

  1. 应用层可靠性优化:实现基于UDP的可靠传输(如添加序列号、确认、重传)。
  2. 广播与组播 :学习UDP广播(SO_BROADCAST)和组播(IP_ADD_MEMBERSHIP)。
  3. 性能优化 :调整Socket缓冲区大小、使用IO多路复用(select/poll/epoll)处理高并发。
  4. 跨平台兼容 :适配Windows系统的UDP编程接口(WSASocket等)。
相关推荐
FoldWinCard8 小时前
D5 Linux 网络及端口命令
linux·运维·服务器
零涂毕业设计10 小时前
毕业设计模块开发-OLED显示屏(IIC协议0.96寸)STM32 ESP32 FPGA Linux驱动免费分享
linux·stm32·单片机·嵌入式·esp32·fpga·oled
tedcloud12310 小时前
OmniRoute怎么部署?开源AI模型路由平台Linux部署教程
linux·服务器·人工智能·开源·音视频
霸道流氓气质10 小时前
KMS 密钥管理服务(Key Management Service)原理与实践
linux·服务器·数据库
SakitamaX10 小时前
LVS(Linux Virtual Server)概念详解
linux·运维·lvs
小张同学a.10 小时前
Linux系统管理
linux·运维
阿龙的工作记录11 小时前
深入理解Linux设备驱动模型
linux·嵌入式硬件·工作记录
代码村新手11 小时前
Linux的基本指令
linux·运维·服务器
hkj880811 小时前
Ubuntu 切换java版本
java·linux·ubuntu
撩得Android一次心动13 小时前
Linux编程笔记3【个人用】
linux·笔记