【Linux网络】Socket编程实战,基于UDP协议的Echo Server

前言:

上文我们讲解了Socket编程的预备【Linux网络】套接字Socket编程预备-CSDN博客

本文我们来讲解一下使用Socket编程基于UDP协议的网络通信:Echo Server,回显服务。


服务器端实现

大致分为两大步:

初始化阶段:1.创建套接字 2.填充并绑定Socket信息

启动服务器:1.接收客户端的信息 2.回显消息

初始化阶段

创建套接字

cpp 复制代码
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

domain(地址族 / 协议族):指定网络层协议,决定套接字使用的地址格式。常见取值:AF_INET:IPv4 协议(最常用)

type(套接字类型):指定传输层协议的特性。常见取值:SOCK_DGRAM:数据报套接字

protocol(具体协议):指定使用的传输层协议。通常设为 0,表示根据 domain 和 type 自动选择默认协议

返回值:
成功:返回一个非负整数(套接字描述符,类似文件描述符,用于后续操作)
失败:返回 -1,并设置全局变量 errno 表示错误原因(如 EAFNOSUPPORT 表示地址族不支持)

绑定信息

cpp 复制代码
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockfd(套接字描述符)
addr(地址结构指针)
addrlen(地址结构长度)

返回值
成功:返回 0,表示套接字已成功绑定到指定地址
失败:返回 -1,表示失败

在上一篇文章我们讲过。对于struct sockaddr* 参数,要看我们想要进行什么类型的通信来决定。如果想要进行网络通信就要传入 struct sockaddr_in的结构体指针;而如果是进行本地通信就要传入 struct sockaddr_un结构体指针。

这里我们是进行的网络通信的,所以下面我们就来看看 struct sockaddr_in结构体:

地址家族:

第一个成员变量表示地址家族。

##:表示将左右两个字符串合并为一个,既 sin_family。

sin_family的类型为 sa_family_t,其本质是短整型变量。

端口号:

其本质就是短整型变量。

IP地址:

其本质是一个in_addr的结构体对象。

结构体in_addr中只有一个成员变量:表示IP地址。其本质是整型变量。

具体实现

cpp 复制代码
class udpserver
{
public:
    udpserver(string& addr, uint16_t port)
        : _addr(inet_addr(addr.c_str())), //注:直接将其转换为合法的ip地址
        _port(port)
    {
        _running = false;
    }

    // 初始化:1.创建套接字 2.填充并绑定地址信息
    void Init()
    {
        // 1.创建套接字
        // 返回套接字描述符 地址族 数据类型 传输协议
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "创建套接字失败!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "创建套接字";

        // 2.绑定信息
        // 2.1填充信息
        struct sockaddr_in local;
        // 将指定内存块的所有字节清零
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;    // IPv4地址族
        local.sin_addr.s_addr = _addr; // IP地址(主机序列转化为网络序列)
        local.sin_port = htons(_port); // 端口号

        // 2.2绑定信息
        int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "绑定失败";
            exit(1);
        }
        LOG(LogLevel::INFO) << "绑定成功";
    }

private:
    int _sockfd;
    uint32_t _addr;
    uint16_t _port;
    bool _running;
}

启动服务器阶段

接收客户端信息

cpp 复制代码
#include <sys/socket.h>

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


sockfd:套接字描述符
buf:指向接收数据的缓冲区
len:缓冲区 buf 能接收的字节数
flags:控制接收行为的标志位,通常为 0
src_addr:指向一个 sockaddr 结构体,用于存储发送方的地址信息
addrlen:指向 socklen_t 类型的指针,用于指定 src_addr 缓冲区的大小(传入时),并返回实际存储的地址长度(传出时)


返回值
成功时:返回接收到的字节数
失败时:返回 -1

向客户端发送信息

cpp 复制代码
#include <sys/socket.h>

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

sockfd:套接字描述符
buf:指向待发送数据的缓冲区
len:待发送数据的长度
flags:控制发送行为的标志位,通常为 0(无特殊行为)
dest_addr:指向目标地址结构体的指针,用于指定数据发送的目标主机和端口
addrlen:结构体的大小


返回值
成功时:返回实际发送的字节数(ssize_t 类型,非负整数),通常等于 len(除非发生截断或错误)
失败时:返回 -1

具体实现

cpp 复制代码
class udpserver
{
public:
    // 启动运行:一直运行不停止;1.接收客户端的信息 2.对客户端发送来的信息进行回显
    void Start()
    {
        // 一定是死循环
        _running = true;
        while (_running)
        {
            // 接收客户端的信息
            char buff[1024];
            struct sockaddr_in peer;
            unsigned int len = sizeof(peer);
            // 套接字描述符,数据存放的缓冲区,接收方式:默认,保存发送方的ip与端口,输入输出参数:输入peer的大小,输出实际读取的数据大小
            ssize_t s = recvfrom(_sockfd, buff, sizeof(buff) - 1, 0, (struct sockaddr*)&peer, &len);
            printf("%s\n", buff);

            // 回显消息
            if (s > 0)
            {
                // 将数据发送给客户端
                buff[s] = 0;
                // 套接字描述符,要发送的信息,发送方式:默认,接收方的ip与端口信息
                // 不应发送整个缓冲区(sizeof(buff)),而应发送实际接收的字节数 s
                ssize_t t = sendto(_sockfd, buff, s, 0, (struct sockaddr*)&peer, len);
                if (t < 0)
                {
                    LOG(LogLevel::WARNING) << "信息发送给客户端失败";
                }
            }
            memset(&buff, 0, sizeof(buff)); // 清理缓存
        }
    }

private:
    int _sockfd;
    uint32_t _addr;
    uint16_t _port;
    bool _running;
}

服务器实现

cpp 复制代码
//UdpServer.hpp

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include "Log.hpp"
using namespace LogModule;

class udpserver
{
public:
    udpserver(string &addr, uint16_t port)
        : _addr(inet_addr(addr.c_str())), // 注:直接将其转换为合法的ip地址
          _port(port)
    {
        _running = false;
    }

    // 初始化:1.创建套接字 2.填充并绑定地址信息
    void Init()
    {
        // 1.创建套接字
        // 返回套接字描述符 地址族 数据类型 传输协议
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "创建套接字失败!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "创建套接字";

        // 2.绑定信息
        // 2.1填充信息
        struct sockaddr_in local;
        // 将指定内存块的所有字节清零
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;    // IPv4地址族
        local.sin_addr.s_addr = _addr; // IP地址(主机序列转化为网络序列)
        local.sin_port = htons(_port); // 端口号

        // 2.2绑定信息
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "绑定失败";
            exit(1);
        }
        LOG(LogLevel::INFO) << "绑定成功";
    }

    // 启动运行:一直运行不停止;1.接收客户端的信息 2.对客户端发送来的信息进行回显
    void Start()
    {
        // 一定是死循环
        _running = true;
        while (_running)
        {
            // 接收客户端的信息
            char buff[1024];
            struct sockaddr_in peer;
            unsigned int len = sizeof(peer);
            // 套接字描述符,数据存放的缓冲区,接收方式:默认,保存发送方的ip与端口,输入输出参数:输入peer的大小,输出实际读取的数据大小
            ssize_t s = recvfrom(_sockfd, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&peer, &len);
            printf("%s\n", buff);

            // 回显消息
            if (s > 0)
            {
                // 将数据发送给客户端
                buff[s] = 0;
                // 套接字描述符,要发送的信息,发送方式:默认,接收方的ip与端口信息
                // 不应发送整个缓冲区(sizeof(buff)),而应发送实际接收的字节数 s
                ssize_t t = sendto(_sockfd, buff, s, 0, (struct sockaddr *)&peer, len);
                if (t < 0)
                {
                    LOG(LogLevel::WARNING) << "信息发送给客户端失败";
                }
            }
            memset(&buff, 0, sizeof(buff)); // 清理缓存
        }
    }

private:
    int _sockfd;
    uint32_t _addr;
    uint16_t _port;
    bool _running;
};
cpp 复制代码
//UdpServer.cc

#include "UdpServer.hpp"
#include <cstdlib>

// 给出 ip地址 端口号
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "Please use: " << argv[0] << " IP " << "PORT " << endl;
    }
    else
    {
        string ip = argv[1];
        uint16_t port = stoi(argv[2]); // 注:字符串转整数
        udpserver us(ip, port);
        us.Init();
        us.Start();
    }
}
cpp 复制代码
//Log.hpp

// 实现日志模块

#pragma once
#include <iostream>
#include <sstream>    // 包含stringstream类
#include <filesystem> //C++17文件操作接口库
#include <fstream>
#include <sys/types.h>
#include <unistd.h>
#include "Mutex.hpp"
using namespace std;
using namespace MutexModule;

// 补充:外部类只能通过内部类的实例化对象,来访问内部类中的方法与成员,且受修饰符限制
//       内部类可以直接访问外部类的方法以及成员,没有限制

namespace LogModule
{
    const string end = "\r\n";

    // 实现刷新策略:a.向显示器刷新 b.向指定文件刷新

    // 利用多态机制实现
    // 包含至少一个纯虚函数的类称为抽象类,不能实例化,只能被继承
    class LogStrategy // 基类
    {
    public:
        //"=0"声明为纯虚函数。纯虚函数强制派生类必须重写该函数
        virtual void SyncLog(const string &message) = 0;
    };

    // 向显示器刷新:子类
    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        void SyncLog(const string &message) override
        {
            // 加锁,访问显示器,显示器也是临界资源
            LockGuard lockguard(_mutex);
            cout << message << end;
        }

    private:
        Mutex _mutex;
    };

    // 向指定文件刷新:子类
    const string defaultpath = "./log";
    const string defaultfile = "my.log";
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const string &path = defaultpath, const string &file = defaultfile)
            : _path(path), _file(file)
        {
            LockGuard lockguard(_mutex);
            // 判断路径是否存在,如果不存在就创建对应的路径
            if (!(filesystem::exists(_path)))
                filesystem::create_directories(_path);
        }

        void SyncLog(const string &message) override
        {
            // 合成最后路径
            string Path = _path + (_path.back() == '/' ? "" : "/") + _file;
            // 打开文件
            ofstream out(Path, ios::app);
            out << message << end;
        }

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

    //

    // 日志等级
    // enum class:强类型枚举。1.必须通过域名访问枚举值 2.枚举值不能隐式类型转化为整型
    enum class LogLevel
    {
        DEBUG,   // 调试级
        INFO,    // 信息级
        WARNING, // 警告级
        ERROR,   // 错误级
        FATAL    // 致命级
    };

    //

    // 将等级转化为字符串
    string LevelToStr(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::DEBUG:
            return "DEBUG";
        case LogLevel::INFO:
            return "DEBUG";
        case LogLevel::WARNING:
            return "WARNING";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        default:
            return "UNKOWN";
        }
    }

    // 获取时间
    string GetTime()
    {
        // time函数:获取当前系统的时间戳
        // localtime_r函数:将时间戳转化为本地时间(可重入函数,localtime则是不可重入函数)
        // struct tm结构体,会将转化之后的本地时间存储在结构体中
        time_t curr = time(nullptr);
        struct tm curr_time;
        localtime_r(&curr, &curr_time);
        char buffer[128];
        snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d %02d:%02d:%02d",
                 curr_time.tm_year + 1900, // 年份是从1900开始计算的,需要加上1900才能得到正确的年份
                 curr_time.tm_mon + 1,     // 月份了0~11,需要加上1才能得到正确的月份
                 curr_time.tm_mday,        // 日
                 curr_time.tm_hour,        // 时
                 curr_time.tm_min,         // 分
                 curr_time.tm_sec);        // 秒

        return buffer;
    }

    //

    // 实现日志信息,并选择刷新策略
    class Logger
    {
    public:
        Logger()
        {
            // 默认选择显示器刷新
            Strategy = make_unique<ConsoleLogStrategy>();
        }

        void EnableConsoleLogStrategy()
        {
            Strategy = make_unique<ConsoleLogStrategy>();
        }

        void EnableFileLogStrategy()
        {
            Strategy = make_unique<FileLogStrategy>();
        }

        // 日志信息
        class LogMessage
        {
        public:
            LogMessage(const LogLevel &level, const string &name, const int &line, Logger &logger)
                : _level(level),
                  _name(name),
                  _logger(logger),
                  _line_member(line)
            {
                _pid = getpid();
                _time = GetTime();
                // 合并:日志信息的左半部分

                stringstream ss; // 创建输出流对象,stringstream可以将输入的所有数据全部转为为字符串
                ss << "[" << _time << "] "
                   << "[" << LevelToStr(_level) << "] "
                   << "[" << _pid << "] "
                   << "[" << _name << "] "
                   << "[" << _line_member << "] "
                   << " - ";

                // 返回ss中的字符串
                _loginfo = ss.str();
            }

            // 日志文件的右半部分:可变参数,重载运算符<<

            // e.g. <<"huang"<<123<<"dasd"<<24
            template <class T>
            LogMessage &operator<<(const T &message) // 引用返回可以让后续内容不断追加
            {
                stringstream ss;
                ss << message;
                _loginfo += ss.str();

                // 返回对象!
                return *this;
            }

            // 销毁时,将信息刷新
            ~LogMessage()
            {
                // 日志文件
                _logger.Strategy->SyncLog(_loginfo);
            }

        private:
            string _time;
            LogLevel _level;
            pid_t _pid;
            string _name;
            int _line_member;
            string _loginfo; // 合并之后的一条完整信息

            // 日志对象
            Logger &_logger;
        };

        // 重载运算符(),便于创建LogMessage对象
        // 这里返回临时对象:当临时对象销毁时,调用对应的析构函数,自动对象中创建好的日志信息进行刷新!
        // 其次局部对象也不能传引用返回!
        LogMessage operator()(const LogLevel &level, const string &name, const int &line)
        {
            return LogMessage(level, name, line, *this);
        }

    private:
        unique_ptr<LogStrategy> Strategy;
    };

    // 为了用户使用更方便,我们使用宏封装一下
    Logger logger;

// 切换刷新策略
#define Enable_Console_LogStrategy() logger.EnableConsoleLogStrategy();
#define Enable_File_LogStrategy() logger.EnableFileLogStrategy();
// 创建日志,并刷新
//__FILE__ 和 __LINE__ 是编译器预定义的宏,作用是获取当前代码所在的文件名、行号
#define LOG(level) logger(level, __FILE__, __LINE__) // 细节:不加;
};
cpp 复制代码
//Mutex.hpp

// 封装锁接口
#pragma once
#include <pthread.h>

namespace MutexModule
{
    class Mutex
    {
    public:
        Mutex()
        {
            pthread_mutex_init(&mutex, nullptr);
        }

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

        void Lock()
        {
            pthread_mutex_lock(&mutex);
        }

        void Unlock()
        {
            pthread_mutex_unlock(&mutex);
        }

        pthread_mutex_t *Get()
        {
            return &mutex;
        }

    private:
        pthread_mutex_t mutex;
    };

    class LockGuard
    {
    public:
        LockGuard(Mutex &mutex)
            : _Mutex(mutex)
        {
            _Mutex.Lock();
        }

        ~LockGuard()
        {
            _Mutex.Unlock();
        }

    private:
        // 为了保证锁的底层逻辑,锁是不能够拷贝的,并且也是没有拷贝构造函数的
        //  避免拷贝,应该引用
        Mutex &_Mutex;
    };
}

客户端实现

1.启动时要给出服务器的ip与端口号

2.创建套接字

3.客户端的端口不需要我们手动绑定,也不能手动的绑定。填写服务器的信息

4.向服务器发送信息

5.接收服务器返回的信息

注意:

client要不要显式的bind? 不要!!首次发送消息,OS会自动给client进行bind,OS知道IP,端口号采用随机端口号的方式

为什么?一个端口号,只能被一个进程bind,为了避免client端口冲突

client端的端口号是几,不重要,只要是唯一的就行!

思考:

那为什么server的实现需要显示的绑定端口号?

因为服务器是要被大量的客户端访问的,这也就意味这个服务器的IP与端口必须是众所周知的,并且不能轻易改变的!

客户端最终实现

cpp 复制代码
#include "Log.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <cstdlib>
#include <iostream>
using namespace LogModule;

// 给出 ip地址 端口号
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "Please use: " << argv[0] << " IP " << "PORT " << endl;
        exit(1);
    }

    uint32_t ip = inet_addr(argv[1]); // 注:字符串转合法ip地址
    uint16_t port = stoi(argv[2]);    // 注:字符串转整数

    // 创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        LOG(LogLevel::FATAL) << "创建套接字失败";
        exit(1);
    }
    LOG(LogLevel::INFO) << "创建套接字";

    // 绑定?不用显示绑定,OS会自动的绑定
    // 填写服务器信息
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = ip;
    local.sin_port = htons(port);

    // 一定是死循环
    while (true)
    {
        // 向服务器发送信息
        cout << "Please Cin # ";
        std::string buff;
        cin >> buff;
        // std::getline(std::cin, buff);
        // buff.size()-1 会丢失最后一个字符,应改为 buff.size()
        ssize_t s = sendto(sockfd, buff.c_str(), buff.size(), 0, (struct sockaddr *)&local, sizeof(local));
        if (s < 0)
        {
            LOG(LogLevel::WARNING) << "向服务器发送信息失败";
            exit(1);
        }

        // 接收服务器返回的信息
        char buffer[1024];
        memset(&buffer, 0, sizeof(buffer));
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        ssize_t ss = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);
        if (ss < 0)
        {
            LOG(LogLevel::WARNING) << "接收服务器信息失败";
            exit(1);
        }
        printf("%s\n", buffer);
        memset(&buff, 0, sizeof(buffer)); // 清理缓存
    }
}

优化服务器

绑定问题

通过实际运行结果,我们发现

bind公网IP,无法绑定(因为云服务器并没有配置公网IP)。

服务器与客户端都绑定内网IP or 本地环回,可以绑定,可以通信。

服务器与客户端绑定不一致,可以绑定,但无法通信。

通过上面的情况,我们会发现:当我们显式的配置服务器的IP地址后,客户端在访问时必须使用服务器bind的IP才能访问,这极大限制了访问的灵活性。

所以我们并不建议,让服务器手动的bind特定的IP地址,而是可以让其bind任意的IP地址。

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

赋值为INADDR_ANY,表示任意地址

这样,服务器在启动时就不用传递IP地址了,只需要传递端口号即可。

处理问题

如果服务器想要对客户端发送来的信息进行一定的处理后,再返回应该怎么做?

可以让main函数处,传入自定义处理方法。在类中使用包装器:function来获取到main函数处传入的方法。并在接收客户端消息后,调用对应方法进行处理,最后将处理结果再发送给客户端。

服务器最终实现

cpp 复制代码
//UdpServer.cc

#include "UdpServer.hpp"
#include <cstdlib>

// 自定义处理方法
std::string func(std::string s)
{
    string h = "hello: ";
    return h + s;
}

// 给出 端口号
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cout << "Please use: " << argv[0] << " PORT" << endl;
    }
    else
    {
        // string ip = argv[1];
        uint16_t port = stoi(argv[1]); // 注:字符串转整数
        udpserver us(port, func);
        us.Init();
        us.Start();
    }
}
cpp 复制代码
//UdpServer.hpp

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

class udpserver
{
    using func_t = function<string(string)>;

public:
    udpserver(uint16_t port, func_t func)
        // : _addr(inet_addr(addr.c_str())), // 注:直接将其转换为合法的ip地址
        : _port(port),
          _func(func)
    {
        _running = false;
    }

    // 初始化:1.创建套接字 2.填充并绑定地址信息
    void Init()
    {
        // 1.创建套接字
        // 返回套接字描述符 地址族 数据类型 传输协议
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "创建套接字失败!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "创建套接字";

        // 2.绑定信息
        // 2.1填充信息
        struct sockaddr_in local;
        // 将指定内存块的所有字节清零
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET; // IPv4地址族
        // local.sin_addr.s_addr = _addr;   //IP地址(主机序列转化为网络序列)
        local.sin_addr.s_addr = INADDR_ANY; // 赋值为INADDR_ANY,表示任意地址
        local.sin_port = htons(_port);      // 端口号

        // 2.2绑定信息
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "绑定失败";
            exit(1);
        }
        LOG(LogLevel::INFO) << "绑定成功";
    }

    // 启动运行:一直运行不停止;1.接收客户端的信息 2.对客户端发送来的信息进行回显
    void Start()
    {
        // 一定是死循环
        _running = true;
        while (_running)
        {
            // 接收客户端的信息
            char buff[1024];
            struct sockaddr_in peer;
            unsigned int len = sizeof(peer);
            // 套接字描述符,数据存放的缓冲区,接收方式:默认,保存发送方的ip与端口,输入输出参数:输入peer的大小,输出实际读取的数据大小
            ssize_t s = recvfrom(_sockfd, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&peer, &len);
            buff[s] = 0;
            printf("%s\n", buff);

            // 回显消息
            if (s > 0)
            {
                // 调用自定义方法
                std::string ss = _func(string(buff));

                // 将数据发送给客户端
                // 套接字描述符,要发送的信息,发送方式:默认,接收方的ip与端口信息
                ssize_t t = sendto(_sockfd, ss.c_str(), ss.size(), 0, (struct sockaddr *)&peer, len);
                if (t < 0)
                {
                    LOG(LogLevel::WARNING) << "信息发送给客户端失败";
                }
            }
            memset(&buff, 0, sizeof(buff)); // 清理缓存
        }
    }

private:
    int _sockfd;
    uint32_t _addr;
    uint16_t _port;
    bool _running;

    // 回调方法
    func_t _func;
};
相关推荐
小白跃升坊8 小时前
基于1Panel的AI运维
linux·运维·人工智能·ai大模型·教学·ai agent
跃渊Yuey8 小时前
【Linux】线程同步与互斥
linux·笔记
杨江8 小时前
seafile docker安装说明
运维
舰长1158 小时前
linux 实现文件共享的实现方式比较
linux·服务器·网络
好好沉淀8 小时前
Docker开发笔记(详解)
运维·docker·容器
zmjjdank1ng9 小时前
Linux 输出重定向
linux·运维
路由侠内网穿透.9 小时前
本地部署智能家居集成解决方案 ESPHome 并实现外部访问( Linux 版本)
linux·运维·服务器·网络协议·智能家居
树℡独9 小时前
ns-3仿真之应用层(三)
运维·服务器·ns3
VekiSon9 小时前
Linux内核驱动——基础概念与开发环境搭建
linux·运维·服务器·c语言·arm开发
zl_dfq9 小时前
Linux 之 【进程信号】(signal、kill、raise、abort、alarm、Core Dump核心转储机制)
linux