【Linux笔记】UpdSocket

一、Socket编程UDP

复制代码
服务端流程:
socket() -> bind() -> recvfrom()  -> (处理业务逻辑) -> sendto()  ->  close()
​
客户端流程:
socket() -> sendto()/recvfrom() -> close()

1.1 EchoServer

1.1.1 头文件包含

A. UdpServer.hpp
复制代码
#pragma once
#include <iostream>     // std::cout / std::cerr / std::endl
#include <string>       // std::string
#include <cstdint>      // uint16_t
#include <sys/socket.h> // socket / bind / recvfrom / sendto
#include <netinet/in.h> // struct sockaddr_in / AF_INET
#include <unistd.h>     // close
#include <cerrno>       // errno
#include <cstring>      // strerror
#include "myLog.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
​
namespace UdpServerModule
{
    using namespace LogModule;
​
    class UdpServer : public NoCopy
    {
    public:
        UdpServer(uint16_t port)
            : _port(port)
        {
        }
​
        // 返回值: Common.hpp 中的 ExitCode, NormalExit=0 表示成功, 失败由 main 决定退出码
        int Init()
        {
            // 1. 创建socket,就是创建了文件细节
            _socketfd = socket(AF_INET, SOCK_DGRAM, 0);
            if (_socketfd == -1)
            {
                LOG(LogLevel::ERROR) << "socket failed: " << strerror(errno) << std::endl;
                return SOCKET_ERR;
            }
            LOG(LogLevel::INFO) << "socket created, socketfd: " << _socketfd << std::endl;
​
            // 2.绑定网络
            InetAddr local(_port);
            if (bind(_socketfd, local.GetAddrPtr(), local.GetLen()) == -1)
            {
                LOG(LogLevel::ERROR) << "bind failed: " << strerror(errno) << std::endl;
                return BIND_ERR;
            }
            return NormalExit;
        }
​
        void Start()
        {
            // 3.接受消息
            char buffer[defaultsize];
            for (;;)
            {
                // 接受发送方的信息
                struct sockaddr_in peer; //(ip + 端口)
                socklen_t len = sizeof(peer);
                ssize_t n = recvfrom(_socketfd, buffer, sizeof(buffer) - 1, 0,
                                     (sockaddr *)&peer, &len);
​
                if (n == -1)
                {
                    LOG(LogLevel::ERROR) << "recvfrom failed: " << strerror(errno) << std::endl;
                    continue; /* UDP 服务器不应因单次错误退出 */
                }
                buffer[n] = 0;
​
                // 打印接受到的消息
                std::cout << "[UdpClient] say # " << std::string(buffer) << std::endl;
​
                // 向客户端回显消息
                std::string text = "[UdpServer] echo # " + std::string(buffer, n);
                ssize_t sent = sendto(_socketfd, text.c_str(), text.size(), 0,
                                      (sockaddr *)&peer, len);
​
                if (sent == -1)
                {
                    LOG(LogLevel::ERROR) << "sendto failed: " << strerror(errno) << std::endl;
                    continue;
                }
            }
        }
        ~UdpServer()
        {
            close(_socketfd);
        }
​
    private:
        int _socketfd = defaultfd;
        uint16_t _port;
        static constexpr int defaultfd = -1;
        static constexpr int defaultsize = 1024;
    };
​
}

1.1.2 源代码

A. UdpClient.cc
复制代码
#include "InetAddr.hpp"
#include "Common.hpp"
#include "myLog.hpp"
​
using namespace LogModule;
​
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " ip " << "port" << std::endl;
        return USAGE_ERR;
    }
​
    // 1.创建socket文件
    int sfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sfd == -1)
    {
        LOG(LogLevel::ERROR) << "socket failed" << std::endl;
        return 1;
    }
​
    // 1.5 用于设置接受超时
    //  设置接收超时: UDP不可靠, 包被安全组/防火墙丢弃时不会报错,
    //  只用阻塞在recvfrom上表现为"卡死"。3秒收不到回显即提示超时, 便于定位问题。
    struct timeval tv;
    tv.tv_sec = 3;
    tv.tv_usec = 0;
    if (setsockopt(sfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) == -1)
    {
        LOG(LogLevel::ERROR) << "setsockopt failed: " << strerror(errno) << std::endl;
        return 1;
    }
​
    // 2.构建服务端地址: "ip port" 两个参数交给 InetAddr 完成解析与主机序/网络序转换
    InetAddr dest_addr(argv[1], argv[2]);
​
    for (;;)
    {
        // 3.向服务端发送消息
        std::string msg;
        std::cout << "Please Enter# ";
        if (!std::getline(std::cin, msg))
            break; // stdin结束(Ctrl+D或管道输入完毕), 退出循环而不是用空消息刷屏
        ssize_t sent = sendto(sfd, msg.c_str(), msg.size(), 0,
                              dest_addr.GetAddrPtr(), dest_addr.GetLen());
        if (sent == -1)
        {
            LOG(LogLevel::ERROR) << "sendto failed" << std::endl;
            return 1;
        }
​
        // 4.接受服务端的回显消息
        // UDP客户端无需bind,内核会在第一次sendto时自动分配临时端口
        char buffer[1024];
​
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        ssize_t n = recvfrom(sfd, buffer, sizeof(buffer) - 1, 0,
                             (sockaddr *)&peer, &len);
                
        if (n == -1)
        {
            if (errno == EAGAIN || errno == EWOULDBLOCK)
            {
                // 超时: 数据报已发出但未收到回显 -> 包被安全组/防火墙丢弃, 或服务端未监听该端口
                LOG(LogLevel::WARNING) << "recvfrom timeout: 发送成功但未收到回显"
                                       << " (请检查: 服务端是否运行 / 云安全组是否放行UDP端口 / 系统防火墙)"
                                       << std::endl;
                continue; /* 丢包是 UDP 的常态, 提示后继续等待下一条输入 */
            }
            LOG(LogLevel::ERROR) << "recvfrom failed: " << strerror(errno) << std::endl;
            return 1;
        }
        buffer[n] = 0;
        std::cout << buffer << std::endl;
    }
​
    close(sfd);
    return 0;
}
B. UdpServer.cc
复制代码
#include "UdpServer.hpp"
#include "Common.hpp"
#include <memory>
#include <iostream>
#include <string>
#include <exception>
using namespace UdpServerModule;
​
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " port" << std::endl;
        return USAGE_ERR;
    }
​
    // 校验端口: 非数字输入 stoi 会抛异常, 超出 [0, 65535] 会被截断, 都提前拦截
    int portValue;
    try
    {
        portValue = std::stoi(argv[1]);
    }
    catch (const std::exception &)
    {
        std::cerr << "invalid port: " << argv[1] << std::endl;
        return USAGE_ERR;
    }
    
    if (portValue < 0 || portValue > 65535)
    {
        std::cerr << "port out of range [0, 65535]: " << portValue << std::endl;
        return USAGE_ERR;
    }
​
    std::unique_ptr<UdpServer> udp = std::make_unique<UdpServer>(static_cast<uint16_t>(portValue));
    int rc = udp->Init();
    if (rc != NormalExit)
    {
        LOG(LogLevel::ERROR) << "server init failed, exit code: " << rc << std::endl;
        return rc;
    }
    udp->Start();
​
    return 0;
}

1.2 DictServer

1.2.1 头文件包含

A. Dict.hpp
复制代码
#pragma once
#include <string>
#include <fstream>
#include <unordered_map>
#include <cctype>
#include <filesystem>
#include "myLog.hpp"
​
using namespace LogModule; // 使得 LOG 宏中的 LogLevel 可用
​
// 词库翻译类: 纯业务逻辑, 与网络收发完全解耦
// 使用约定:
//   - 词库仅由构造函数从文件一次性载入, 之后全程只读 (只有 const 查询) ->
//     因此无需加锁; 即便未来换用线程池并发应答, 并发查询也天然安全。
//   - 文件格式: 每行一条 "英文单词 空格 中文释义"; 空行与 '#' 开头视为注释。
class Dict
{
public:
    explicit Dict(const std::string &path)
        : _path(path)
    {
        Load();
    }
​
    // 查询单词释义: 入参忽略大小写; 未命中返回非空提示, 保证客户端总能收到应答
    std::string translate(const std::string &word) const
    {
        auto it = _dic.find(ToLower(word));
        if (it != _dic.end())
            return it->second;
        return "未收录单词: " + word;
    }
​
private:
    void Load()
    {
        std::ifstream in(_path);
        if (!in.is_open())
        {
            LOG(LogLevel::ERROR) << "open dict file failed: " << _path << std::endl;
            LOG(LogLevel::ERROR) << "current working directory: "
                                 << std::filesystem::current_path() << std::endl;
            return; // 文件缺失时以空词库降级, 不中断服务
        }
​
        std::string line;
        while (std::getline(in, line))
        {
            if (line.empty() || line[0] == '#')
                continue; // 空行 / 注释行
​
            size_t pos = line.find(' ');
            if (pos == std::string::npos)
                continue; // 无分隔空格, 视为坏行跳过
​
            std::string key = ToLower(line.substr(0, pos));
            std::string val = line.substr(pos + 1);
            _dic[key] = val; // 同词条后者覆盖前者
        }
    }
​
    // 统一转小写, 使 "Hello" / "HELLO" / "hello" 命中同一词条
    static std::string ToLower(std::string s)
    {
        for (char &c : s)
            c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
        return s;
    }
​
    std::string _path;
    std::unordered_map<std::string, std::string> _dic;
};
B. UdpServer.hpp
复制代码
#pragma once
#include <iostream>     // std::cout / std::cerr / std::endl
#include <string>       // std::string
#include <cstdint>      // uint16_t
#include <sys/socket.h> // socket / bind / recvfrom / sendto
#include <netinet/in.h> // struct sockaddr_in / AF_INET
#include <unistd.h>     // close
#include <cerrno>       // errno
#include <cstring>      // strerror
#include <functional>
#include "myLog.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
​
namespace UdpServerModule
{
    using namespace LogModule;
​
    using func_t = std::function<std::string(const std::string &req)>;
​
    class UdpServer : public NoCopy
    {
    public:
        UdpServer(uint16_t port, func_t func)
            : _port(port),
              _func(func)
        {
        }
​
        // 返回值: Common.hpp 中的 ExitCode, NormalExit=0 表示成功, 失败由 main 决定退出码
        int Init()
        {
            // 1. 创建socket,就是创建了文件细节
            _socketfd = socket(AF_INET, SOCK_DGRAM, 0);
            if (_socketfd == -1)
            {
                LOG(LogLevel::ERROR) << "socket failed: " << strerror(errno) << std::endl;
                return SOCKET_ERR;
            }
            LOG(LogLevel::INFO) << "socket created, socketfd: " << _socketfd << std::endl;
​
            // 2.绑定网络
            InetAddr local(_port);
            if (bind(_socketfd, local.GetAddrPtr(), local.GetLen()) == -1)
            {
                LOG(LogLevel::ERROR) << "bind failed: " << strerror(errno) << std::endl;
                return BIND_ERR;
            }
            return NormalExit;
        }
​
        void Start()
        {
            // 3.接受消息
            char buffer[defaultsize];
            for (;;)
            {
                // 接受发送方的信息
                struct sockaddr_in peer; //(ip + 端口)
                socklen_t len = sizeof(peer);
                ssize_t n = recvfrom(_socketfd, buffer, sizeof(buffer) - 1, 0,
                                     (sockaddr *)&peer, &len);
​
                if (n == -1)
                {
                    LOG(LogLevel::ERROR) << "recvfrom failed: " << strerror(errno) << std::endl;
                    continue; /* UDP 服务器不应因单次错误退出 */
                }
                buffer[n] = 0;
​
                // 调用业务回调(翻译服务), 得到应答文本
                std::string value = _func(std::string(buffer, n));
​
                // 将应答回传给发起请求的客户端
                ssize_t sent = sendto(_socketfd, value.c_str(), value.size(), 0,
                                      (sockaddr *)&peer, len);
                if (sent == -1)
                {
                    LOG(LogLevel::ERROR) << "sendto failed: " << strerror(errno) << std::endl;
                    continue; /* UDP 单条失败不拖垮整个服务 */
                }
            }
        }
        ~UdpServer()
        {
            close(_socketfd);
        }
​
    private:
        int _socketfd = defaultfd;
        uint16_t _port;
        func_t _func; // 执行任务回调
        static constexpr int defaultfd = -1;
        static constexpr int defaultsize = 1024;
    };
​
}

1.2.2 源代码

A. UdpClient.cc
复制代码
#include "InetAddr.hpp"
#include "Common.hpp"
#include "myLog.hpp"
​
using namespace LogModule;
​
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " ip " << "port" << std::endl;
        return USAGE_ERR;
    }
​
    // 1.创建socket文件
    int sfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sfd == -1)
    {
        LOG(LogLevel::ERROR) << "socket failed" << std::endl;
        return 1;
    }
​
    // 1.5 用于设置接受超时
    //  设置接收超时: UDP不可靠, 包被安全组/防火墙丢弃时不会报错,
    //  只用阻塞在recvfrom上表现为"卡死"。3秒收不到回显即提示超时, 便于定位问题。
    struct timeval tv;
    tv.tv_sec = 3;
    tv.tv_usec = 0;
    if (setsockopt(sfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) == -1)
    {
        LOG(LogLevel::ERROR) << "setsockopt failed: " << strerror(errno) << std::endl;
        return 1;
    }
​
    // 2.构建服务端地址: "ip port" 两个参数交给 InetAddr 完成解析与主机序/网络序转换
    InetAddr dest_addr(argv[1], argv[2]);
​
    for (;;)
    {
        // 3.向服务端发送消息
        std::string msg;
        std::cout << "Please Enter# ";
        if (!std::getline(std::cin, msg))
            break; // stdin结束(Ctrl+D或管道输入完毕), 退出循环而不是用空消息刷屏
        ssize_t sent = sendto(sfd, msg.c_str(), msg.size(), 0,
                              dest_addr.GetAddrPtr(), dest_addr.GetLen());
        if (sent == -1)
        {
            LOG(LogLevel::ERROR) << "sendto failed" << std::endl;
            return 1;
        }
​
        // 4.接受服务端的回显消息
        // UDP客户端无需bind,内核会在第一次sendto时自动分配临时端口
        char buffer[1024];
​
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        ssize_t n = recvfrom(sfd, buffer, sizeof(buffer) - 1, 0,
                             (sockaddr *)&peer, &len);
                
        if (n == -1)
        {
            if (errno == EAGAIN || errno == EWOULDBLOCK)
            {
                // 超时: 数据报已发出但未收到回显 -> 包被安全组/防火墙丢弃, 或服务端未监听该端口
                LOG(LogLevel::WARNING) << "recvfrom timeout: 发送成功但未收到回显"
                                       << " (请检查: 服务端是否运行 / 云安全组是否放行UDP端口 / 系统防火墙)"
                                       << std::endl;
                continue; /* 丢包是 UDP 的常态, 提示后继续等待下一条输入 */
            }
            LOG(LogLevel::ERROR) << "recvfrom failed: " << strerror(errno) << std::endl;
            return 1;
        }
        buffer[n] = 0;
        std::cout << buffer << std::endl;
    }
​
    close(sfd);
    return 0;
}
B. UdpServer.cc
复制代码
#include "UdpServer.hpp"
#include "Common.hpp"
#include "Dict.hpp"
#include <memory>
#include <iostream>
#include <string>
#include <exception>
using namespace UdpServerModule;
​
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " port" << std::endl;
        return USAGE_ERR;
    }
​
    // 校验端口: 非数字输入 stoi 会抛异常, 超出 [0, 65535] 会被截断, 都提前拦截
    int portValue;
    try
    {
        portValue = std::stoi(argv[1]);
    }
    catch (const std::exception &)
    {
        std::cerr << "invalid port: " << argv[1] << std::endl;
        return USAGE_ERR;
    }
​
    if (portValue < 0 || portValue > 65535)
    {
        std::cerr << "port out of range [0, 65535]: " << portValue << std::endl;
        return USAGE_ERR;
    }
    // 词库文件: 可执行文件通常在 src/ 下运行, 词库位于其上级目录
    std::unique_ptr<Dict> dict = std::make_unique<Dict>("../dict.txt");
    std::unique_ptr<UdpServer> udp = std::make_unique<UdpServer>(static_cast<uint16_t>(portValue),
                                                                 [&dict](const std::string &req)
                                                                 {
                                                                     return dict->translate(req);
                                                                 });
    int rc = udp->Init();
    if (rc != NormalExit)
    {
        LOG(LogLevel::ERROR) << "server init failed, exit code: " << rc << std::endl;
        return rc;
    }
    udp->Start();
​
    return 0;
}

1.3 ChatServer

1.3.1 头文件包含

A. Route.hpp
B. UdpServer.hpp
复制代码
#pragma once
#include <iostream>     // std::cout / std::cerr / std::endl
#include <string>       // std::string
#include <cstdint>      // uint16_t
#include <sys/socket.h> // socket / bind / recvfrom / sendto
#include <netinet/in.h> // struct sockaddr_in / AF_INET
#include <unistd.h>     // close
#include <cerrno>       // errno
#include <cstring>      // strerror
#include <functional>
#include "myLog.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"

namespace UdpServerModule
{
    using namespace LogModule;
    using namespace ThreadPoolModule;
    using func_t = std::function<void(const InetAddr &peer, int socketfd, const std::string &msg)>;

    class UdpServer : public NoCopy
    {
    public:
        UdpServer(uint16_t port, func_t func)
            : _port(port),
              _func(func)
        {
        }

        // 返回值: Common.hpp 中的 ExitCode, NormalExit=0 表示成功, 失败由 main 决定退出码
        int Init()
        {
            // 1. 创建socket,就是创建了文件细节
            _socketfd = socket(AF_INET, SOCK_DGRAM, 0);
            if (_socketfd == -1)
            {
                LOG(LogLevel::ERROR) << "socket failed: " << strerror(errno) << std::endl;
                return SOCKET_ERR;
            }
            LOG(LogLevel::INFO) << "socket created, socketfd: " << _socketfd << std::endl;

            // 2.绑定网络
            InetAddr local(_port);
            if (bind(_socketfd, local.GetAddrPtr(), local.GetLen()) == -1)
            {
                LOG(LogLevel::ERROR) << "bind failed: " << strerror(errno) << std::endl;
                return BIND_ERR;
            }
            return NormalExit;
        }

        void Start()
        {
            // 3.接受消息
            char buffer[defaultsize];
            for (;;)
            {
                // 接受发送方的信息
                struct sockaddr_in peer; //(ip + 端口)
                socklen_t len = sizeof(peer);
                ssize_t n = recvfrom(_socketfd, buffer, sizeof(buffer) - 1, 0,
                                     (sockaddr *)&peer, &len);

                if (n == -1)
                {
                    LOG(LogLevel::ERROR) << "recvfrom failed: " << strerror(errno) << std::endl;
                    continue; /* UDP 服务器不应因单次错误退出 */
                }
                buffer[n] = 0;
                InetAddr client(peer);

                _func(client, _socketfd, std::string(buffer));
            }
        }
        ~UdpServer()
        {
            close(_socketfd);
        }

    private:
        int _socketfd = defaultfd;
        uint16_t _port;
        func_t _func; // 执行任务回调
        static constexpr int defaultfd = -1;
        static constexpr int defaultsize = 1024;
    };

}

1.3.2 源代码

A. UdpClient.cc
复制代码
#include "InetAddr.hpp"
#include "Common.hpp"
#include "myLog.hpp"
#include "myThread.hpp"
#include <sys/time.h>
#include <atomic>
#include <cerrno>
#include <cstring>
#include <iostream>
#include <string>

using namespace LogModule;
using namespace ThreadModule; // 接收线程使用项目自研的 pthread 封装 Thread

// 双线程全双工客户端:
//   接收线程 —— 独立 recvfrom + 打印: 主线程停在 stdin 等待输入时也能随时收到广播
//   主线程   —— 读输入 + sendto: 发送是非阻塞的, 不需要任何超时
// 为什么用双线程分工而不是线程池打印:
//   线程池适合"大量相似、独立、昂贵的并行任务"; 这里只有一个 socket、
//   打印极快, 需要的只是"收发两条线各干各的"。且多 worker 并发写 cout
//   会让一行消息被另一行切开(输出交错)、顺序也与到达顺序不一致。
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " ip " << "port" << std::endl;
        return USAGE_ERR;
    }

    // 1.创建socket文件
    int sfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sfd == -1)
    {
        LOG(LogLevel::ERROR) << "socket failed" << std::endl;
        return 1;
    }

    // 1.5 设置接收超时:
    //   UDP不可靠, 包被安全组/防火墙丢弃时阻塞在 recvfrom 上会表现为"卡死"。
    //   这里给接收线程 500ms 短超时: 收不到包时轮询退出标志, 让进程能快速退出。
    //   (收到数据会立即返回, 超时只影响"无数据"的空转, 不影响正常收发)
    struct timeval tv;
    tv.tv_sec = 0;
    tv.tv_usec = 500 * 1000; // 500ms
    if (setsockopt(sfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) == -1)
    {
        LOG(LogLevel::ERROR) << "setsockopt failed: " << strerror(errno) << std::endl;
        close(sfd);
        return 1;
    }

    // 2.构建服务端地址: "ip port" 两个参数交给 InetAddr 完成解析与主机序/网络序转换
    InetAddr dest_addr(argv[1], argv[2]);

    // 3.接收线程: 唯一消费 socket 下行数据的地方, 收到即打印
    //   Thread(项目自研 pthread 封装): 构造时传入"线程名 + 回调"(内部 bind 成无参任务),
    //   start() 启动, join() 回收。回调 [&] 捕获 running/sfd, join 发生在这些对象销毁前,
    //   所以捕获的引用始终有效。char buffer[1024] 声明在回调体内, 归线程栈所有。
    std::atomic<bool> running{true};
    Thread recv_thread("recv",
                       [&]()
                       {
                           char buffer[1024];
                           while (running.load(std::memory_order_relaxed))
                           {
                               struct sockaddr_in peer;
                               socklen_t len = sizeof(peer);
                               ssize_t n = recvfrom(sfd, buffer, sizeof(buffer) - 1, 0,
                                                    (sockaddr *)&peer, &len);
                               if (n == -1)
                               {
                                   if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
                                       continue; // 超时/被信号中断: 回到循环顶部, 用退出标志判断是否停止
                                   LOG(LogLevel::ERROR) << "recvfrom failed: " << strerror(errno) << std::endl;
                                   break;
                               }
                               buffer[n] = 0;
                               // 全客户端唯一的打印点: 单线程输出, 天然有序且不会交错
                               std::cout << buffer << std::endl;
                           }
                       });

    recv_thread.start();

    // 4.主线程: 读输入 -> 发送, 循环直到 EOF 或输入 exit
    for (;;)
    {
        std::string msg;
        std::cout << "Please Enter# ";
        if (!std::getline(std::cin, msg))
            break; // stdin结束(Ctrl+D或管道输入完毕), 退出循环而不是用空消息刷屏
        if (msg == "exit")
            break; // 显式退出命令

        ssize_t sent = sendto(sfd, msg.c_str(), msg.size(), 0,
                              dest_addr.GetAddrPtr(), dest_addr.GetLen());
        if (sent == -1)
        {
            LOG(LogLevel::ERROR) << "sendto failed: " << strerror(errno) << std::endl;
            running.store(false, std::memory_order_relaxed);
            recv_thread.join(); // 先收线程再关 socket, 避免在途 recvfrom 撞上 close
            close(sfd);
            return 1;
        }
    }

    // 5.收尾: 先令接收线程退出并等它结束, 再关 socket。
    //   顺序不能反: 若先 close, 正在阻塞的 recvfrom 可能与新 fd 产生复用竞态;
    //   先 join 则接收线程至多再空转 500ms(SO_RCVTIMEO)就会返回并读到退出标志。
    running.store(false, std::memory_order_relaxed);
    recv_thread.join();
    close(sfd);
    return 0;
}
B. UdpServer.cc
复制代码
#include "UdpServer.hpp"
#include "Common.hpp"
#include "Route.hpp"
#include <memory>
#include <iostream>
#include <string>
#include <exception>
using namespace UdpServerModule;

using task_t = std::function<void()>;
int main(int argc, char *argv[])
{
    // 1.校验启动进程: 进程名+端口号
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " port" << std::endl;
        return USAGE_ERR;
    }

    // 2.校验端口: 非数字输入 stoi 会抛异常, 超出 [0, 65535] 会被截断, 都提前拦截
    int portValue;
    try
    {
        portValue = std::stoi(argv[1]);
    }
    catch (const std::exception &)
    {
        std::cerr << "invalid port: " << argv[1] << std::endl;
        return USAGE_ERR;
    }
    if (portValue < 0 || portValue > 65535)
    {
        std::cerr << "port out of range [0, 65535]: " << portValue << std::endl;
        return USAGE_ERR;
    }

    // 2.注册路由服务
    std::unique_ptr<Route> route = std::make_unique<Route>();

    // 3.启动线程池
    std::unique_ptr<ThreadPool<task_t>> pool = std::make_unique<ThreadPool<task_t>>(15);

    // 4.启动Udp服务器: 网络回调把消息包装成无参任务交给线程池, worker 拿到即可执行
    std::unique_ptr<UdpServer> udp = std::make_unique<UdpServer>(static_cast<uint16_t>(portValue),
                                                                 [&route, &pool](const InetAddr &peer, int socketfd, const std::string &msg)
                                                                 {
                                                                     // 参数在入队现场按值捕获定型; route 以裸指针捕获 (unique_ptr 不可拷贝)
                                                                     pool->PushTask([r = route.get(), peer, socketfd, msg]()
                                                                                    {
                                                                                        r->MessageRoute(peer, socketfd, msg);
                                                                                    });
                                                                 });

    int rc = udp->Init();
    if (rc != NormalExit)
    {
        LOG(LogLevel::ERROR) << "server init failed, exit code: " << rc << std::endl;
        return rc;
    }
    pool->Start(); // 启动 worker 线程, 否则任务入队后无人消费
    udp->Start();
    pool->Stop();
    return 0;
}

二、附录

2.1 头文件补充

2.1.1 Common.hpp

复制代码
#pragma once
#include <iostream>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string>
#include <sys/time.h>
#include <cerrno>

// 错误码
enum ExitCode
{
    NormalExit,
    USAGE_ERR,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    ACCEPT_ERR
};

class NoCopy
{
public:
    NoCopy() = default; // 显式保留默认构造(声明了删除的拷贝构造会抑制隐式默认构造)
    NoCopy(const NoCopy &) = delete;
    NoCopy &operator=(const NoCopy &) = delete;
    ~NoCopy() = default;
private:
};

2.1.2 InetAddr.hpp

复制代码
#pragma once
#include <string>        // std::string / std::stoi
#include <cstring>       // memset
#include <cstdint>       // uint16_t
#include <netinet/in.h>  // struct sockaddr_in / htons / INADDR_ANY
#include <arpa/inet.h>   // inet_pton / inet_ntop
#include "Common.hpp"

class InetAddr
{
public:
    InetAddr() = default;

    //点分十进制ip 和 字符串型port
    InetAddr(const std::string &ip, const std::string port)
        : _ip(ip),
          _port(std::stoi(port))
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr.s_addr);
    }

    //点分十进制ip 和 短整型port
    InetAddr(const std::string &ip, uint16_t port)
        : _ip(ip),
          _port(port)
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr.s_addr);
    }

    //默认ip "0.0.0.0" 监听所有窗口 和 短整型port
    InetAddr(uint16_t port)
        : _ip("0.0.0.0"),
          _port(port)
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(port);
        _addr.sin_addr.s_addr = INADDR_ANY;
    }

    // 网络套接字 -> 本地套接字
    InetAddr(const struct sockaddr_in &addr)
        : _addr(addr)
    {
        char buffer[64];
        _ip = inet_ntop(AF_INET, &_addr.sin_addr.s_addr, buffer, sizeof(buffer));
        _port = ntohs(_addr.sin_port);
    }

    std::string GetIp() const
    {
        return _ip;
    }

    uint16_t GetPort() const
    {
        return _port;
    }

    socklen_t GetLen() const
    {
        return sizeof(_addr);
    }

    std::string GetStringAddr() const
    {
        return _ip + ":" + std::to_string(_port);
    }

    const struct sockaddr_in &GetAddr() const
    {
        return _addr;
    }

    const struct sockaddr *GetAddrPtr() const
    {
        return (const struct sockaddr *)&_addr;
    }

private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};

2.1.3 myLog.hpp

复制代码
#pragma once

#include <sys/types.h>
#include <unistd.h>
#include <fstream>
#include <sstream>
#include <memory>
#include <filesystem>
#include "myMutex.hpp"

namespace LogModule
{
    // 默认路径
    const std::string defaultpath = "./log/";
    const std::string defaultname = "log.txt";

    // 获取当前时间
    static std::string GetCurrTime()
    {
        time_t now = time(nullptr); // 获取当前时间
        struct tm t;
        localtime_r(&now, &t); // 线程安全,必须 _r
        char buf[64] = {0};
        strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &t);
        return buf;
    }

    // 日志等级
    enum class LogLevel
    {
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };

    // 等级枚举 → 字符串
    std::string LogLevelToString(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::DEBUG:
            return "DEBUG";
        case LogLevel::INFO:
            return "INFO";
        case LogLevel::WARNING:
            return "WARNING";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        default:
            return "UNKNOWN";
        }
    }

    // LevelTag: 流式日志的"参数包"
    //  LOG(INFO) << "x"; 中, LOG 宏把 等级/文件/行号 打包后经 << 传给 Logger
    struct LevelTag
    {
        LogLevel level;
        const char *file;
        int line;
    };

    // LogStrategy: 抽象的日志刷新策略
    //  通过多态实现: a.在显示器打印 b.向指定文件写入
    class LogStrategy
    {
    public:
        LogStrategy() = default;

        virtual ~LogStrategy() = default;                     // 让编译器合成默认析构,不抑制移动语义
        virtual void SyncLog(const std::string &message) = 0; // 纯虚函数
    private:
    };

    // ConsoleLogStrategy:将日志输出到显示器
    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        ConsoleLogStrategy() = default;
        void SyncLog(const std::string &message) override
        {
            {
                MutexModule::LockGuard lg(_mutex);
                std::cout << message << std::endl;
            }
        }

    private:
        MutexModule::Mutex _mutex;
    };

    // FileLogStrategy : 将日志信息输出到文件
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const std::string &path = defaultpath, // 默认参数已覆盖无参调用
                        const std::string &name = defaultname)
            : _fullpath(path + name)
        {
            std::filesystem::create_directories(path);
            _ofs.open(_fullpath, std::ios::app);
        }
        void SyncLog(const std::string &message) override
        {
            {
                MutexModule::LockGuard lg(_mutex);
                _ofs << message << '\n';
            }
        }
        ~FileLogStrategy()
        {
            if (_ofs.is_open())
                _ofs.close();
        }

    private:
        std::string _fullpath;
        std::ofstream _ofs;
        MutexModule::Mutex _mutex;
    };

    class Message; // 前置声明, 完整定义见 Logger 之后 (它需要调用 Logger 的公开接口)

    class Logger
    {
    public:
        static Logger &GetInstance() // 单例入口
        {
            static Logger instance;
            return instance;
        }

        void Log(LogLevel level, const char *file, int line, std::string message); // 定义见 Message 之后

        // 判断某等级是否会被记录 (供 Message 构造时提前短路)
        bool ShouldLog(LogLevel level) const { return level >= _logLevel; }

        // 统一落盘出口: 只做策略输出 (格式化由 Message 完成)
        void SyncText(const std::string &line); // 定义见 Message 之后

        void SetLogLevel(LogLevel level) { _logLevel = level; }
        void SetStrategy(std::unique_ptr<LogStrategy> strategy) { _strategy = std::move(strategy); }

        // 流式入口: LOG(INFO) << "x" << 42;
        // 返回临时 Message, 整个表达式结束后由析构函数统一落盘
        Message operator<<(const LevelTag &tag);

    private:
        Logger()
            : _logLevel(LogLevel::DEBUG),
              _strategy(std::make_unique<ConsoleLogStrategy>())
        {
        }

        Logger(const Logger &) = delete;
        Logger &operator=(const Logger &) = delete;
        ~Logger() = default;

        LogLevel _logLevel; // 日志的等级
        std::unique_ptr<LogStrategy> _strategy;
    };

    // Message: 一条正在构建的日志行 (流式收集 + 格式化 + 落盘 合一)
    //  流式用法:  LOG(INFO) << a << b; 内容先收集进 _oss,
    //             整个表达式结束时临时对象析构 → 格式化并交给策略落盘。
    //  函数式用法: Logger::Log() 内部也构造 Message 填入文本, 复用同一通道。
    //  只通过 Logger 的公开接口 (ShouldLog / SyncText) 协作, 不触碰内部实现。
    //  每条语句一个独立缓冲 → 线程安全, 也不需要写 std::endl。
    class Message
    {
    public:
        Message(LogLevel level, const char *file, int line, Logger &logger)
            : _level(level),
              _file(file),
              _line(line),
              _pid(getpid()),
              _valid(logger.ShouldLog(level)), // 等级过滤: 不达标直接短路
              _logger(logger)
        {
        }

        ~Message()
        {
            if (!_valid)
                return;
            std::string text = _oss.str();
            if (text.empty()) // 空语句 LOG(INFO); 什么都不做
                return;
            _logger.SyncText(Format(text)); // 拼好完整一行, 交给策略落盘
        }

        template <typename T>
        Message &operator<<(const T &value) // 数字/字符串/自定义类型均可用
        {
            if (_valid)
                _oss << value;
            return *this;
        }

        // 兼容 std::endl / std::flush 等流操纵符: 忽略即可, 析构时才真实落盘
        Message &operator<<(std::ostream &(*)(std::ostream &))
        {
            return *this;
        }

        Message(Message &&) = default;     // 允许临时对象移动 (C++17 返回值优化也兜底)
        Message(const Message &) = delete; // 拷贝会重复落盘, 禁止

    private:
        // 拼完整行: [时间] [等级] [pid] [文件] [行号] - 内容
        std::string Format(const std::string &content) const
        {
            std::ostringstream oss;
            oss << '[' << GetCurrTime() << "] [" << LogLevelToString(_level) << "] ["
                << _pid << "] [" << _file << "] [" << _line << "] - " << content;
            return oss.str();
        }

        LogLevel _level;         // 日志的等级
        const char *_file;       // 在哪个文件打印
        int _line;               // 文件的行号
        pid_t _pid;              // 进程 id
        bool _valid;             // 等级过滤结果
        Logger &_logger;         // 落盘时回交给 Logger
        std::ostringstream _oss; // 流式内容的缓冲
    };

    // ---- 以下成员定义必须放在 Message 之后 (需要 Message 的完整类型) ----

    // 函数式入口: 构造一个 Message 填入文本, 由 m 析构时落盘 (过滤由 Message 内部短路)
    inline void Logger::Log(LogLevel level, const char *file, int line, std::string message)
    {
        Message m(level, file, line, *this);
        m << message;
    }

    inline void Logger::SyncText(const std::string &line)
    {
        _strategy->SyncLog(line); // 策略输出
    }

    // 流式入口: operator<< 按值返回也需要 Message 的完整类型
    inline Message Logger::operator<<(const LevelTag &tag)
    {
        return Message(tag.level, tag.file, tag.line, *this);
    }
}

#define LOG(level) \
    LogModule::Logger::GetInstance() << LogModule::LevelTag { (level), __FILE__, __LINE__ }

2.1.4 myMutex.hpp

复制代码
#pragma once
#include <iostream>
#include <pthread.h>

namespace MutexModule
{
    class Mutex
    {
    public:
        Mutex()
        {
            pthread_mutex_init(&_mutex, nullptr);
        }
        Mutex(const Mutex &) = delete;
        Mutex &operator=(const Mutex &) = delete;

        void lock()
        {
            pthread_mutex_lock(&_mutex);
        }

        void unlock()
        {
            pthread_mutex_unlock(&_mutex);
        }

        // 暴露底层句柄,供条件变量等需要原生 pthread_mutex_t* 的场景使用
        pthread_mutex_t *native_handle()
        {
            return &_mutex;
        }

        ~Mutex()
        {
            pthread_mutex_destroy(&_mutex);
        }

    private:
        pthread_mutex_t _mutex;
    };

    class LockGuard
    {
    public:
        LockGuard(Mutex &mutex)
            : _mutex(mutex)
        {
            _mutex.lock();
        }

        LockGuard(const LockGuard &) = delete;
        LockGuard &operator=(const LockGuard &) = delete;

        ~LockGuard()
        {
            _mutex.unlock();
        }

    private:
        Mutex &_mutex;
    };
}

2.1.5 myThread.hpp

复制代码
#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include <cstring>
#include <cstdlib>

// 基于 C 接口 (pthread) 的 C++ 线程封装
namespace ThreadModule
{
    // 已绑参的可调用对象类型: 无参无返回
    using func_t = std::function<void()>;

    // 内部上下文: 承载可调用对象 + 线程名
    // 独立分配在堆上, 是为了把生命周期与 Thread 对象解耦
    // (线程可能在 Thread 对象析构后仍在运行, 不能让回调引用悬空)
    class ThreadData
    {
        friend class Thread; // 须带 class: 否则 Thread 尚未声明, 会被当成友元函数声明

    public:
        ThreadData(const func_t &func, const std::string &name)
            : _func(func),
              _name(name)
        {
        }

    private:
        func_t _func;
        std::string _name;
    };

    // 线程状态: 防止非法操作 (重复 join / join 已 detach 的线程等)
    enum class ThreadStatus
    {
        NEW,      // 已构造, 未启动
        RUNNING,  // 运行中
        DETACHED, // 已分离
        JOINED    // 已回收
    };

    // 线程自动编号: inline 函数 + static 局部变量
    // C++11 下 inline 函数的 static 局部量跨编译单元共享, 头文件安全
    inline int NextThreadId()
    {
        static int id = 1;
        return id++;
    }

    class Thread
    {
    public:
        // 接受任意可调用对象 + 参数, 用 std::bind 绑参后转为 std::function<void()>
        template <typename F, typename... Args>
        Thread(const std::string &name, F &&f, Args &&...args)
        {
            _name = name + "-" + std::to_string(NextThreadId());
            _status = ThreadStatus::NEW;
            _td = new ThreadData(
                std::bind(std::forward<F>(f), std::forward<Args>(args)...),
                _name);
        }

        ~Thread()
        {
            // 析构策略 B : 仍在运行就自动 detach, 保证不泄漏
            if (_status == ThreadStatus::RUNNING)
            {
                pthread_detach(_tid); // 不断言, 容错 (线程可能已结束)
            }
            // 从未启动: 跳板不会运行, 自己清理 _td
            if (_status == ThreadStatus::NEW && _td != nullptr)
            {
                delete _td;
                _td = nullptr;
            }
            // 已启动 (RUNNING/DETACHED/JOINED): _td 由跳板结束时释放, 此处不动
        }

        // 禁用拷贝: 一个 pthread_t 不能被两个对象管理 (double join / double free)
        Thread(const Thread &) = delete;
        Thread &operator=(const Thread &) = delete;

        // 启动线程 (构造与启动分离, 便于统一管理一批线程)
        void start()
        {
            if (_status != ThreadStatus::NEW)
            {
                std::cerr << "[Thread:" << _name << "] already started\n";
                return;
            }
            int n = pthread_create(&_tid, nullptr, start_routine, _td);
            if (n != 0)
            {
                std::cerr << "pthread_create error: " << strerror(n) << "\n";
                std::abort();
            }
            _status = ThreadStatus::RUNNING;
        }

        // 等待回收
        void join()
        {
            if (_status != ThreadStatus::RUNNING)
            {
                std::cerr << "[Thread:" << _name << "] cannot join (status not RUNNING)\n";
                return;
            }
            int n = pthread_join(_tid, nullptr);
            if (n != 0)
            {
                std::cerr << "pthread_join error: " << strerror(n) << "\n";
                std::abort();
            }
            _status = ThreadStatus::JOINED;
            _td = nullptr; // _td 已被跳板释放, 仅置空标记
        }

        // 分离
        void detach()
        {
            if (_status != ThreadStatus::RUNNING)
            {
                std::cerr << "[Thread:" << _name << "] cannot detach (status not RUNNING)\n";
                return;
            }
            int n = pthread_detach(_tid);
            if (n != 0)
            {
                std::cerr << "pthread_detach error: " << strerror(n) << "\n";
                std::abort();
            }
            _status = ThreadStatus::DETACHED;
            _td = nullptr; // _td 由跳板结束时释放
        }

        const std::string &name() const { return _name; }
        bool isRunning() const { return _status == ThreadStatus::RUNNING; }

    private:
        // 跳板: 签名必须匹配 pthread 要求的 void*(*)(void*)
        // 由于非 static 成员函数隐含 this, 签名不匹配, 因此必须用 static
        static void *start_routine(void *arg)
        {
            ThreadData *td = static_cast<ThreadData *>(arg);
            td->_func(); // 执行用户回调
            delete td;   // 跳板作为最终消费者, 释放上下文
            return nullptr;
        }

    private:
        pthread_t _tid;
        std::string _name; // 自己保留一份, name() 在 join/detach 后仍可用
        ThreadStatus _status;
        ThreadData *_td = nullptr;
    };

} // namespace ThreadModule

2.1.6 ThreadPool.hpp

复制代码
#pragma once
#include "myLog.hpp"
#include "myThread.hpp"
#include "myMutex.hpp"
#include "myCond.hpp"
#include <vector>
#include <queue>
#include <memory>

namespace ThreadPoolModule
{
    using namespace LogModule;
    const int defalutnum = 5;

    template <typename T>
    class ThreadPool
    {
    public:
        ThreadPool(const int num = defalutnum)
        {
            for (int i = 0; i < num; i++)
            {
                _threads.emplace_back(std::make_unique<ThreadModule::Thread>(
                    "worker",
                    [this]()
                    {
                        HandleTask();
                    }));
            }
        }

        // 工作线程调用:处理任务队列中的任务
        void HandleTask()
        {
            while (true)
            {
                T task;
                {
                    MutexModule::LockGuard lg(_mutex);
                    // 1.用while循环防止虚假唤醒 和 线程池是否退出
                    while (_task_q.empty() && _isrunning)
                    {
                        _cond.Wait(_mutex);
                    }

                    // 线程退出
                    if (_task_q.empty() && !_isrunning)
                    {
                        return;
                    }

                    // 2.线程获取任务
                    task = _task_q.front();
                    _task_q.pop();
                }
                // 3.线程处理任务
                task();
            }
        }

        // 用户线程调用:向任务队列中添加任务
        void PushTask(const T &task)
        {
            {
                MutexModule::LockGuard lg(_mutex);
                _task_q.push(task);
            }
            _cond.Signal();
        }

        // 创建所有线程
        void Start()
        {
            {
                MutexModule::LockGuard lg(_mutex);
                if (_isrunning)
                    return;
                _isrunning = true;
            }

            for (const auto &t : _threads)
            {
                t->start();
            }
        }

        // 广播唤醒所有阻塞的 worker
        void Stop()
        {
            // 注意:需要加锁对_isrunning进行设置
            {
                MutexModule::LockGuard lg(_mutex);
                if (!_isrunning)
                    return;
                _isrunning = false;
            }
            _cond.Broadcast();
        }

        void Join()
        {
            for (auto &t : _threads)
            {
                if (t->isRunning()) // 只回收运行中的;未启动/已回收的跳过
                    t->join();
            }
        }

        ~ThreadPool()
        {
            Stop();
            Join();
        }

    private:
        std::vector<std::unique_ptr<ThreadModule::Thread>> _threads; // 管理线程
        std::queue<T> _task_q;
        MutexModule::Mutex _mutex;
        CondModule::Cond _cond;
        bool _isrunning = false;
    };
}
相关推荐
皓月盈江1 小时前
Linux系统grep、sed 、awk介绍下,使用方法及区别?
linux·运维·服务器·sed·grep·awk
RickyWasYoung1 小时前
二阶魔方底层逻辑分析
笔记
云栖梦泽1 小时前
Linux内存管理
linux·运维·服务器
吴声子夜歌1 小时前
Nginx应用与运维——Nginx Web服务应用实战(静态文件服务器的搭建)
运维·前端·nginx
Lucis__2 小时前
编译链接全链路解析:ELF形成到可执行加载的发展历程
linux·程序·elf文件
海宇服务2 小时前
零信任架构实战:基于海宇身份证OCR构建自动化自助终端核验网关
运维·人工智能·架构·自动化
ken22322 小时前
ubuntu 24.04, audacity 4 AppImage 加载 libavdevice.so.62 /.61 /.63 失败:依赖缺失?
linux·运维·ubuntu
NightReader2 小时前
vCenter8部署
linux
Sammyyyyy2 小时前
结构化决策模型 Jev 实战笔记,3 个原语、4 个限制、5 个落地场景
人工智能·笔记·开发工具·编程语言