【Linux网络编程】UDP套接字编程:系统编程组件、基础回显服务、InetAddr与地址转换、翻译服务与线程池聊天室

【Linux网络编程】UDP套接字编程:系统编程组件、基础回显服务、InetAddr与地址转换、翻译服务与线程池聊天室


🌈 say-fall:个人主页 🚀 专栏:《手把手教你学会C++》 | 《系统深入Linux操作系统》 | 《数据结构与算法》 | 《小游戏与项目》 💪 格言:做好你自己,才能吸引更多人,与他们共赢,这才是最好的成长方式。


前言

我们了解完计算机网络基础,知道了 IP 加端口号才能定位到一台主机上的那一个进程,下面就用Linux网络编程里的 UDP 套接字,把这份认识落到一份能跑起来的代码上。

刚学完网络基础面对真正的 socket 代码可能会有这些疑问:一个 UDP 服务端到底要填哪些参数才能收消息?客户端明明没写 bind,回复又是怎么送回去的?

现象 你可能以为的原因 真正发生的事情
服务端能收到消息 把 IP 写死在结构体里就行 绑 INADDR_ANY 才能监听本机所有网卡
客户端没 bind 也能通信 客户端不需要地址 首次 sendto 时 OS 自动分配 IP 和随机端口
消息能原样回显 recvfrom 只负责拿数据 它顺带带回了对端的 sockaddr_in,回发全靠这个地址
换个功能就要重写服务端 收发逻辑得跟着业务一起改 用 std::function 把业务从网络层剥出去,外面传什么就做什么
多人聊天室不卡 一个单线程循环就够用 线程池接任务,客户端收和发各起一个线程

问题来了: 地址为什么要在字符串和整数之间转来转去?inet_ntoa 为什么不能连着用两次?一个回显服务器又是怎么一步步变成翻译服务器、再变成聊天室的?

答案就是------socket + bind + recvfrom/sendto 这套固定骨架!先把地址填对、把端口绑上,剩下的业务全交给外部传进来的函数;再往上加一层 InetAddr 封装和线程池,同一份 UdpServer 就能从回显走到翻译,最后走到多人聊天室。

通过本文,你将掌握:

技能 应用场景
Log / Mutex / Thread / Cond / ThreadPool 复用日志、互斥锁、线程与线程池,把系统编程的轮子直接搬过来
socket、bind、recvfrom、sendto 写出第一个能收能回的 UDP 服务端与客户端
INADDR_ANY 与网络字节序 多网卡时不漏收消息,看懂 htons / ntohs 在转什么
客户端隐式 bind 明白为什么客户端不用自己绑端口,OS 在首次发送时就办好了
InetAddr 与 inet_pton / inet_ntop 把地址转换和强转收进一个类,避开 inet_ntoa 静态缓冲区的坑
std::function 与 lambda 分层 换业务不改网络代码,做出英汉翻译服务
线程池 + Route 广播 管理在线用户,把一条消息分发给所有人
客户端收发双线程 让「等输入」和「等消息」互不阻塞

前置知识: C++ 基础语法与 lambda、Linux 系统编程里的线程与互斥、计算机网络基础里的 IP / 端口 / 网络字节序。


文章目录


正文:

系统编程的一些实现

在实现网络通信功能之前,先把系统编程里面实现的一些东西拿出来作为前置了解,此处不多做讲解,想要了解如何实现的可以看【Linux系统编程】的文章。

Log.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <filesystem>
#include <sstream>
#include <fstream>
#include <memory>
#include <unistd.h>
#include <ctime>
#include "Mutex.hpp"

namespace LogModule
{
    using namespace MutexModule;

    const std::string gsep = "\r\n";
    class LogStrategy
    {
    public:
        virtual ~LogStrategy() = default;
        virtual void SyncLog(const std::string &massage) = 0;
    };

    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        void SyncLog(const std::string &message) override
        {
            LockGuard lockGuard(_mutex);
            std::cout << message << gsep;
        }
        ~ConsoleLogStrategy()
        {
        }

    private:
        Mutex _mutex;
    };

    const std::string defaultpath = "./log";
    const std::string defaultfile = "my.log";

    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile)
            : _path(path),
              _file(file)
        {
            LockGuard lockGuard(_mutex);
            if (std::filesystem::exists(_path))
            {
                return;
            }
            try
            {
                // 监控区域
                std::filesystem::create_directories(_path);
            }
            catch (const std::filesystem::filesystem_error &e)
            {
                std::cerr << e.what() << "\n";
            }
        }

        void SyncLog(const std::string &message) override
        {
            LockGuard lockGuard(_mutex);

            std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;

            std::ofstream out(filename, std::ios::app);
            if (!out.is_open())
            {
                return;
            }
            out << message << gsep;
            out.close();
        }
        ~FileLogStrategy()
        {
        }

    private:
        std::string _path;
        std::string _file;
        Mutex _mutex;
    };

    ///////////////////////////////////////////////////////////
    enum class LogLevel
    {
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };

    std::string LeveltoStr(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";
        }
    }

    std::string GetTimeStamp()
    {
        time_t curr = time(nullptr);
        struct tm curr_tm;
        localtime_r(&curr, &curr_tm);

        char timebuffer[128];
        snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d  %02d:%02d:%02d", curr_tm.tm_year + 1900, curr_tm.tm_mon + 1, curr_tm.tm_mday, curr_tm.tm_hour, curr_tm.tm_min, curr_tm.tm_sec);

        return timebuffer;
    }

    class Logger
    {
    public:
        Logger()
        {
            EnableConsoleStrategy();
        }
        void EnableFileStrategy()
        {
            _fflush_strategy = std::make_unique<FileLogStrategy>();
        }
        void EnableConsoleStrategy()
        {
            _fflush_strategy = std::make_unique<ConsoleLogStrategy>();
        }

        class LogMessage
        {
        public:
            LogMessage(LogLevel &level, std::string src_name, int line_number, Logger &logger)
                : _level(level), _curr_time(GetTimeStamp()), _pid(getpid()), _src_name(src_name), _line_number(line_number), _logger(logger)
            {
                std::stringstream ss;
                ss << "[" << _curr_time << "]"
                   << "[" << LeveltoStr(level) << "]"
                   << "[" << _pid << "]"
                   << "[" << _src_name << "]"
                   << "[" << _line_number << "]"
                   << " - ";
                _loginfo = ss.str();
            }

            template <typename T>
            LogMessage &operator<<(const T &info)
            {
                std::stringstream ss;
                ss << info;
                _loginfo += ss.str();
                return *this;
            }

            ~LogMessage()
            {
                if (_logger._fflush_strategy)
                {
                    _logger._fflush_strategy->SyncLog(_loginfo);
                }
            }

        private:
            LogLevel _level;
            std::string _curr_time;
            pid_t _pid;
            std::string _src_name;
            int _line_number;
            std::string _loginfo; // 合并之后,一条完整的信息
            Logger &_logger;
        };

        //
        LogMessage operator()(LogLevel level, std::string name, int line)
        {
            return LogMessage(level, name, line, *this);
        }
        ~Logger()
        {
        }

    private:
        std::unique_ptr<LogStrategy> _fflush_strategy;
    };

    // 全局日志对象
    Logger logger;

// 使用宏,简化用户操作,获取文件名和行号
#define LOG(level) logger(level, __FILE__, __LINE__)
#define Enable_Console_Log_Strategy() logger.EnableConsoleStrategy()
#define Enable_File_Log_Strategy() logger.EnableFileStrategy()
}

Mutex.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <pthread.h>

namespace MutexModule
{
    class Mutex
    {
    public:
        Mutex()
        {
            pthread_mutex_init(&_mutex, nullptr);
        }
        void Lock()
        {
            int n = pthread_mutex_lock(&_mutex);
            (void)n;
        }
        void Unlock()
        {
            int n = pthread_mutex_unlock(&_mutex);
            (void)n;
        }
        pthread_mutex_t *GetMutex()
        {
            return &_mutex;
        }

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

    private:
        pthread_mutex_t _mutex;
    };

    class LockGuard
    {
    public:
        LockGuard(Mutex &mutex) : _mutex(mutex)
        {
            _mutex.Lock();
        }
        ~LockGuard()
        {
            _mutex.Unlock();
        }

    private:
        Mutex &_mutex; // 传的是引用 → 守卫操作的就是同一把锁
    };
}

Thread.hpp

cpp 复制代码
#pragma once

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

namespace ThreadModule
{
    static uint32_t number = 1;
    class Thread
    {
        using func_t = std::function<void()>;

    private:
        void EnableRunning()
        {
            _isRunning = true;
        }
        void EnableDetach()
        {
            _isDetached = true;
        }

        static void *Routine(void *args) // 需要执行的操作是利用传入的this指针使用传入的函数
        {
            Thread *self = static_cast<Thread *>(args);
            self->EnableRunning();
            if (self->_isDetached)
                self->Detach();
            pthread_setname_np(self->_tid, self->_name.c_str());
            self->_func();

            return nullptr; //  不传递任何信息,本意指的是join捕获的类似退出码
        }

    public:
        Thread(func_t func) // 传入函数
            : _tid(0),
              _isRunning(false),
              _isDetached(false),
              res(nullptr),
              _func(func)
        {
            _name = "Thread-" + std::to_string(number++);
        }

        std::string Getname()
        {
            return _name;
        }

        bool Start()
        {
            if (_isRunning)
                return false;
            int n = pthread_create(&_tid, nullptr, Routine, (void *)this);
            if (n == 0)
                return true;
            else
                return false;
        }

        void Detach()
        {
            if (_isDetached)
                return;
            if (_isRunning)
                pthread_detach(_tid);
            EnableDetach();
        }

        bool Stop()
        {
            if (_isRunning)
            {
                int n = pthread_cancel(_tid);
                if (n == 0)
                {
                    _isRunning = false;
                    return true;
                }
                else
                    return false;
            }
            else
                return false;
        }

        void Join()
        {
            if(_isDetached)
                return;
            int n = pthread_join(_tid, &res);
            if(n == 0)
                std::cout << "join回收成功" << std::endl;//
            else
                std::cout << "join失败" << std::endl;
        }

        int Id()
        {
            return _tid;
        }

        ~Thread()
        {
        }

    private:
        std::string _name;
        pthread_t _tid;
        bool _isRunning;
        bool _isDetached;
        void *res;
        func_t _func;
    };
}

Cond.hpp

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include "Mutex.hpp"

using namespace MutexModule;

namespace CondModule
{
    class Cond
    {
    public:
        Cond()
        {
            pthread_cond_init(&_cond, nullptr);
        }
        void Wait(Mutex &mutex)
        {
            int n = pthread_cond_wait(&_cond, mutex.GetMutex());
            (void)n;
        }
        void Signal()
        {
            int n = pthread_cond_signal(&_cond);
            (void)n;
        }
        void Broadcast()
        {
            int n = pthread_cond_broadcast(&_cond);
            (void)n;
        }
        ~Cond()
        {
            pthread_cond_destroy(&_cond);
        }

    private:
        pthread_cond_t _cond;
    };
}

ThreadPool.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include "Log.hpp"
#include "Thread.hpp"
#include "Cond.hpp"
#include "Mutex.hpp"

// .hpp header only

namespace ThreadPoolModule
{
    using namespace ThreadModule;
    using namespace LogModule;
    using namespace CondModule;
    using namespace MutexModule;

    static const int gnum = 5;
    template <typename T>
    class ThreadPool
    {
    private:
        void WakeUpAllThread()
        {
            LockGuard lockguard(_mutex);
            if (_sleepernum)
                _cond.Broadcast();
            LOG(LogLevel::INFO) << "唤醒所有的休眠线程";
        }

        void WakeUpOne()
        {
            _cond.Signal();
            LOG(LogLevel::INFO) << "唤醒一个休眠线程";
        }

        ThreadPool(int num = gnum) : _num(num), _isrunning(false), _sleepernum(0)
        {
            for (int i = 0; i < num; i++)
            {
                _threads.emplace_back(
                    [this]()
                    {
                        HandlerTask();
                    });
            }
        }
        void Start()
        {
            if (_isrunning)
                return;
            _isrunning = true;
            for (auto &thread : _threads)
            {
                thread.Start();
                // LOG(LogLevel::INFO) << "start new thread success: " << thread.Name();
            }
        }

        ThreadPool(const ThreadPool<T> &) = delete;
        ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;

    public:
        static ThreadPool<T> *GetInstance()
        {
            if (inc == nullptr)
            {
                LockGuard lockguard(_lock);

                LOG(LogLevel::DEBUG) << "获取单例....";
                if (inc == nullptr)
                {
                    LOG(LogLevel::DEBUG) << "首次使用单例, 创建之....";
                    inc = new ThreadPool<T>();
                    inc->Start();
                }
            }

            return inc;
        }
        void Stop()
        {
            if (!_isrunning)
                return;
            _isrunning = false;

            // 唤醒所有的线层
            WakeUpAllThread();
        }
        void Join()
        {
            for (auto &thread : _threads)
            {
                thread.Join();
            }
        }
        void HandlerTask()
        {
            char name[128];
            pthread_getname_np(pthread_self(), name, sizeof(name));
            while (true)
            {
                T t;
                {
                    LockGuard lockguard(_mutex);
                    // 1. a.队列为空 b. 线程池没有退出
                    while (_taskq.empty() && _isrunning)
                    {
                        _sleepernum++;
                        _cond.Wait(_mutex);
                        _sleepernum--;
                    }
                    // 2. 内部的线程被唤醒
                    if (!_isrunning && _taskq.empty())
                    {
                        LOG(LogLevel::INFO) << name << " 退出了, 线程池退出&&任务队列为空";
                        break;
                    }

                    // 一定有任务
                    t = _taskq.front(); // 从q中获取任务,任务已经是线程私有的了!!!
                    _taskq.pop();
                }
                t(); // 处理任务,需/要在临界区内部处理吗?1 0
            }
        }
        bool Enqueue(const T &in)
        {
            if (_isrunning)
            {
                LockGuard lockguard(_mutex);
                _taskq.push(in);
                if (_threads.size() == _sleepernum)
                    WakeUpOne();
                return true;
            }
            return false;
        }
        ~ThreadPool()
        {
        }

    private:
        std::vector<Thread> _threads;
        int _num; // 线程池中,线程的个数
        std::queue<T> _taskq;
        Cond _cond;
        Mutex _mutex;

        bool _isrunning;
        int _sleepernum;

        // bug??
        static ThreadPool<T> *inc; // 单例指针
        static Mutex _lock;
    };

    template <typename T>
    ThreadPool<T> *ThreadPool<T>::inc = nullptr;

    template <typename T>
    Mutex ThreadPool<T>::_lock;

}

基础echo message

一、Udpserver.hpp_基础echo版

该服务端想实现一个收到客户端消息并回显的功能

头文件以及命名空间使用

cpp 复制代码
// 四个socket的经典头文件
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
///////////////////////////
#include <iostream>
#include <string>
#include "Log.hpp"// 我们要在使用过程中用日志打印调试信息

using namespace LogModule;

Udpserver类

cpp 复制代码
class UdpServer
{
    // 先来解释下成员变量
private:
    // 在实现回显功能的过程要进行socket,为了所有客户端都能找到服务端,需要ip和port
    // 当然一个服务端可以绑定多个网卡,其实就是多个进程,那这种情况怎么办呢?我们下面来改进
    // _sockfd作为返回值也要放在成员变量里面
    uint16_t _port;
    std::string _ip; // 点分十进制
    size_t _sockfd;

    // 判断是否在运行
    bool _running;

public:
    UdpServer(std::string ip, uint16_t port)
        : _ip(ip), _port(port)
    {
    }
    void Init()
    {
        // 1.打开套接字(网络文件)
        size_t sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        // AF_INET 和 SOCK_DGRAM就可以确定是 udp 协议了
        // 网络       面向数据报
        if (sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket fail";
            exit(1);
        }
        _sockfd = sockfd;
        LOG(LogLevel::INFO) << "socket success";

        // 2.bind
        // 填写结构体
        struct sockaddr_in local;
        bzero(&local, sizeof(local));                   // 将结构体清零,也可使用 memset
        local.sin_port = htons(_port);                  // 转成网络字节序
        local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO
        // inet_addr 用来让c字符串型参数变成整型
        // 手动实现:先分离字符串,然后直接把 4 个字节按"192、168、1、1"的顺序存进结构体------因为 IP 的网络序本来就是"高位字节在低地址",直接按顺序摆字节就是网络序。具体实现看下面代码块
        local.sin_family = AF_INET;
        // 确定地址族

        // bind
        // int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        // 绑定问题->ip问题
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind fail";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind success";
    }
    void Strat()
    {
        // 收发消息
        _running = true;
        while (_running)
        {
            // 收消息
            char buffer[1024];
            struct sockaddr_in peer; // 用来接收客户端的socket信息
            socklen_t len = sizeof(peer);
            int s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); // 因为这里最后一个参数是要即作为输入型参数,又作为输出型参数的,传地址
            if (s < 0)
            {
                uint16_t peer_port = ntohs(peer.sin_port);      // 转换到本地字节序
                std::string peer_ip = inet_ntoa(peer.sin_addr); // 转换成字符串,方便查看
                buffer[s] = 0;
                // 这里可以做封装分层来实现其他功能,后续再改动
                LOG(LogLevel::INFO) << "[" << peer_ip << ":" << peer_port << "]" << buffer;
                // 发消息
                std::string echo_string = "server echo@";
                echo_string += buffer;
                sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
            }
        }
    }
    ~UdpServer()
    {
    }
};
  • 这是一段用来手动实现本地ip转网络的实现代码。
cpp 复制代码
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <arpa/inet.h>

// ========== 核心思想:按顺序存字节的结构体 ==========
// 注意字段顺序:a 是 IP 的最高位字节(192),d 是最低位字节(1)
struct ip_bytes
{
    uint8_t a; // 第1段: 192 (网络序中位于最低地址/最高位)
    uint8_t b; // 第2段: 168
    uint8_t c; // 第3段: 1
    uint8_t d; // 第4段: 1
};

// ========== 方法:分离 -> 存入结构体 -> char* 强转读出 ==========
uint32_t manual_inet_addr(const char *ip_str)
{
    unsigned int a, b, c, d;
    if (sscanf(ip_str, "%u.%u.%u.%u", &a, &b, &c, &d) != 4)
        return INADDR_NONE;
    if (a > 255 || b > 255 || c > 255 || d > 255)
        return INADDR_NONE;

    // 把分离出的四段按顺序放进结构体
    struct ip_bytes ip;
    ip.a = a;
    ip.b = b;
    ip.c = c;
    ip.d = d;

    // 关键一步:把结构体强转成 uint32_t*
    // 结构体在内存中就是连续的4字节:a b c d
    // 而 sin_addr 需要的正是内存中按 a b c d 排列的字节!
    uint32_t result;
    memcpy(&result, &ip, sizeof(result)); // 等价于 *(uint32_t*)&ip,但更规范
    return result;
}

int main(void)
{
    const char *ip = "192.168.1.1";

    uint32_t mine = manual_inet_addr(ip);
    uint32_t sys  = inet_addr(ip);

    printf("IP: %s\n", ip);
    printf("手动(结构体+强转): 0x%08X\n", mine);
    printf("系统 inet_addr   : 0x%08X\n", sys);
    printf("一致: %s\n\n", mine == sys ? "是 ✅" : "否 ❌");

    // ---------- 用 char* 验证内存布局 ----------
    printf("强转 uint32_t 后的内存 (低地址 -> 高地址):\n  ");
    char *p = (char *)&mine;
    for (int i = 0; i < 4; i++)
        printf("%02X ", p[i]);
    // 小端机器上会打印: 01 01 A8 C0 ------ 因为整数本身按小端存

    printf("\n\n结构体 ip_bytes 的内存 (低地址 -> 高地址):\n  ");
    struct ip_bytes ip2;
    sscanf(ip, "%u.%u.%u.%u", &ip2.a, &ip2.b, &ip2.c, &ip2.d);
    char *q = (char *)&ip2;
    for (int i = 0; i < 4; i++)
        printf("%02X ", q[i]);
    // 打印: C0 A8 01 01 ------ 恰好就是网络序的字节排列!

    // ---------- 直接填进 sockaddr_in ----------
    struct sockaddr_in local;
    bzero(&local, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(8080);
    memcpy(&local.sin_addr.s_addr, &ip2, 4); // 4字节直接搬进去

    printf("\n\nsin_addr 内存: ");
    char *s = (char *)&local.sin_addr;
    for (int i = 0; i < 4; i++)
        printf("%02X ", s[i]);
    printf("\n");
    printf("inet_ntoa 还原: %s\n", inet_ntoa(local.sin_addr)); // 192.168.1.1

    return 0;
}
  • 运行结果
bash 复制代码
IP: 192.168.1.1
手动(结构体+强转): 0xC0A80101
系统 inet_addr   : 0xC0A80101
一致: 是 ✅

强转 uint32_t 后的内存 (低地址 -> 高地址):
  01 01 A8 C0

结构体 ip_bytes 的内存 (低地址 -> 高地址):
  C0 A8 01 01     ← 这就是网络序!

sin_addr 内存: C0 A8 01 01
inet_ntoa 还原: 192.168.1.1

注意到上面有一条:local.sin_addr.s_addr = inet_addr(_ip.c_str());//TODO

是的,这里实际上有些问题:

一台服务器上有可能会有许多网卡,这就可能导致一台机器的IP地址不止一个。

所以当我们服务端只绑定一个固定IP地址的时候,我们就不能利用其他IP地址来接收信息,或者进行网络通信。

  • 那解决方法是什么呢?

我们不显示的传IP地址:

c 复制代码
local.sin_addr.s_addr = INADDR_ANY;

INADDR_ANY实际上是0,表示"监听本机的所有网络接口(网卡)上的 IP 地址"

如此一来,其他代码改变为:

cpp 复制代码
#pragma once
// 这个头文件的内容是实现UPD套接字的使用

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/un.h>
#include "Log.hpp"

using namespace LogModule;
class UdpServer
{
public:
    UdpServer(uint16_t port)
        : _port(port)
    {
    }
    void Init()
    {
        // 1.打开套接字(网络文件)
        size_t sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if(sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket fail";
            exit(1);
        }
        _sockfd = sockfd;
        LOG(LogLevel::INFO) << "socket success";


        // 2.bind
        // 填写结构体
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_port = htons(_port);
        // local.sin_addr.s_addr = inet_addr(_ip.c_str());//TODO
        local.sin_addr.s_addr = INADDR_ANY;
        local.sin_family = AF_INET;

        // bind
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        // 绑定问题->ip问题
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "bind fail";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind success";
    }
    void Strat()
    {
        // 收发消息
        _running = true;
        while(_running)
        {
            // 收消息
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if(s > 0)
            {
            	uint16_t peer_port = ntohs(peer.sin_port);
            	std::string peer_ip = inet_ntoa(peer.sin_addr);
           		buffer[s] = 0;


           		 // LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port << "]" << buffer;
           		 // 发消息
           		 std::string echo_string = "server echo@";
           		 echo_string += buffer;
           		 sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);   
            }
        }
    }
    ~UdpServer()
    {
    }

private:
    uint16_t _port;
    // std::string _ip; // 点分十进制
    size_t _sockfd;
    
    bool _running;
};

二、Udpserver.cpp_基础echo版

由于取消了必须显式绑定ip,所以main只需要传两个参数就够了

cpp 复制代码
#include <iostream>
#include <cstring>
#include <memory>
#include "Udpserver.hpp"

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

    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(std::stoi(argv[1]));
    usvr->Init();
    usvr->Strat();
    return 0;
}

三、Udpclient.cpp_基础echo版

cpp 复制代码
#include <iostream>
#include <string>
#include <cstring> // memset的头
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int main(int argc, char *argv[])
{
    // ./Udpclient ip port
    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]);

    // 打开socket
    size_t sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == 0)
    {
        std::cout << "socket error" << std::endl;
        return 2;
    }
    std::cout << "socket success" << std::endl;

    //  填写 sockaddr_in 结构体
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());
    server.sin_port = htons(server_port);

    // bind
    // 说明一点:bind是bind自己的socket
    // 那么这里需要bind吗?
    // 必然是需要的,但是不需要显式bing
    // 首次发送消息,OS会自动给client进行bind,OS知道IP,端口号采用随机端口号的方式
    // (client每次都是率先发消息的,所以只需要确保自己的socket信息能够发出即可,并不在乎是哪个ip和port → 客户端的端口"是几"根本不重要)


    while(true)
    {
        std::string input;
        std::cout << "Please Enter# ";
        getline(std::cin, input);
        // 发消息
        int n = sendto(sockfd, input.c_str(), input.size(), 0, (sockaddr *)&server, sizeof(server));
        (void)n;
        // 收消息
        struct sockaddr_in peer;
        char buffer[1024];
        socklen_t len = sizeof(peer);
        int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (sockaddr *)&peer, &len);
        if (m > 0)
        {
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
    }

    return 0;
}

简单在这里说明一下c++的输入函数:

方法 形式 读空格 读换行 缓冲区
>> 提取运算符 cin >> x ❌ 停在空白 ❌ 安全
std::getline getline(cin, str) ✅ ✅(丢弃) 自动管理 ⭐
cin.getline cin.getline(buf, n) ✅ ✅(丢弃) 固定缓冲区
cin.get cin.get() / cin.get(buf,n) ✅ ❌(保留) 可变
cin.read cin.read(buf, n) ✅ 原始 ❌ 二进制读取
cin.ignore 辅助方法 --- --- 清空缓冲区

简单的翻译功能

接下来我们对上面这部分代码做一下封装分层和功能调整

一、Udpserver.hpp_翻译版

这部分的代码没有很大的调整,只是添加了function函数对象的封装。在外部传入不同的任务,服务器即可执行不同功能。

cpp 复制代码
#pragma once
// 这个头文件的内容是实现UPD套接字的使用

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Inetaddr.hpp"
#include "Log.hpp"

using namespace LogModule;

using func_t = std::function<std::string(const std::string &, InetAddr &)>;

class UdpServer
{
public:
    UdpServer(uint16_t port, func_t func)
        : _port(port), _func(func)
    {
    }
    void Init()
    {
        // 1.打开套接字(网络文件)
        size_t sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket fail";
            exit(1);
        }
        _sockfd = sockfd;
        LOG(LogLevel::INFO) << "socket success";

        // 2.bind
        // 填写结构体
        InetAddr local(_port);

        // bind
        int n = bind(_sockfd, CONV(local), local.NetAddrLen());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind fail";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind success";
    }
    void Strat()
    {
        // 收发消息
        _running = true;
        while (_running)
        {
            // 收消息
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (s > 0)
            {
                uint16_t peer_port = ntohs(peer.sin_port);
                std::string peer_ip = inet_ntoa(peer.sin_addr);
                buffer[s] = 0;

                // 添加函数对象让功能和网络服务器发送分开
                InetAddr client(peer);
                std::string result = _func(buffer, client);

                // 发消息
                sendto(_sockfd, result.c_str(), result.size(), 0, CONV(client), client.NetAddrLen());
            }
        }
    }
    ~UdpServer()
    {
    }

private:
    uint16_t _port;
    size_t _sockfd;

    bool _running;
    func_t _func;
};

值得注意的是,这里引入了一个新的类:InetAddr

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// 网络地址和主机地址之间进行转换的类

#define CONV(addr) ((struct sockaddr*)&addr)

class InetAddr
{
public:
    InetAddr(struct sockaddr_in &addr) : _addr(addr)
    {
        // 网络转本地
        _port = ntohs(_addr.sin_port); // 从网络中拿到的!网络序列
        //_ip = inet_ntoa(_addr.sin_addr); // 4字节网络风格的IP -> 点分十进制的字符串风格的IP
        // inet_ntoa()所有调用共享同一个静态缓冲区,后一次调用会直接覆盖前一次的结果,有更好的选择
        char buffer[128];
        inet_ntop(_addr.sin_family, &_addr.sin_addr.s_addr, buffer, sizeof(buffer));
        _ip = buffer;
    }
    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(_addr.sin_family, ip.c_str(), &_addr.sin_addr.s_addr);
    }
    InetAddr(uint16_t port) : _port(port)
    {
        // 本地转网络
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        _addr.sin_addr.s_addr = INADDR_ANY;
    }

    uint16_t Port() { return _port; }
    std::string Ip() { return _ip; }
    const struct sockaddr_in &NetAddr() { return _addr; }
    const struct sockaddr *NetAddrPtr()
    {
        return CONV(_addr);
    }
    socklen_t NetAddrLen()
    {
        return sizeof(_addr);
    }
    bool operator==(const InetAddr &addr)
    {
        return addr._ip == _ip && addr._port == _port;
    }
    std::string StringAddr()
    {
        return _ip + ":" + std::to_string(_port);
    }

    ~InetAddr()
    {
    }

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

这个类是整个翻译版服务端的「地址门面」:它把 struct sockaddr_in 这个又长又难用的结构体包起来,对外只暴露 Ip()、Port()、NetAddr()、NetAddrLen() 等语义清晰的接口,让上层代码不必再关心字节序转换、sockaddr 强转这些细节。

具体来说,它承担了三件事:

  • 网络转本地 :从 recvfrom 拿到的 sockaddr_in 里,端口是网络字节序、IP 是 4 字节整数。构造函数里用 ntohs 把端口转回本地字节序,用 inet_ntop 把 IP 转成点分十进制字符串,这样日志里打印出来的就是人能看懂的 192.168.1.1:8080。
  • 本地转网络 :服务端 bind 之前需要自己构造地址,于是提供了 InetAddr(uint16_t port) 这个只传端口的构造,内部把 sin_addr.s_addr 设为 INADDR_ANY,表示监听本机所有网卡;另一个 InetAddr(ip, port) 则用于需要指定具体 IP 的场景。
  • 统一类型转换 :bind、sendto、recvfrom 这些系统调用要的都是 struct sockaddr*,而实际填的是 sockaddr_in。类里用 CONV 宏和 NetAddrPtr() 把这一步强转封装掉,调用处写 CONV(local) 即可,既简洁又不容易写错。

正因为有了这层封装,UdpServer::Init() 里原本一大段「填结构体、转字节序、强转指针」的样板代码,才能被压缩成 InetAddr local(_port); 加一行 bind 这么清爽。

这里必须要提及一下 inet_aton、inet_pton、inet_ntoa、inet_ntop 这四兄弟------它们都是 IP 地址在「点分十进制字符串」和「网络字节序整数」之间转换的函数,但分属两代 API,安全性和用法差别很大。InetAddr 类里之所以注释掉 inet_ntoa 而改用 inet_ntop,正是因为前者返回的是函数内部的静态缓冲区,多次调用会互相覆盖,在多线程或连续打印多个地址时会踩坑。下面就把这四者的区别和安全性彻底讲清楚.

inet_aton、inet_pton和inet_ntoa和inet_ntop的安全性和区别

1. 先按方向分组

方向 老 API(IPv4 专用) 新 API(IPv4/IPv6 通用)
字符串 → 网络字节序整数 inet_aton inet_pton
网络字节序整数 → 字符串 inet_ntoa inet_ntop
  • inet_aton / inet_ntoa:来自早期的 arpa/inet.h,只支持 IPv4,且 inet_ntoa 返回的是静态缓冲区。
  • inet_pton / inet_ntop:带 p(presentation,表示形式)和 n(numeric,数值形式),同时支持 AF_INET 和 AF_INET6,由调用者提供缓冲区。

2. 逐个看用法与坑

inet_aton:字符串 → struct in_addr

cpp 复制代码
int inet_aton(const char *cp, struct in_addr *inp);
  • 成功返回非 0,失败返回 0。
  • 它把结果写进你传入的 in_addr,不涉及静态缓冲区,本身是线程安全的。
  • 但它只认 IPv4,且对非法输入(如 "999.1.1.1")的判定比较宽松,历史上还支持 "1.2.3"、"0x7f.1" 这类简写,容易埋坑。

inet_ntoa:struct in_addr → 字符串(有坑!)

cpp 复制代码
char *inet_ntoa(struct in_addr in);
  • 返回值指向函数内部的一块静态缓冲区,所有调用共享同一块内存。
  • 这意味着:非线程安全,且连续两次调用会互相覆盖。比如:
cpp 复制代码
struct in_addr a, b;
// ... 分别赋值 ...
printf("%s %s\n", inet_ntoa(a), inet_ntoa(b)); // 两个参数可能打印出同一个地址!

因为参数求值顺序不确定,两次 inet_ntoa 可能都写进同一块缓冲区,后一次覆盖前一次。这正是文章里 InetAddr 类注释掉 inet_ntoa、改用 inet_ntop 的原因。

inet_pton:字符串 → 数值(推荐)

cpp 复制代码
int inet_pton(int af, const char *src, void *dst);
  • af 传 AF_INET 或 AF_INET6,dst 由调用者提供(IPv4 传 struct in_addr*,IPv6 传 struct in6_addr*)。
  • 返回 1 表示成功,0 表示 src 不是合法地址,-1 表示 af 不支持(并设置 errno)。
  • 严格校验格式,不认简写,线程安全。

inet_ntop:数值 → 字符串(推荐)

cpp 复制代码
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
  • 缓冲区 dst 和大小 size 由调用者提供,因此线程安全,也不会互相覆盖。
  • 缓冲区大小建议用宏:IPv4 用 INET_ADDRSTRLEN(16),IPv6 用 INET6_ADDRSTRLEN(46)。
  • 成功返回 dst,失败返回 NULL 并设置 errno。

3. 安全性对比小结

对比项 inet_aton / inet_ntoa inet_pton / inet_ntop
线程安全 inet_ntoa 不安全 (静态缓冲区);inet_aton 安全 都安全
缓冲区归属 inet_ntoa 由库内部管理 由调用者提供,可控
协议支持 仅 IPv4 IPv4 + IPv6
输入校验 宽松,接受简写 严格
推荐程度 仅维护老代码时使用 新代码首选

4. 落到本文的 InetAddr 类

cpp 复制代码
// 不推荐:静态缓冲区,多次调用会互相覆盖
// _ip = inet_ntoa(_addr.sin_addr);

// 推荐:自己提供缓冲区,线程安全
char buffer[INET_ADDRSTRLEN];
inet_ntop(_addr.sin_family, &_addr.sin_addr.s_addr, buffer, sizeof(buffer));
_ip = buffer;

同理,字符串转数值时用 inet_pton 替代 inet_addr / inet_aton:

cpp 复制代码
inet_pton(AF_INET, ip.c_str(), &_addr.sin_addr.s_addr);

一句话记忆:带 p 的是新一代、线程安全、支持 IPv6,新代码一律用它;inet_ntoa 的静态缓冲区是经典陷阱,能不用就不用。

区别

二、Udpserver.cpp_翻译版

由于在上方添加了function函数对象作为构造函数的参数,所以这里无可避免地要添加一个函数:

cpp 复制代码
std::unique_ptr<UdpServer> 
    	usvr = std::make_unique<UdpServer>(port, [&dict](const std::string &word, InetAddr &client)
           { return dict.Translate(word, client); });

函数采用lambda表达式的方式传入,因为要调用Dict,这里传入dict。

在此之前我们要解释Dict是什么和创建Dict对象并配置文件。

cpp 复制代码
// 翻译单词、录入词典文件
#pragma once
#include <fstream>
#include <iostream>
#include <string>
#include <unordered_map>
#include "Inetaddr.hpp"

const std::string defaultdict = "./dictionary.txt";
const std::string sep = ": ";

class Dict
{
public:
    Dict(const std::string &path = defaultdict) : _pathfile(path)
    {
    }
    bool LoadDict()
    {
        std::ifstream in(_pathfile);
        if(!in.is_open())
        {
            LOG(LogLevel::FATAL) << "配置失败...";
            return false;
        }
        std::string line;
        while(getline(in, line))
        {
            int pos = line.find(sep);
            if(pos == std::string::npos)
            {
                LOG(LogLevel::WARNING) << "格式错误:" << line;
                continue;
            }
            std::string english = line.substr(0, pos);
            std::string chinese = line.substr(pos + sep.size());
            if(chinese.empty() || english.empty())
            {
                LOG(LogLevel::WARNING) << "格式错误:" << line;
                continue;
            }
            _dict.insert(std::make_pair(english, chinese));
            LOG(LogLevel::DEBUG) << "加载成功: " << line;
        }

        LOG(LogLevel::INFO) << "配置完成!";
        in.close();
        return true;
    }

    std::string Translate(const std::string &word, InetAddr &client)
    {
        auto iter = _dict.find(word);

        if(iter == _dict.end())
        {
            LOG(LogLevel::DEBUG) << "进入到了翻译模块, [" << client.Ip() << " : " << client.Port() << "]# " << word << "不存在";
            std::string result = word + "不存在";
            return result;
        }
        LOG(LogLevel::INFO) << "进入到了翻译模块, [" << client.Ip() << " : " << client.Port() << "]# " << word << iter->second;
        return iter->second;
    }

    ~Dict()
    {
    }

private:
    std::string _pathfile;
    std::unordered_map<std::string, std::string> _dict;
};

这里的主要函数有两个,一个是LoadDict,一个是Translate。

LoadDict是利用文件操作加载加载TXT文件内的英汉互译到哈希表中,方便查询。

Translate是利用哈希表做键值对的查询,并输出日志。

cpp 复制代码
#include <iostream>
#include <cstring>
#include <memory>
#include "Inetaddr.hpp"
#include "Udpserver.hpp"
#include "Dict.hpp"

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

    Dict dict;
    dict.LoadDict();

    std::unique_ptr<UdpServer> 
    	usvr = std::make_unique<UdpServer>(port, [&dict](const std::string &word, InetAddr &client)
           { return dict.Translate(word, client); });
    usvr->Init();
    usvr->Strat();
    return 0;
}

三、Udpclient.cpp_翻译版

cpp 复制代码
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int main(int argc, char *argv[])
{
    // ./Udpclient ip port
    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]);

    // 打开socket
    size_t sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cout << "socket error" << std::endl;
        return 2;
    }
    std::cout << "socket success" << std::endl;

    //  填写 sockaddr_in 结构体
    InetAddr server(server_ip, server_port);

    while(true)
    {
        std::string input;
        std::cout << "Please Enter# ";
        getline(std::cin, input);
        // 发消息
        int n = sendto(sockfd, input.c_str(), input.size(), 0, CONV(server), server.NetAddrLen());
        (void)n;
        // 收消息
        struct sockaddr_in peer;
        char buffer[1024];
        socklen_t len = sizeof(peer);
        int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (sockaddr *)&peer, &len);
        if (m > 0)
        {
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
    }
    return 0;
}

简单的聊天室功能

下面继续对UDP的代码做升级和功能调整

上面都是单线程去处理回显消息或者是翻译的任务,接下来我们加入线程池来加快服务端的速度

一、Udpserver.hpp_聊天室版

Udpserver.hpp与之前相比没有变化,可以忽略。

cpp 复制代码
#pragma once
// 这个头文件的内容是实现UPD套接字的使用

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Inetaddr.hpp"
#include "Log.hpp"

using namespace LogModule;

using func_t = std::function<void(const size_t &, const std::string &, InetAddr &)>;

class UdpServer
{
public:
    UdpServer(uint16_t port, func_t func)
        : _port(port), _func(func)
    {
    }
    void Init()
    {
        // 1.打开套接字(网络文件)
        size_t sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket fail";
            exit(1);
        }
        _sockfd = sockfd;
        LOG(LogLevel::DEBUG) << "socket success";

        // 2.bind
        // 填写结构体
        InetAddr local(_port);

        // bind
        int n = bind(_sockfd, CONV(local), local.NetAddrLen());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind fail";
            exit(2);
        }
        LOG(LogLevel::DEBUG) << "bind success";
    }
    void Strat()
    {
        // 收发消息
        _running = true;
        while (_running)
        {
            // 收消息
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (s > 0)
            {
                uint16_t peer_port = ntohs(peer.sin_port);
                std::string peer_ip = inet_ntoa(peer.sin_addr);
                buffer[s] = 0;

                // 添加函数对象让功能和网络服务器发送分开
                InetAddr client(peer);
                _func(_sockfd, buffer, client);
            }
        }
    }
    ~UdpServer()
    {
    }

private:
    uint16_t _port;
    size_t _sockfd;

    bool _running;
    func_t _func;
};

二、Udpserver.cpp_聊天室版

.cpp之中变化的点是路由类的加入:

Doute的功能是对于在线用户的管理和服务端收到的消息进行分发,这里是简略实现的,想要更加严谨的版本可以手动升级。

cpp 复制代码
#include <iostream>
#include <string>
#include <list>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Mutex.hpp"
#include "Inetaddr.hpp"

using namespace MutexModule;

class Route
{
private:
    // 判断是不是新用户
    bool IsNewUser(InetAddr &peer)
    {
        for (auto u : _users)
        {
            if (peer == u)
                return false;
        }
        return true;
    }

    // 新增用户
    void AddUser(InetAddr &peer)
    {
        LOG(LogLevel::INFO) << "新增了一个用户:" << peer.StringAddr();
        _users.push_back(peer);
    }

    // 广播消息
    void BroadCast(const size_t &socket, const std::string &message, InetAddr &peer)
    {
        std::string send_message = peer.StringAddr() + " @ " + message;
        for (auto u : _users)
        {
            sendto(socket, send_message.c_str(), send_message.size(), 0, CONV(u), sizeof(u));
        }
    }

    // 判断并退出
    void Quit(const std::string &message, InetAddr &peer)
    {
        if (message == "QUIT")
        {
            for (auto iter = _users.begin(); iter != _users.end(); iter++)
            {
                if (peer == *iter)
                {
                    LOG(LogLevel::INFO) << "离线了一个用户:" << peer.StringAddr();
                    _users.erase(iter);
                    break;
                }
            }
        }
    }

public:
    Route()
    {
    }
    void MessageRoute(const size_t &socket, const std::string &message, InetAddr &peer)
    {
        // 访问的是共享资源(_users)
        LockGuard lockguard(_mutex);

        // 判断是不是新用户,是则加入
        if (IsNewUser(peer))
            AddUser(peer);

        // 将消息发送给所有人
        BroadCast(socket, message, peer);

        // 消息是QUIT就退出
        Quit(message, peer);
    }
    ~Route()
    {
    }

private:
    std::list<InetAddr> _users;
    Mutex _mutex;
};
cpp 复制代码
#include <iostream>
#include <cstring>
#include <memory>
#include <functional>
#include "Udpserver.hpp"
#include "Route.hpp"
#include "ThreadPool.hpp"

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

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

    Route route;

    auto tp = ThreadPool<task_t>::GetInstance();

    std::unique_ptr<UdpServer>
        usvr = std::make_unique<UdpServer>(port, [&tp, &route](const size_t &socket, const std::string &message, InetAddr &client)
            {
                task_t t= std::bind(&Route::MessageRoute,&route,socket, message, client);
                tp -> Enqueue(t);
             });
    usvr->Init();
    usvr->Strat();
    return 0;
}

三、Udpclient.cpp_聊天室版

这这个模块中比较重要的一点是由于多用户的加入,收消息和发消息必须实现非阻的分别进行,于是这里用两个线程来实现。

cpp 复制代码
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Thread.hpp"
#include "Inetaddr.hpp"

using namespace ThreadModule;

int sockfd = 0;
std::string server_ip;
uint16_t server_port = 0;
pthread_t id;

void Send()
{
    InetAddr server(server_ip, server_port);
    while (true)
    {
        std::string input;
        std::cout << "Please Enter# ";
        getline(std::cin, input);
        // 发消息
        int n = sendto(sockfd, input.c_str(), input.size(), 0, CONV(server), server.NetAddrLen());
        (void)n;

        if (input == "QUIT")
        {
            pthread_cancel(id);
            break;
        }
    }
}
void Recv()
{
    // 收消息
    while (true)
    {
        struct sockaddr_in peer;
        char buffer[1024];
        socklen_t len = sizeof(peer);
        int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (sockaddr *)&peer, &len);
        if (m > 0)
        {
            buffer[m] = 0;
            std::cerr << buffer << std::endl;
        }
    }
}

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

    // 打开socket
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cout << "socket error" << std::endl;
        return 2;
    }
    std::cout << "socket success" << std::endl;

    // 客户端改用多线程解决阻塞时只能处理一个用户的bug
    Thread sender(Send);
    Thread recver(Recv);

    sender.Start();
    recver.Start();

    id = recver.Id();

    sender.Join();
    recver.Join();

    return 0;
}

  • 本节完...

📝 作者:say-fall | 编辑:say-fall | 🌟 原创不易,如果对你有帮助,记得 👍 点赞 + ⭐ 收藏 哦!

相关推荐
ö Constancy30 分钟前
一台电脑的硬件组成与作用:按功能分类详解(Windows 与 Linux 视角)
linux·windows·电脑
江湖有缘36 分钟前
基于Ubuntu部署SoduWork:企业级 AI 原生协同平台实战全流程
linux·人工智能·ubuntu·ai·soduwork
无奈笑天下9 小时前
银河麒麟上nproc命令教程详解
linux·运维·服务器
我不会插花弄玉12 小时前
6.传输层协议UDP【由浅入深-网络】
网络·网络协议·udp
Rabitebla13 小时前
【Linux】进程控制(十二) 自定义 Shell(二):内建命令与进程工作目录管理
linux·c++·笔记·学习·bash
火云代理13 小时前
更换网络后为什么会退出登录?分清 IP、Cookie 与会话的关系
服务器·网络·网络协议·tcp/ip·php·ip
威嵌神州13 小时前
嵌入式排故实录:高并发下串口数据“超时丢包”的根因剖析与架构优化
运维·服务器·嵌入式硬件·缓存
xbzb16 小时前
容器 IP 每次重建都变?docker network 五种模式与自定义网桥固定地址
网络·docker·容器·ip·network·固定
火云代理17 小时前
同一个网站,有的网络能打开、有的打不开?认识 IPv4 与 IPv6 的访问差异
服务器·网络·网络协议·tcp/ip·测试工具·php·状态模式