Socket编程UDP

Socket 编程-UDP

文章目录

    • [Socket 编程-UDP](#Socket 编程-UDP)
      • [1. Version1-Echo Server](#1. Version1-Echo Server)
        • [1.1 服务端代码](#1.1 服务端代码)
        • [1.2 客户端代码](#1.2 客户端代码)
        • [1.3 测试](#1.3 测试)
      • [2. Version2-Dictionary Server](#2. Version2-Dictionary Server)
        • [2.1 词典](#2.1 词典)
        • [2.2 词典服务器代码](#2.2 词典服务器代码)
        • [2.3 词典客户端实现](#2.3 词典客户端实现)
        • [2.4 测试](#2.4 测试)
      • [3. Version3-简单聊天室](#3. Version3-简单聊天室)
        • [3.1 路由模块实现](#3.1 路由模块实现)
        • [3.2 聊天室服务器代码](#3.2 聊天室服务器代码)
        • [3.3 聊天室客户端代码](#3.3 聊天室客户端代码)
        • [3.4 测试](#3.4 测试)
      • [4. 总结](#4. 总结)

1. Version1-Echo Server

实现一个简单的回显服务器和客户端程序

1.1 服务端代码

UdpServer.hpp

cpp 复制代码
#pragma once

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
#include <string>
#include <functional>
#include "Log.hpp"  // 之前编写的日志模块

using namespace LogModule;

class UdpServer
{
    int default_file_descriptor = -1;
    using func_t = std::function<std::string(void *)>;

public:
    UdpServer(uint16_t port, func_t func)
        : _socket_fd(default_file_descriptor),
          _port(port),
          _is_running(false),
          _func(func)
    {
    }
    ~UdpServer(){}
    // 初始化工作
    void Initial()
    {
        // 1. 创建套接字-得到socket文件描述符
        _socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_socket_fd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket create success, socket file descriptor: " << _socket_fd;

        // 2. 填充server的sockaddr_in信息
        struct sockaddr_in server_info;
        bzero(&server_info, sizeof(server_info)); // 清空sockaddr_in中原有的内容
        server_info.sin_port = htons(_port);      // 填充port
        server_info.sin_family = AF_INET;         // IPv4的地址族
        // server通常会有多张网卡,如果绑定某个指定的IP地址,那么client发送到其它IP地址的数据就无法接收
        // ./server 8080 192.168.2.203,那么server就只能接收来自192.168.2.203的消息,甚至连127.0.0.1也不能接收
        server_info.sin_addr.s_addr = INADDR_ANY; // 填充IP地址(在sin_addr中的s_addr中)

        // 3. 绑定server的IP和port
        // server需要绑定,因为server是提供服务方,要有一个固定的IP+port持续监听client发送来的数据
        int ret = bind(_socket_fd, (struct sockaddr *)&server_info, sizeof(server_info));
        if (ret < 0)
        {
            LOG(LogLevel::FATAL) << "bind error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "bind success, bind file descriptor: " << _socket_fd;
    }
    // 启动UDP服务
    void Start()
    {
        // 修改标志位
        _is_running = true;
        // 一直运行,直到主动退出-长服务
        while (_is_running)
        {
            // server收到client发送的消息要进行echo-再发一次给client
            // 1. 接收从client发来的消息
            char buffer[1024];
            struct sockaddr_in client_info; // client的信息
            socklen_t len = sizeof(client_info);
            ssize_t receive_size = recvfrom(_socket_fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client_info, &len);
            if (receive_size < 0)
            {
                LOG(LogLevel::ERROR) << "recvfrom failed";
            }
            else
            {
                buffer[receive_size] = '\0'; // 保证消息完整性
                uint16_t peer_port = static_cast<uint16_t>(htons(client_info.sin_port));
                std::string peer_ip = inet_ntoa(client_info.sin_addr);

                std::cout << "Received a message with the IP address " << peer_ip << " via port " << peer_port << ":" << std::endl;
                // 回调处理 - 将具体的处理逻辑交给外部,内部专注于连接
                std::string echo_message = _func(buffer);

                // 2. 将消息发回给client
                ssize_t send_size = sendto(_socket_fd, echo_message.c_str(), echo_message.size(), 0, (struct sockaddr *)&client_info, len);
                if (send_size < 0)
                {
                    LOG(LogLevel::ERROR) << "sendto failed";
                }
            }
        }
    }

private:
    int _socket_fd;   // 打开socket的文件描述符
    uint16_t _port;   // 要绑定的port
    bool _is_running; // 运行标志位
    func_t _func;     // 外部回调函数
};

UdpServer.cpp

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

std::string Task(void *args)
{
    std::cout << "Start processing" << std::endl;
    std::string message("Server-side echo \"");
    message += static_cast<char *>(args);
    message += "\"";
    std::cout << "Processing complete" << std::endl;
    return message;
}

// ./udp_server 8080 127.0.0.1
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cout << "Use it like this: " << argv[0] << " port" << std::endl;
        return 1;
    }
    // uint16_t port = *argv[1];    // 字符类型->uint16_t
    uint16_t port = static_cast<uint16_t>(std::atoi(argv[1]));
    // std::string ip = argv[2];
    std::unique_ptr<UdpServer> udp_server = std::make_unique<UdpServer>(UdpServer(port, Task));
    udp_server->Initial();
    udp_server->Start();

    return 0;
}
1.2 客户端代码

UdpClient.hpp

cpp 复制代码
#include <iostream>
#include <string>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include "Log.hpp"

using namespace LogModule;

class UdpClient
{
    int default_file_descriptor = -1;

public:
    UdpClient(uint16_t port, std::string ip)
        : _socket_fd(default_file_descriptor),
          _server_port(port),
          _server_ip(ip)
    {
    }
    void Initial()
    {
        // 1. 获取socket文件描述符
        _socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_socket_fd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket create success, socket file descriptor: " << _socket_fd;
        // 2. 绑定IP和端口
        // 不同client程序可以使用相同的源端口连接到不同的服务器
        // 因为连接是由(源IP,源端口,目标IP,目标端口)四元组唯一确定的
        // 如果手动绑定,就可能会出现端口冲突的问题
        // client只需要知道server的IP+port就可以发送数据-不用考虑通过哪个port发送(由操作系统自动分配)-也不用绑定IP(由操作系统选择合适的网卡发送)
    }
    void Start()
    {
        // 用于填充server信息
        struct sockaddr_in server_info;
        memset(&server_info, 0, sizeof(server_info)); // 清空
        server_info.sin_family = AF_INET;
        server_info.sin_port = htons(_server_port);
        server_info.sin_addr.s_addr = inet_addr(_server_ip.c_str());
        // client也是一样的,不主动退出就一直运行
        while (true)
        {
            std::string message;
            message.clear();
            std::cout << "Please enter# ";
            std::getline(std::cin, message); // C++版的getline
            socklen_t len_send = sizeof(server_info);
            // 1. 发送消息给server
            ssize_t send_size = sendto(_socket_fd, message.c_str(), message.size(), 0, (struct sockaddr *)&server_info, len_send);
            if (send_size < 0)
            {
                LOG(LogLevel::ERROR) << "sendto failed";
            }
            // 2. 接收server发回来的消息
            char buffer[1024];
            struct sockaddr_in receive_info; // server的信息
            socklen_t len_receive = sizeof(receive_info);
            ssize_t receive_size = recvfrom(_socket_fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&receive_info, &len_receive);
            if (receive_size < 0)
            {
                LOG(LogLevel::ERROR) << "recvfrom failed";
            }
            buffer[receive_size] = '\0';
            std::cout << buffer << std::endl;
        }
    }
    ~UdpClient() {}

private:
    int _socket_fd;
    uint16_t _server_port;
    std::string _server_ip;
};

UdpClient.cpp

c 复制代码
#include <iostream>
#include <string>
#include <memory>
#include "UdpClient.hpp"
#include "Log.hpp"

// ./udp_client 8080 127.0.0.1
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "Use it like this: " << argv[0] << " port " << "ip" << std::endl;
        return 1;
    }
    uint16_t port = static_cast<uint16_t>(std::atoi(argv[1]));
    std::string ip = argv[2];
    std::unique_ptr<UdpClient> udp_client = std::make_unique<UdpClient>(UdpClient(port, ip));
    udp_client->Initial();
    udp_client->Start();
    return 0;
}
1.3 测试

启动服务端:

bash 复制代码
$ ./udp_server 8080
[2025-12-13 19:25:27] [INFO] [UdpServer.hpp] [40] - socket create success, socket file descriptor: 3
[2025-12-13 19:25:27] [INFO] [UdpServer.hpp] [60] - bind success, bind file descriptor: 3
Received a message with the IP address 127.0.0.1 via port 51939:
Start processing
Processing complete
Received a message with the IP address 127.0.0.1 via port 51939:
Start processing
Processing complete
Received a message with the IP address 127.0.0.1 via port 51939:
Start processing
Processing complete
^C
 130 $ 

启动客户端:

bash 复制代码
$ ./udp_client 8080 127.0.0.1
[2025-12-13 19:26:07] [INFO] [UdpClient.hpp] [30] - socket create success, socket file descriptor: 3
Please enter# 你好
Server-side echo "你好"
Please enter# Hello
Server-side echo "Hello"
Please enter# Goodbye
Server-side echo "Goodbye"
Please enter# ^C
 130 $ 

可以看到,客户端发送的消息都被服务端正确接收并处理,随后服务端将处理结果回传给客户端,客户端也正确接收并显示了回显内容。UDP 通信实现成功!

期中,服务器通常不绑定特定 IP 地址,而是绑定到 INADDR_ANY,以便监听所有可用的网络接口。这种方式确保服务器能够接收发送到任何本地 IP 地址的数据包,从而提高了服务的可用性和灵活性。

INADDR_ANY 是一个特殊的 IP 地址,表示"所有可用的接口"。当服务器绑定到INADDR_ANY 时,它实际上是在告诉操作系统监听所有网络接口上的数据包,而不仅仅是某个特定的 IP 地址。这对于服务器来说是非常有用的,因为它允许服务器接收来自任何网络接口的数据,而不需要事先知道客户端将连接到哪个具体的 IP 地址。

其具体定义其实就是 0 转换而来:

c 复制代码
/* Address to accept any incoming messages.  */
#define	INADDR_ANY		((in_addr_t) 0x00000000)

2. Version2-Dictionary Server

实现一个简单的英译汉网络词典

2.1 词典

DictServer.hpp

cpp 复制代码
#pragma once

#include <string>
#include <map>
#include <fstream>
#include "Log.hpp"

using namespace LogModule;

#define SEP " "

std::string default_dict_file = "./dictionary.txt";

class Dict
{
public:
    Dict(std::string path = default_dict_file)
        : _dict_file(path)
    {
    }
    bool Load()
    {
        std::fstream dict_file;
        dict_file.open(_dict_file, std::ios::in);
        if (!dict_file.is_open())
        {
            LOG(LogLevel::ERROR) << "Open " << _dict_file.c_str() << " failed";
            return false;
        }
        std::string line;
        ssize_t line_number = 0; // 字典行号计数器
        while (std::getline(dict_file, line))
        {
            ++line_number;
            // abandon  v.抛弃,放弃
            std::string english;
            std::string info;

            auto pos_forward = line.find(SEP);
            auto pos_reverse = line.rfind(SEP);

            english = line.substr(0, pos_forward);
            info = line.substr(pos_reverse + 1, line.size());

            // LOG(LogLevel::DEBUG) << "English: " << english << ", explain: " << info;

            if (english.empty() || info.empty())
            {
                LOG(LogLevel::ERROR) << _dict_file << " line " << line_number
                                     << (english.empty() ? " no english content" : "no explain information")
                                     << "[RAW-LINE - " << line << "]";
            }

            _dict.emplace(std::make_pair(english, info));
        }

        dict_file.close();
        LOG(LogLevel::INFO) << "Load complete";
        return true;
    }
    std::string Translate(const std::string &word)
    {
        LOG(LogLevel::INFO) << "Request for translation services";
        return _dict[word];
    }
    ~Dict() {}

private:
    std::string _dict_file;
    std::map<std::string, std::string> _dict;
};
2.2 词典服务器代码

每次手动写 inet* 地址结构和进行地址信息太麻烦了,封装一个地址结构,转换函数 InetAddr.hpp

cpp 复制代码
#pragma once

#include <sys/socket.h>
#include <arpa/inet.h>
#include <string>

class Addr
{
public:
    Addr(struct sockaddr_in addr)
        : _raw_addr(addr),
          _port(static_cast<uint16_t>(htons(addr.sin_port))),
          _ip(inet_ntoa(addr.sin_addr))
    {
    }
    ~Addr() {}
    uint16_t GetPort() { return _port; }
    std::string GetIp() { return _ip; }
    struct sockaddr_in GetRawAddr() { return _raw_addr; }

private:
    struct sockaddr_in _raw_addr; // 原生地址结构
    uint16_t _port;               // 端口
    std::string _ip;              // IP地址信息
};

UdpServer.hpp

cpp 复制代码
#pragma once

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
#include <string>
#include <functional>
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace LogModule;

class UdpServer
{
    int default_file_descriptor = -1;
    using func_t = std::function<std::string(std::string)>;

public:
    UdpServer(uint16_t port, func_t func)
        : _socket_fd(default_file_descriptor),
          _port(port),
          //   _ip(ip),
          _is_running(false),
          _func(func)
    {
    }
    ~UdpServer()
    {
    }
    // 初始化工作
    void Initial()
    {
        // 1. 创建套接字-得到socket文件描述符
        _socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_socket_fd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket create success, socket file descriptor: " << _socket_fd;

        // 填充server的sockaddr_in信息
        struct sockaddr_in server_info;
        bzero(&server_info, sizeof(server_info)); // 清空sockaddr_in中原有的内容
        server_info.sin_port = htons(_port);      // 填充port
        server_info.sin_family = AF_INET;         // IPv4的地址族
        // server_info.sin_addr.s_addr = inet_addr(_ip.c_str()); // 填充IP地址(在sin_addr中的s_addr中)
        // server通常会有多张网卡,如果绑定某个指定的IP地址,那么client发送到其它IP地址的数据就无法接收
        server_info.sin_addr.s_addr = INADDR_ANY; // 填充IP地址(在sin_addr中的s_addr中)

        // 2. 绑定server的IP和port
        // server需要绑定,因为server是提供服务方,要有一个固定的IP+port持续监听client发送来的数据
        int ret = bind(_socket_fd, (struct sockaddr *)&server_info, sizeof(server_info));
        if (ret < 0)
        {
            LOG(LogLevel::FATAL) << "bind error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "bind success, bind file descriptor: " << _socket_fd;
    }
    // 启动UDP服务
    void Start()
    {
        // 修改标志位
        _is_running = true;
        // 一直运行,直到主动退出
        while (_is_running)
        {
            // server收到client发送的消息要进行echo-再发一次给client
            // 1. 接收从client发来的消息
            char word[1024];
            struct sockaddr_in client_info; // client的信息
            socklen_t len = sizeof(client_info);
            ssize_t receive_size = recvfrom(_socket_fd, word, sizeof(word) - 1, 0, (struct sockaddr *)&client_info, &len);
            if (receive_size < 0)
            {
                LOG(LogLevel::ERROR) << "recvfrom failed";
            }
            else
            {
                // 每次发来的就是一个单词
                word[receive_size] = '\0'; // 保证消息完整性

                // 封装成为一个类
                Addr info(client_info);
                // uint16_t peer_port = static_cast<uint16_t>(htons(client_info.sin_port));
                // std::string peer_ip = inet_ntoa(client_info.sin_addr);

                LOG(LogLevel::INFO) << "Received a word with the IP address " << info.GetIp() << " via port " << info.GetPort();
                // 回调处理 -> 不仅仅是回调,控制传出的参数 -> 外部需要的参数读通过这里传出
                std::string result = _func(word);
                if (result.empty())
                    result = "No such word found";

                // 将处理逻辑交给外部,内部专注于连接
                // std::string echo_message("Server-side echo \"");
                // echo_message += buffer;
                // echo_message += "\"";

                // 2. 将消息发回给client
                ssize_t send_size = sendto(_socket_fd, result.c_str(), result.size(), 0, (struct sockaddr *)&client_info, len);
                if (send_size < 0)
                {
                    LOG(LogLevel::ERROR) << "sendto failed";
                }
            }
        }
    }

private:
    int _socket_fd; // 打开socket的文件描述符
    uint16_t _port; // 要绑定的port
    bool _is_running; // 运行标志位
    func_t _func;     // 外部回调函数
};

UdpServer.cc

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

// ./udp_server 8080
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cout << "Use it like this: " << argv[0] << " port" << std::endl;
        return 1;
    }
    uint16_t port = static_cast<uint16_t>(std::atoi(argv[1]));
    
    std::unique_ptr<Dict> dict = std::make_unique<Dict>();
    dict->Load();
    
    // 模块的解耦合,从网络通信模块跳转到翻译模块
    std::unique_ptr<UdpServer> udp_server =
        std::make_unique<UdpServer>(UdpServer(port, [&dict](std::string word) -> std::string
                                              { return dict->Translate(word); }));
    udp_server->Initial();
    udp_server->Start();

    return 0;
}
2.3 词典客户端实现

UdpClient.hpp

cpp 复制代码
#include <iostream>
#include <string>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include "Log.hpp"

using namespace LogModule;

class UdpClient
{
    int default_file_descriptor = -1;

public:
    UdpClient(uint16_t port, std::string ip)
        : _socket_fd(default_file_descriptor),
          _server_port(port),
          _server_ip(ip)
    {
    }
    void Initial()
    {
        // 1. 获取socket文件描述符
        _socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_socket_fd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket create success, socket file descriptor: " << _socket_fd;
    }
    void Start()
    {
        // 填充server信息
        struct sockaddr_in server_info;
        memset(&server_info, 0, sizeof(server_info));
        server_info.sin_family = AF_INET;
        server_info.sin_port = htons(_server_port);
        server_info.sin_addr.s_addr = inet_addr(_server_ip.c_str());
        while (true)
        {
            std::string message;
            message.clear();
            std::cout << "Please enter# ";
            std::getline(std::cin, message); // C++版的getline
            socklen_t len_send = sizeof(server_info);
            // 1. 发送消息给server
            ssize_t send_size = sendto(_socket_fd, message.c_str(), message.size(), 0, (struct sockaddr *)&server_info, len_send);
            if (send_size < 0)
            {
                LOG(LogLevel::ERROR) << "sendto failed";
            }
            // 2. 接收server发回来的消息
            char buffer[1024];
            struct sockaddr_in receive_info; // server的信息
            socklen_t len_receive = sizeof(receive_info);
            ssize_t receive_size = recvfrom(_socket_fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&receive_info, &len_receive);
            if (receive_size < 0)
            {
                LOG(LogLevel::ERROR) << "recvfrom failed";
            }
            buffer[receive_size] = '\0';
            std::cout << buffer << std::endl;
        }
    }
    ~UdpClient() {}

private:
    int _socket_fd;
    uint16_t _server_port;
    std::string _server_ip;
};

UdpClient.cpp

cpp 复制代码
#include <iostream>
#include <string>
#include <memory>
#include "UdpClient.hpp"
#include "Log.hpp"

// ./udp_client 8080 127.0.0.1
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "Use it like this: " << argv[0] << " port " << "ip" << std::endl;
        return 1;
    }
    uint16_t port = static_cast<uint16_t>(std::atoi(argv[1]));
    std::string ip = argv[2];
    std::unique_ptr<UdpClient> udp_client = std::make_unique<UdpClient>(UdpClient(port, ip));
    udp_client->Initial();
    udp_client->Start();
    return 0;
}
2.4 测试

词典文件 dictionary.txt 内容如下:

txt 复制代码
abandon  v.抛弃,放弃
abandonment  n.放弃
abbreviation  n.缩写
abeyance  n.缓办,中止
abide  v.遵守
......

启动词典服务器:

bash 复制代码
$./udp_server 8080
[2025-12-13 19:59:28] [INFO] [Dictionary.hpp] [58] - Load complete
[2025-12-13 19:59:28] [INFO] [UdpServer.hpp] [41] - socket create success, socket file descriptor: 3
[2025-12-13 19:59:28] [INFO] [UdpServer.hpp] [60] - bind success, bind file descriptor: 3
[2025-12-13 19:59:41] [INFO] [UdpServer.hpp] [90] - Received a word with the IP address 127.0.0.1 via port 50828
[2025-12-13 19:59:41] [INFO] [Dictionary.hpp] [63] - Request for translation services
[2025-12-13 19:59:43] [INFO] [UdpServer.hpp] [90] - Received a word with the IP address 127.0.0.1 via port 50828
[2025-12-13 19:59:43] [INFO] [Dictionary.hpp] [63] - Request for translation services
[2025-12-13 19:59:52] [INFO] [UdpServer.hpp] [90] - Received a word with the IP address 127.0.0.1 via port 50828
[2025-12-13 19:59:52] [INFO] [Dictionary.hpp] [63] - Request for translation services
[2025-12-13 20:00:00] [INFO] [UdpServer.hpp] [90] - Received a word with the IP address 127.0.0.1 via port 50828
[2025-12-13 20:00:00] [INFO] [Dictionary.hpp] [63] - Request for translation services
^C
 130 $

启动词典客户端:

bash 复制代码
[2025-12-13 19:59:40] [INFO] [UdpClient.hpp] [30] - socket create success, socket file descriptor: 3
Please enter# good
adj.好的,善良的
Please enter# evil
adj.坏的,邪恶的
Please enter# goodbye
No such word found
Please enter# nhtg
No such word found
Please enter# ^C
 130 $

可以看到,客户端成功发送单词到服务器,服务器查询词典并返回翻译结果,客户端正确接收并显示了翻译内容。英译汉网络词典实现成功!

3. Version3-简单聊天室

实现一个简单的 UDP 聊天室程序

3.1 路由模块实现

Router.hpp

cpp 复制代码
#pragma once

#include <vector>
#include <string>
#include "InetAddr.hpp"
#include "Log.hpp"
#include "Lock.hpp" // 之前编写的互斥锁模块

using namespace LogModule;
using namespace MutexModule;

class Route
{
private:
    bool IsExit(const Identifier &info)
    {
        for (auto &user : _online_users)
        {
            if (user == info)
            {
                return true;
            }
        }
        return false;
    }
    void AddUser(const Identifier &info)
    {
        _online_users.push_back(info);
    }
    void DeleteUser(const Identifier &info)
    {
        std::vector<Identifier>::iterator first = _online_users.begin();
        while (first != _online_users.end())
        {
            if (*first == info)
            {
                LOG(LogLevel::INFO) << info.GetIp() << " has logged out";
                first = _online_users.erase(first); // 注意迭代器失效问题
                break;
            }
            else
            {
                ++first;
            }
        }
    }

public:
    Route() {}
    // 目标socket文件,发送内容,发送对象信息
    void ForwardMessage(int sock_fd, const std::string message, const Identifier info)
    {
        // 1、2、3 在多线程场景下是并发执行的,存在并发问题-一个用户已经退了,但是消息广播到房间的时候没有更新
        // 简单处理-TODO -> 采用将消息封装(广播信息、退出信息、更新用户信息)的方式做封装
        LockGuard lockguard(_mutex);
        // 1. 判断用户是否在房间(无登录策略,用户发消息就代表上线)
        if (!IsExit(info))
        {
            AddUser(info);
            // A user from 127.0.0.1[8080] is online
            LOG(LogLevel::INFO) << "A user from " << info.GetIp() << "[" << info.GetPort() << "] " << "is online";
        }
        // 2. 将消息发到房间
        for (auto &user : _online_users)
        {
            socklen_t len = user.Size();
            // 对消息再做一次封装
            std::string send_message = info.GetIp() + " via ";
            send_message += std::to_string(info.GetPort());
            send_message += " say>>> ";
            send_message += message;
            // 发送给所有在线用户
            ssize_t send_size = sendto(sock_fd, send_message.c_str(), send_message.size(), 0, (struct sockaddr *)&user.GetRawAddr(), len);
            if (send_size < 0)
            {
                LOG(LogLevel::ERROR) << "sendto failed";
            }
        }
        // 3. 检查用户是否退出
        if (message == "QUIT")
        {
            DeleteUser(info);
        }
    }
    ~Route() {}

private:
    // STL容器本身就不是线程安全的,所以如果存在多线程同时进行访问_online_users就会出现并发问题
    std::vector<Identifier> _online_users; // 在线用户
    Mutex _mutex;
};
3.2 聊天室服务器代码

更新转换函数 InetAddr.hpp

cpp 复制代码
#pragma once

#include <sys/socket.h>
#include <arpa/inet.h>
#include "Log.hpp"

using namespace LogModule;

// Identifier -> 内部传递参数(IP+Port)使用
class Identifier
{
public:
    Identifier(struct sockaddr_in addr) : _raw_addr(addr)
    {
        // 网络转主机
        _port = ntohs(addr.sin_port);
        char ip_buffer[64];
        memset(ip_buffer, 0, sizeof(ip_buffer));
        inet_ntop(AF_INET, &_raw_addr.sin_addr, ip_buffer, sizeof(ip_buffer)); // 更加安全的做法
        _ip = (ip_buffer == nullptr ? 0 : ip_buffer);
        if (_ip.empty())
            LOG(LogLevel::ERROR) << "inet_ntop failed";
    }
    Identifier(std::string ip, uint16_t port) : _ip(ip), _port(port)
    {
        // 主机转网络
        memset(&_raw_addr, 0, sizeof(_raw_addr));
        _raw_addr.sin_family = AF_INET;
        _raw_addr.sin_port = htons(_port);
        if (ip == "0")
        {
            _ip = "0.0.0.0";
            _raw_addr.sin_addr.s_addr = INADDR_ANY; // 处理特殊情况 -> 避免报invalid format错误
        }
        else
        {
            int ret = inet_pton(AF_INET, _ip.c_str(), &_raw_addr.sin_addr); // 更加安全的做法
            if (ret != 1)
                LOG(LogLevel::ERROR) << (ret == 0 ? "invalid format" : "inet_pton failed");
        }
    }

    ~Identifier() {}
    bool operator==(const Identifier info) const
    {
        // 1. 仅以IP作为唯一性标识=
        // 2. IP + Port作为唯一性标识
        return (info._ip == _ip) && (info._port == _port);
    }
    uint16_t GetPort() const { return _port; }
    std::string GetIp() const { return _ip; }
    struct sockaddr_in &GetRawAddr() { return _raw_addr; }
    socklen_t Size() const
    {
        return sizeof(_raw_addr);
    }

private:
    struct sockaddr_in _raw_addr; // 原始地址结构
    uint16_t _port;               // 端口
    std::string _ip;              // IP地址信息
};

UdpServer.hpp

cpp 复制代码
#pragma once

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
#include <string>
#include <functional>
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace LogModule;

class UdpServer
{
    int default_file_descriptor = -1;
    using func_t = std::function<void(int socket_fd, const std::string &str, const Identifier &info)>; // 回调函数

public:
    UdpServer(uint16_t port, func_t func)
        : _socket_fd(default_file_descriptor),
          _port(port),
          _is_running(false),
          _func(func)
    {
    }
    ~UdpServer()
    {
    }
    // 初始化工作
    void Initial()
    {
        // 1. 创建套接字-得到socket文件描述符
        _socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_socket_fd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket create success, socket file descriptor: " << _socket_fd;

        // 2. 填充server的sockaddr_in信息
        Identifier server_info("0", _port); // √

        // 3. 绑定server的IP和port
        // server需要绑定,因为server是提供服务方,要有一个固定的IP+port持续监听client发送来的数据
        int ret = bind(_socket_fd, (struct sockaddr *)&server_info.GetRawAddr(), sizeof(server_info.GetRawAddr()));
        if (ret < 0)
        {
            LOG(LogLevel::FATAL) << "bind error!";
            exit(1);
        }

        LOG(LogLevel::INFO) << "bind success, bind file descriptor: " << _socket_fd;
    }
    // 启动Server服务
    void Start()
    {
        _is_running = true;
        // 一直运行,直到主动退出
        while (_is_running)
        {
            char message[1024];
            struct sockaddr_in client_info; // 用于保存client的信息
            socklen_t len = sizeof(client_info);

            // 1. 接收来自client的消息
            ssize_t receive_size = recvfrom(_socket_fd, message, sizeof(message) - 1, 0, (struct sockaddr *)&client_info, &len);
            if (receive_size < 0)
            {
                LOG(LogLevel::ERROR) << "recvfrom failed";
            }
            else
            {
                message[receive_size] = '\0'; // 保证消息完整性
                // 将client信息封装成为一个类 - 便于往Route层传递
                Identifier info(client_info);

                // 回调处理 -> 不仅仅是回调,控制传出的参数 -> 外部需要的参数读通过这里传出
                // 2. 将消息+Client信息传递给Route模块
                _func(_socket_fd, message, info);
            }
        }
    }

private:
    int _socket_fd;   // 打开socket的文件描述符
    uint16_t _port;   // 要绑定的port
    bool _is_running; // 运行标志位
    func_t _func;     // 外部回调函数
};

UdpServer.cpp:

cpp 复制代码
#include <iostream>
#include <memory>
#include <string>
#include <functional>
#include "UdpServer.hpp"
#include "Route.hpp"
#include "ThreadPool.hpp"   // 之前编写的线程池模块

using namespace ThreadPoolModule;

using task_t = std::function<void(void)>;

// ./udp_server 8080
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cout << "Use it like this: " << argv[0] << " port" << std::endl;
        return 1;
    }
    uint16_t port = static_cast<uint16_t>(std::atoi(argv[1]));

    // 1. 路由模块 > 消息转发
    Route route;

    // 2. 创建线程池
    ThreadPool<task_t> *pool = ThreadPool<task_t>::GetSingleton();
    pool->Start();

    // 模块的解耦合,从网络通信模块跳转到翻译模块
    // 3. 网络模块 > 通信服务 > 线程池处理消息转发任务
    std::unique_ptr<UdpServer> udp_server =
        std::make_unique<UdpServer>(UdpServer(port, [&route, &pool](int socket_fd, const std::string &message, const Identifier &info)
                                              { auto t = std::bind(&Route::ForwardMessage, &route, socket_fd, message, info);
                                                pool->Enqueue(t); }));

    udp_server->Initial();
    udp_server->Start();
    return 0;
}
3.3 聊天室客户端代码

UdpClient.hpp

cpp 复制代码
#include <iostream>
#include <string>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "Thread.hpp"   // 之前编写的线程模块
#include "InetAddr.hpp"

using namespace LogModule;
using namespace ThreadModule;

class UdpClient
{
private:
    int default_file_descriptor = -1;
    // 发送线程
    void Send(void *agrs)
    {
        while (_is_online)
        {
            // 1. 内部传递的Server信息
            // Identifier peer_info(server_info);
            Identifier peer_info(_server_ip, _server_port);
            socklen_t len_send = peer_info.Size();

            std::string message;

            std::cout << "Please enter# ";   // 标准输出 > 1
            std::getline(std::cin, message); // 标准输入 > 0

            // 2. 对消息做判断
            if (message == "QUIT")
            {
                _is_online = false;
            }

            // 3. 发送给Server
            ssize_t send_size = sendto(_socket_fd, message.c_str(), message.size(), 0, (struct sockaddr *)&peer_info.GetRawAddr(), len_send);
            if (send_size < 0)
            {
                LOG(LogLevel::ERROR) << "sendto failed";
            }
        }
    }
    // 接收线程
    void Receive(void *agrs)
    {
        while (_is_online)
        {
            char buffer[1024];
            // 1. 发送方的信息
            struct sockaddr_in peer_info;
            socklen_t peer_len = sizeof(peer_info);

            // 2. 接收来自Server的信息
            ssize_t receive_size = recvfrom(_socket_fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer_info, &peer_len);
            if (receive_size < 0)
            {
                LOG(LogLevel::ERROR) << "recvfrom failed";
            }
            else
            {
                buffer[receive_size] = '\0';
                // 不能在这里获取到原始Client的IP和Port,因为Route也是Server的一部分
                // 只有Server保留了原始Client的IP和Port,并且只传递给了Route模块
                // std::cout << buffer << std::endl;   // 标准输出 > 1
                std::cerr << buffer << std::endl; // 标准错误 > 2
            }

            // 3. 对消息做判断
            if (buffer == "QUIT")
            {
                _is_online = false;
            }
        }
    }

public:
    UdpClient(uint16_t port, std::string ip)
        : _socket_fd(default_file_descriptor),
          _server_port(port),
          _server_ip(ip)
    {
    }
    // 初始化工作
    void Initial()
    {
        // 1. 获取socket文件描述符
        _socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_socket_fd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket create success, socket file descriptor: " << _socket_fd;
        // 2. 绑定IP和端口 - client不用绑定
    }
    // 启动收发服务
    void Start()
    {
        _is_online = true;
        // 1. 创建收发线程
        // 收发数据的执行本质是交给线程执行!
        Thread send_thread([this](void *args)
                           { this->Send(args); });
        Thread receive_thread([this](void *args)
                              { this->Receive(args); });

        // 2. 启动收发线程
        receive_thread.Start();
        send_thread.Start();

        // 3. 回收收发线程资源(Send和Receive结束后自动回收)
        send_thread.Join();
        receive_thread.Join();
    }

    ~UdpClient() {}

private:
    int _socket_fd;         // 向哪个socket文件中发送
    uint16_t _server_port;  // 向哪个Port发送
    std::string _server_ip; // 向哪个IP发送
    bool _is_online;        // 是否在线
};

UdpClient.cpp:

cpp 复制代码
#include <iostream>
#include <string>
#include <memory>
#include "UdpClient.hpp"
#include "Log.hpp"

using namespace ThreadModule;

// ./udp_client 8080 172.0.0.1
// ./udp_client 8080 127.0.0.1 2>/dev/pts/2
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "Use it like this: " << argv[0] << " port " << "ip" << std::endl;
        return 1;
    }
    uint16_t port = static_cast<uint16_t>(std::atoi(argv[1]));
    std::string ip = argv[2];

    // 创建client实体
    std::unique_ptr<UdpClient> udp_client = std::make_unique<UdpClient>(UdpClient(port, ip));
    udp_client->Initial();
    udp_client->Start();
    return 0;
}

注意:

  • 在启动客户端的时候,需要将标准错误重定向到一个新的终端设备文件中,这样可以区分标准输入输出和接收的消息输出,避免混淆。
  • 在代码中,我们使用了标准错误流 std::cerr 来输出接收到的消息,这样即使标准输出被重定向,接收的消息仍然可以正确显示在指定的终端设备上
3.4 测试

启动聊天室服务器:

bash 复制代码
$ ./udp_server 8080
[2025-12-13 20:21:21] [INFO] [ThreadPool.hpp] [62] - Attempt to obtain the singleton
[2025-12-13 20:21:21] [INFO] [ThreadPool.hpp] [69] - Singleton creation successful
[2025-12-13 20:21:21] [INFO] [ThreadPool.hpp] [93] - Thread[1] start
[2025-12-13 20:21:21] [INFO] [ThreadPool.hpp] [93] - Thread[2] start
[2025-12-13 20:21:21] [INFO] [ThreadPool.hpp] [93] - Thread[3] start
[2025-12-13 20:21:21] [INFO] [ThreadPool.hpp] [93] - Thread[4] start
[2025-12-13 20:21:21] [INFO] [ThreadPool.hpp] [93] - Thread[5] start
[2025-12-13 20:21:21] [INFO] [ThreadPool.hpp] [93] - Thread[6] start
[2025-12-13 20:21:21] [INFO] [UdpServer.hpp] [40] - socket create success, socket file descriptor: 3
[2025-12-13 20:21:21] [INFO] [UdpServer.hpp] [65] - bind success, bind file descriptor: 3
[2025-12-13 20:21:41] [INFO] [ThreadPool.hpp] [24] - Wake up a thread
[2025-12-13 20:21:41] [DEBUG] [Route.hpp] [61] - A user from 127.0.0.1[48582] is online
[2025-12-13 20:21:47] [INFO] [ThreadPool.hpp] [24] - Wake up a thread
[2025-12-13 20:21:47] [DEBUG] [Route.hpp] [61] - A user from 139.9.7.213[57987] is online
[2025-12-13 20:21:57] [INFO] [ThreadPool.hpp] [24] - Wake up a thread
[2025-12-13 20:22:02] [INFO] [ThreadPool.hpp] [24] - Wake up a thread
[2025-12-13 20:23:08] [INFO] [ThreadPool.hpp] [24] - Wake up a thread
[2025-12-13 20:23:08] [DEBUG] [Route.hpp] [37] - 139.9.7.213 has logged out
[2025-12-13 20:23:13] [INFO] [ThreadPool.hpp] [24] - Wake up a thread
[2025-12-13 20:23:13] [DEBUG] [Route.hpp] [37] - 127.0.0.1 has logged out
^C
 130 $ 

启动聊天室客户端1:

bash 复制代码
$ ./udp_client 8080 127.0.0.1 2>/dev/pts/2
[2025-12-13 20:21:25] [INFO] [UdpClient.hpp] [111] - socket create success, socket file descriptor: 3
Please enter# 你好
Please enter# 我看到了
Please enter# QUIT        
$ 

对应的/dev/pts/2输出:

bash 复制代码
$ 127.0.0.1 via 48582 say>>> 你好
139.9.7.213 via 57987 say>>> Hello
139.9.7.213 via 57987 say>>> 我上线了
127.0.0.1 via 48582 say>>> 我看到了
139.9.7.213 via 57987 say>>> QUIT
127.0.0.1 via 48582 say>>> QUIT

启动聊天室客户端2:

bash 复制代码
$ ./udp_client 8080 139.9.7.213 2>/dev/pts/4
[2025-12-13 20:21:31] [INFO] [UdpClient.hpp] [111] - socket create success, socket file descriptor: 3
Please enter# Hello
Please enter# 我上线了
Please enter# QUIT
$ 

对应的/dev/pts/4输出:

bash 复制代码
$ 139.9.7.213 via 57987 say>>> Hello
139.9.7.213 via 57987 say>>> 我上线了
127.0.0.1 via 48582 say>>> 我看到了
139.9.7.213 via 57987 say>>> QUIT

可以看到,两个客户端成功连接到服务器,并且可以相互发送和接收消息。当客户端发送 "QUIT" 消息时,服务器正确地将其从在线用户列表中移除。简单聊天室实现成功!

4. 总结

通过三个案例可以基本总结出 UDP 编程的核心流程:

  1. 创建套接字 socket()
  2. 绑定地址 bind()
  3. 接收数据 recvfrom()
  4. 发送数据 sendto()
  5. 关闭套接字 close()
  6. 根据实际需求,添加相应的业务逻辑模块,如词典查询、消息路由等

不过要注意的是:

  • 服务端一般绑定的地址是 INADDR_ANY,以便接收发送到任意本机 IP 的数据
  • 客户端一般不需要绑定地址,由系统自动分配 IP 和端口
  • UDP 是无连接的协议,不需要建立连接,直接发送和接收数据
相关推荐
kessy11 小时前
LKT4304加密芯片在工业PLC控制器中的安全应用案例
网络
悟凡爱学习2 小时前
Linux 操作系统&消息队列
linux·运维·服务器
i建模2 小时前
Ubuntu增加安装桌面环境
linux·运维·ubuntu
嵌入式×边缘AI:打怪升级日志2 小时前
2.3.2 目录与文件操作命令(保姆级详解)
linux·运维·服务器
艾莉丝努力练剑2 小时前
MySQL查看命令速查表
linux·运维·服务器·网络·数据库·人工智能·mysql
哈__2 小时前
Index-TTS 声音克隆搭载cpolar内网穿透,随时随地生成专属语音!
网络
皮皮哎哟2 小时前
Linux网络最终篇:TCP并发服务器
linux·服务器·select·epoll·poll·tcp并发
雨洛lhw2 小时前
基于 FPGA 的主机 IP 自动配置方案设计
udp·mac·ip·fpga·dhcp
程序喵大人2 小时前
源码剖析:iostream 的缓冲区设计
开发语言·c++·iostream