【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;
};
相关推荐
头发还没掉光光6 小时前
Linux多线程之生产消费模型,日志版线程池
linux·运维·开发语言·数据结构·c++
2501_938780286 小时前
服务器 Web 安全:Nginx 配置 X-Frame-Options 与 CSP 头,防御 XSS 与点击劫持
服务器·前端·安全
广然6 小时前
跨厂商(华为 & H3C)防火墙 IPSec 隧道部署
服务器·网络·华为
Gold Steps.6 小时前
常见的Linux发行版升级openSSH10.+
linux·运维·服务器·安全·ssh
啊吧怪不啊吧6 小时前
SQL之表的查改(上)
服务器·数据库·sql
TomcatLikeYou7 小时前
blender4.5 使用外部IDE(pycharm)编辑脚本(bpy)实践指南
服务器·pycharm·php·blender
我爱钱因此会努力7 小时前
ansible实战- 关机
linux·运维·服务器·centos·自动化·ansible
路由侠内网穿透.7 小时前
本地部署集成全能平台 Team.IDE 并实现外部访问
运维·服务器·数据库·ide·远程工作
Wang's Blog8 小时前
Linux小课堂: 系统救援模式操作指南:修复启动问题与重置Root密码
linux·运维