socket编程-UDP(1)-设计echo server进行接口使用

发布于:2025-08-04 ⋅ 阅读:(14) ⋅ 点赞:(0)

socket编程-UDP(1)-设计echo server进行接口使用

简单的回显服务器和客户端代码

Makefile:

.PHONY:all
all:udpclient udpserver

udpclient:UdpClient.cc
	g++ -o $@ $^ -std=c++17 -static
udpserver:UdpServer.cc
	g++ -o $@ $^ -std=c++17

.PHONY:clean
clean:
	rm -f udpclient udpserver

注释:

用于编译和清理两个 UDP 网络程序(客户端和服务器)

  1. .PHONY:all

    • 声明 all 是一个伪目标,不代表实际文件
  2. all:udpclient udpserver

    • 默认目标 all 依赖于 udpclientudpserver
    • 执行 make 时会自动构建这两个目标
  3. udpclient:UdpClient.cc

    • 定义如何构建 udpclient 可执行文件
    • 依赖源文件 UdpClient.cc
    • 编译命令:g++ -o $@ $^ -std=c++17 -static
      • $@ 表示目标文件名(udpclient)
      • $^ 表示所有依赖文件(UdpClient.cc)
      • -std=c++17 指定使用 C++17 标准
      • -static 静态链接,生成的可执行文件不依赖动态库
  4. udpserver:UdpServer.cc

    • 定义如何构建 udpserver 可执行文件
    • 依赖源文件 UdpServer.cc
    • 编译命令:g++ -o $@ $^ -std=c++17
      • 与客户端类似,但没有 -static 选项,会动态链接
  5. .PHONY:clean

    • 声明 clean 是一个伪目标
  6. clean:

    • 清理目标
    • 执行命令:rm -f udpclient udpserver
      • 强制删除(-f)生成的两个可执行文件

使用说明:

  • 直接运行 make 会编译生成两个可执行文件
  • 运行 make clean 会删除生成的可执行文件

注意:客户端使用了静态链接(-static),而服务器没有,这可能是为了客户端能在更多环境中运行而不依赖系统库。

动静态库(静态库和动态库)是程序链接和运行的两种不同方式,它们在 链接时机、内存占用、更新维护 等方面有显著区别。以下是详细对比:

1. 静态库(Static Library)

特点

  • 扩展名.a(Linux)、.lib(Windows)。
  • 链接时机:在 编译链接阶段 直接嵌入到可执行文件中。
  • 内存占用:每个程序独立加载库的副本,占用更多内存。
  • 部署:可执行文件独立运行,不依赖外部库文件。
  • 更新:需重新编译整个程序才能更新库。

工作原理

源代码
编译为.o文件
静态库.a
链接器
独立可执行文件

优缺点

优点 缺点
无需外部依赖,部署简单 可执行文件体积大
运行速度快(无动态加载开销) 更新库需重新编译程序
避免动态库版本冲突 内存浪费(多进程无法共享库代码)

使用场景

  • 对性能要求极高的场景(如嵌入式系统)。
  • 需要避免外部依赖的独立程序。

2. 动态库(Dynamic Library / Shared Library)

特点

  • 扩展名.so(Linux)、.dll(Windows)、.dylib(macOS)。
  • 链接时机:在 程序运行时 由系统动态加载。
  • 内存占用:多个进程共享同一库文件,节省内存。
  • 部署:需确保目标系统存在匹配的库文件。
  • 更新:替换库文件即可生效(需保证接口兼容)。

工作原理

源代码
编译为.o文件
动态库.so
链接器
轻量可执行文件
运行时加载动态库

优缺点

优点 缺点
节省磁盘和内存空间 部署需携带库文件或确保系统存在
库更新无需重新编译程序 存在版本冲突风险(如DLL Hell)
支持热更新(修复Bug无需重启程序) 轻微性能开销(动态加载符号)

使用场景

  • 大型软件(如Office、浏览器)的模块化设计。
  • 需要频繁更新的库(如系统API)。

3. 核心区别对比

对比项 静态库 动态库
链接时机 编译时 运行时
文件独立性 可执行文件独立 依赖外部库文件
内存占用 高(多进程不共享) 低(多进程共享)
更新维护 需重新编译 替换库文件即可
加载速度 快(无运行时加载开销) 慢(需动态加载)
兼容性问题 需处理版本兼容性

4. 实际案例

静态库示例(Linux)

# 编译静态库
g++ -c libfoo.cpp -o libfoo.o
ar rcs libfoo.a libfoo.o

# 链接静态库
g++ main.cpp -L. -lfoo -o main_static

动态库示例(Linux)

# 编译动态库
g++ -shared -fPIC libfoo.cpp -o libfoo.so

# 链接动态库
g++ main.cpp -L. -lfoo -o main_dynamic

# 运行前需设置库路径
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
./main_dynamic

5. 如何选择?

  • 选静态库
    • 需要程序完全独立部署。
    • 库代码稳定且无需频繁更新。
  • 选动态库
    • 注重节省内存和磁盘空间。
    • 需要模块化更新(如插件系统)。

Mutex.hpp

#pragma once
#include <iostream>
#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;
        }
        ~Mutex()
        {
            pthread_mutex_destroy(&_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;
    };
}

这段代码实现了一个 基于 POSIX 线程(pthread)的互斥锁(Mutex)模块,包含 Mutex 类和 LockGuard 类,用于多线程环境下的资源同步。以下是详细解析:

1. 命名空间 MutexModule

namespace MutexModule { ... }
  • 作用:将代码封装在命名空间中,避免与其他库的命名冲突。

2. Mutex 类(核心互斥锁)

成员变量

pthread_mutex_t _mutex;  // POSIX 互斥锁对象

构造函数

Mutex() {
    pthread_mutex_init(&_mutex, nullptr);  // 初始化互斥锁(默认属性)
}
  • pthread_mutex_init:初始化互斥锁,nullptr 表示使用默认属性(非递归锁)。

加锁与解锁

void Lock() {
    int n = pthread_mutex_lock(&_mutex);  // 阻塞直到获取锁
    (void)n;  // 忽略返回值(实际工程中应检查错误)
}
void Unlock() {
    int n = pthread_mutex_unlock(&_mutex); // 释放锁
    (void)n;
}
  • pthread_mutex_lock:如果锁已被其他线程持有,当前线程会阻塞。
  • (void)n:显式忽略返回值(实际项目中建议检查 n != 0 的错误情况)。

析构函数

~Mutex() {
    pthread_mutex_destroy(&_mutex);  // 销毁互斥锁
}
  • 注意:必须在没有线程持有锁时调用,否则行为未定义。

获取原始锁指针

pthread_mutex_t* Get() {
    return &_mutex;  // 返回底层 pthread_mutex_t 指针
}
  • 用途:需要与原生 pthread 函数交互时使用(如 pthread_cond_wait)。

3. LockGuard 类(RAII 锁守卫)

构造函数(加锁)

LockGuard(Mutex &mutex) : _mutex(mutex) {
    _mutex.Lock();  // 构造时自动加锁
}
  • RAII 思想:利用构造函数获取资源(锁)。

析构函数(解锁)

~LockGuard() {
    _mutex.Unlock();  // 析构时自动释放锁
}
  • 关键作用:即使代码块因异常退出,也能保证锁被释放,避免死锁。

成员变量

Mutex &_mutex;  // 引用形式的 Mutex 对象
  • 注意:使用引用避免拷贝问题(pthread_mutex_t 不可拷贝)。

4. 核心设计思想

  1. 封装原生 pthread 锁
    • 提供更易用的 C++ 接口(如 Lock()/Unlock())。
    • 隐藏底层 pthread_mutex_t 的复杂性。
  2. RAII(资源获取即初始化)
    • LockGuard 在构造时加锁,析构时解锁,确保锁的安全释放。
    • 避免手动调用 Unlock() 的遗漏风险。

5. 使用示例

基本用法

MutexModule::Mutex mtx;

void ThreadFunc() {
    MutexModule::LockGuard lock(mtx);  // 自动加锁
    // 临界区代码
    // 离开作用域时自动解锁
}

对比原生 pthread 代码

// 原生 pthread 写法
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mutex);
// 临界区
pthread_mutex_unlock(&mutex);

// 使用 LockGuard 后的写法
{
    MutexModule::LockGuard lock(mtx);
    // 临界区
}  // 自动解锁

总结

  • Mutex:封装 pthread_mutex_t,提供加锁/解锁接口。
  • LockGuard:RAII 工具类,自动管理锁的生命周期。
  • 用途:保护多线程环境下的共享资源,避免数据竞争。
  • 优势:比手动调用 pthread_mutex_lock/unlock 更安全、更简洁。

Log.hpp

#ifndef __LOG_HPP__
#define __LOG_HPP__

#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem> //C++17
#include <sstream>
#include <fstream>
#include <memory>
#include <ctime>
#include <unistd.h>
#include "Mutex.hpp"

namespace LogModule
{
    using namespace MutexModule;

    const std::string gsep = "\r\n";
    // 策略模式,C++多态特性
    // 2. 刷新策略 a: 显示器打印 b:向指定的文件写入
    //  刷新策略基类
    class LogStrategy
    {
    public:
        ~LogStrategy() = default;
        virtual void SyncLog(const std::string &message) = 0;
    };

    // 显示器打印日志的策略 : 子类
    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        ConsoleLogStrategy()
        {
        }
        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; // "./log/" + "my.log"
            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;
    };

    // 形成一条完整的日志&&根据上面的策略,选择不同的刷新方式

    // 1. 形成日志等级
    enum class LogLevel
    {
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };
    std::string Level2Str(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;
    }

    // 1. 形成日志 && 2. 根据不同的策略,完成刷新
    class Logger
    {
    public:
        Logger()
        {
            EnableConsoleLogStrategy();
        }
        void EnableFileLogStrategy()
        {
            _fflush_strategy = std::make_unique<FileLogStrategy>();
        }
        void EnableConsoleLogStrategy()
        {
            _fflush_strategy = std::make_unique<ConsoleLogStrategy>();
        }

        // 表示的是未来的一条日志
        class LogMessage
        {
        public:
            LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger)
                : _curr_time(GetTimeStamp()),
                  _level(level),
                  _pid(getpid()),
                  _src_name(src_name),
                  _line_number(line_number),
                  _logger(logger)
            {
                // 日志的左边部分,合并起来
                std::stringstream ss;
                ss << "[" << _curr_time << "] "
                   << "[" << Level2Str(_level) << "] "
                   << "[" << _pid << "] "
                   << "[" << _src_name << "] "
                   << "[" << _line_number << "] "
                   << "- ";
                _loginfo = ss.str();
            }
            // LogMessage() << "hell world" << "XXXX" << 3.14 << 1234
            template <typename T>
            LogMessage &operator<<(const T &info)
            {
                // a = b = c =d;
                // 日志的右半部分,可变的
                std::stringstream ss;
                ss << info;
                _loginfo += ss.str();
                return *this;
            }

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

        private:
            std::string _curr_time;
            LogLevel _level;
            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.EnableConsoleLogStrategy()
    #define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
}

#endif

这段代码实现了一个 基于策略模式的日志系统,支持 多日志级别多输出方式(控制台/文件)和 线程安全 的日志记录功能。以下是详细解析:

1. 核心设计思想

  • 策略模式:通过 LogStrategy 基类抽象日志输出方式,派生出 ConsoleLogStrategy(控制台输出)和 FileLogStrategy(文件输出)。
  • RAII(资源获取即初始化):利用 LogMessage 类的构造和析构,自动组装日志内容并触发输出。
  • 线程安全:使用 Mutex 类保护共享资源(如文件写入、控制台输出)。

2. 关键组件解析

(1) 日志级别 LogLevel

enum class LogLevel {
    DEBUG,   // 调试信息
    INFO,    // 普通信息
    WARNING, // 警告
    ERROR,   // 错误
    FATAL    // 致命错误
};
  • 通过 Level2Str() 函数将枚举转换为字符串(如 DEBUG"DEBUG")。

(2) 时间戳生成 GetTimeStamp()

std::string GetTimeStamp() {
    // 示例输出: "2023-08-20 14:30:45"
    time_t curr = time(nullptr);
    struct tm curr_tm;
    localtime_r(&curr, &curr_tm);  // 线程安全的时间转换
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "%04d-%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 buffer;
}

(3) 策略基类 LogStrategy

class LogStrategy {
public:
    virtual void SyncLog(const std::string &message) = 0;
    virtual ~LogStrategy() = default;
};
  • 纯虚函数 SyncLog:子类需实现具体的日志输出逻辑。

(4) 控制台输出策略 ConsoleLogStrategy

class ConsoleLogStrategy : public LogStrategy {
public:
    void SyncLog(const std::string &message) override {
        LockGuard lock(_mutex);  // 线程安全
        std::cout << message << gsep;  // gsep = "\r\n"
    }
private:
    Mutex _mutex;
};

(5) 文件输出策略 FileLogStrategy

class FileLogStrategy : public LogStrategy {
public:
    FileLogStrategy(const std::string &path = "./log", const std::string &file = "my.log") 
        : _path(path), _file(file) {
        // 自动创建日志目录(如果不存在)
        std::filesystem::create_directories(_path);
    }
    void SyncLog(const std::string &message) override {
        LockGuard lock(_mutex);
        std::string filename = _path + "/" + _file;
        std::ofstream out(filename, std::ios::app);  // 追加模式
        out << message << gsep;
    }
private:
    std::string _path, _file;
    Mutex _mutex;
};

(6) 日志组装与输出 LoggerLogMessage

class Logger {
public:
    // 切换输出策略
    void EnableFileLogStrategy() { 
        _fflush_strategy = std::make_unique<FileLogStrategy>(); 
    }
    void EnableConsoleLogStrategy() { 
        _fflush_strategy = std::make_unique<ConsoleLogStrategy>(); 
    }

    // 日志条目构建器
    class LogMessage {
    public:
        LogMessage(LogLevel level, const std::string &src_name, int line, Logger &logger) 
            : _level(level), _src_name(src_name), _line_number(line), _logger(logger) {
            // 组装固定部分(时间、级别、PID、文件名、行号)
            _loginfo = "[" + GetTimeStamp() + "] [" + Level2Str(_level) + "] " +
                      "[" + std::to_string(getpid()) + "] " +
                      "[" + _src_name + ":" + std::to_string(_line_number) + "] - ";
        }

        // 支持链式追加日志内容(如 LOG(INFO) << "Error: " << errno;)
        template <typename T>
        LogMessage &operator<<(const T &data) {
            std::stringstream ss;
            ss << data;
            _loginfo += ss.str();
            return *this;
        }

        // 析构时触发日志输出
        ~LogMessage() {
            if (_logger._fflush_strategy) {
                _logger._fflush_strategy->SyncLog(_loginfo);
            }
        }
    private:
        std::string _loginfo;
        // ... 其他字段省略
    };

    // 生成日志条目
    LogMessage operator()(LogLevel level, const std::string &file, int line) {
        return LogMessage(level, file, line, *this);
    }
private:
    std::unique_ptr<LogStrategy> _fflush_strategy;
};

(7) 全局日志对象与宏

// 全局单例日志对象
Logger logger;

// 简化用户调用的宏
#define LOG(level) logger(level, __FILE__, __LINE__)
#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
  • LOG(level):自动填充文件名(__FILE__)和行号(__LINE__),例如:
    LOG(LogLevel::INFO) << "User login: " << username;
    

3. 使用示例

(1) 输出到控制台

Enable_Console_Log_Strategy();
LOG(LogLevel::DEBUG) << "Debug message: " << 42;

输出示例

[2023-08-20 14:30:45] [DEBUG] [1234] [main.cpp:20] - Debug message: 42

(2) 输出到文件

Enable_File_Log_Strategy();
LOG(LogLevel::ERROR) << "Failed to open file: " << filename;

文件内容

[2023-08-20 14:31:00] [ERROR] [1234] [server.cpp:45] - Failed to open file: config.ini

4. 关键优势

  1. 灵活的输出策略:可动态切换控制台/文件输出。
  2. 线程安全:所有输出操作受互斥锁保护。
  3. 易用性:通过宏和流式接口简化调用。
  4. 自动化:时间戳、PID、文件名等自动填充。

纯虚函数(Pure Virtual Function)的定义

纯虚函数是 C++ 中用于定义抽象基类(Abstract Base Class)的特殊虚函数,它没有具体实现,而是强制派生类必须重写(Override)该函数。纯虚函数的语法是在虚函数声明后加上 = 0

基本语法

virtual 返回类型 函数名(参数列表) = 0;

核心特性

  1. 抽象基类

    • 包含纯虚函数的类称为 抽象类,不能直接实例化对象。
    • 派生类必须实现所有纯虚函数,否则也会成为抽象类。
  2. 接口强制规范

    • 纯虚函数定义了一个接口规范,确保所有派生类遵循统一的接口设计。
  3. 多态支持

    • 通过基类指针或引用调用纯虚函数时,实际执行的是派生类的实现(动态绑定)。

示例代码

1. 定义纯虚函数

class Shape {  // 抽象基类
public:
    virtual double area() const = 0;  // 纯虚函数
    virtual ~Shape() {}              // 虚析构函数(重要!)
};

2. 派生类必须实现纯虚函数

class Circle : public Shape {
public:
    Circle(double r) : radius(r) {}
    double area() const override {   // 必须实现 area()
        return 3.14 * radius * radius;
    }
private:
    double radius;
};

class Square : public Shape {
public:
    Square(double s) : side(s) {}
    double area() const override {   // 必须实现 area()
        return side * side;
    }
private:
    double side;
};

3. 使用多态

int main() {
    Shape* shapes[] = {new Circle(5), new Square(4)};
    for (Shape* s : shapes) {
        std::cout << "Area: " << s->area() << std::endl;  // 动态调用派生类的 area()
    }
    // 释放资源
    for (Shape* s : shapes) delete s;
    return 0;
}

关键点

  1. 抽象类的作用

    • 作为接口规范,定义“能做什么”(What),而不关心“如何做”(How)。
    • 例如,Shape 类规定所有图形必须能计算面积,但具体计算方式由派生类决定。
  2. 与普通虚函数的区别

    特性 纯虚函数 普通虚函数
    实现 无默认实现(= 0 有默认实现
    派生类要求 必须重写 可选重写
    类类型 使类成为抽象类 类仍可实例化
  3. 虚析构函数的重要性

    • 如果基类有虚函数(尤其是纯虚函数),必须声明虚析构函数,确保通过基类指针删除派生类对象时能正确调用派生类的析构函数。

实际应用场景

  1. 设计模式中的接口(如策略模式、工厂模式):

    class Logger {
    public:
        virtual void log(const std::string& msg) = 0;
        virtual ~Logger() {}
    };
    
    class FileLogger : public Logger { /*...*/ };
    class ConsoleLogger : public Logger { /*...*/ };
    
  2. 框架或库的扩展点

    • 用户通过继承抽象类并实现纯虚函数来扩展功能。

常见问题

Q:纯虚函数可以有实现吗?

  • 可以,但通常不推荐。派生类仍需重写,但可通过基类名显式调用基类的实现:
    class Base {
    public:
        virtual void foo() = 0;
    };
    void Base::foo() { std::cout << "Base impl\n"; }  // 纯虚函数的实现
    
    class Derived : public Base {
    public:
        void foo() override {
            Base::foo();  // 调用基类实现
            std::cout << "Derived impl\n";
        }
    };
    

Q:抽象类可以有非虚函数吗?

  • 可以。抽象类可以包含普通成员函数、成员变量等,但至少有一个纯虚函数。

总结

  • 纯虚函数是定义接口的核心机制,强制派生类实现特定功能。
  • 抽象类用于建模通用行为,不能实例化。
  • 多态通过基类指针/引用调用纯虚函数实现运行时绑定。

链式追加(Chaining)的含义

链式追加是一种 通过连续调用成员函数或操作符来简化代码 的编程风格,每次调用返回对象自身的引用(*this),从而允许在单行代码中完成多个操作。

核心特点

  1. 返回对象引用:每个方法调用后返回 *this(即当前对象)。
  2. 连续调用:通过 .-> 运算符串联多个操作。
  3. 代码简洁:减少临时变量,提高可读性。

示例:实现链式追加

1. 基础链式调用(成员函数)

class StringBuilder {
public:
    StringBuilder& append(const std::string& str) {
        data += str;
        return *this;  // 返回当前对象的引用
    }

    std::string get() const { return data; }

private:
    std::string data;
};

// 使用链式追加
StringBuilder sb;
sb.append("Hello").append(" ").append("World");
std::cout << sb.get();  // 输出 "Hello World"

2. 操作符重载链式调用(如日志系统)

class LogMessage {
public:
    LogMessage& operator<<(const std::string& msg) {
        buffer += msg;
        return *this;  // 返回当前对象的引用
    }

    LogMessage& operator<<(int num) {
        buffer += std::to_string(num);
        return *this;
    }

    std::string get() const { return buffer; }

private:
    std::string buffer;
};

// 链式追加多种类型
LogMessage log;
log << "Error: " << 404 << " Not Found";
std::cout << log.get();  // 输出 "Error: 404 Not Found"

链式追加的优势

场景 传统写法 链式追加写法
字符串拼接 sb.append("A"); sb.append("B"); sb.append("A").append("B");
日志记录 log << "A"; log << "B"; log << "A" << "B";
条件过滤(如SQL构建) query.where("A"); query.where("B"); query.where("A").where("B");

实际应用案例

1. 日志系统(如问题中的 LogMessage 类)

LOG(INFO) << "User: " << username << " logged in at " << time;
  • 通过重载 operator<< 实现链式追加日志内容。

2. 流式接口(如 std::cout

std::cout << "Value: " << 42 << "\n";
  • C++标准库中的 ostream 就是链式追加的经典实现。

3. 构建器模式(Builder Pattern)

Car car = CarBuilder().setWheels(4).setColor("Red").build();
  • 每个配置方法返回 *this,允许链式调用。

实现链式追加的关键

  1. 返回类型:成员函数需返回当前对象的引用(ClassName&)。
  2. 操作符重载:如 operator<<operator+= 等需返回 *this
  3. 支持多类型:通过重载支持不同参数类型(如字符串、数字等)。

注意事项

  • 避免返回临时对象:错误示例:
    StringBuilder append(const std::string& str) {
        return StringBuilder(data + str);  // 错误!返回临时对象会中断链式调用
    }
    
  • 线程安全:如果链式调用涉及共享资源,需加锁保护。

总结

链式追加通过 返回对象引用 实现连续调用,广泛用于日志系统、流式接口、构建器模式等场景,显著提升代码简洁性和可读性。其核心是让每个操作“返回自身”,从而形成流畅的API调用链。

UdpServer.hpp

#pragma once

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

using namespace LogModule;

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

const int defaultfd = -1;

// 你是为了进行网络通信的!
class UdpServer
{
public:
    UdpServer(uint16_t port, func_t func)
        : _sockfd(defaultfd),
        //   _ip(ip),
          _port(port),
          _isrunning(false),
          _func(func)
    {
    }
    void Init()
    {
        // 1. 创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd : " << _sockfd;

        // 2. 绑定socket信息,ip和端口, ip(比较特殊,后续解释)
        // 2.1 填充sockaddr_in结构体
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        // 我会不会把我的IP地址和端口号发送给对方?
        // IP信息和端口信息,一定要发送到网络!
        // 本地格式->网络序列
        local.sin_port = htons(_port);
        // IP也是如此,1. IP转成4字节 2. 4字节转成网络序列 -> in_addr_t inet_addr(const char *cp);
        //local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO
        local.sin_addr.s_addr = INADDR_ANY;

        // 那么为什么服务器端要显式的bind呢?IP和端口必须是众所周知且不能轻易改变的!
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind success, sockfd : " << _sockfd;
    }
    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 1. 收消息, client为什么要个服务器发送消息啊?不就是让服务端处理数据。
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (s > 0)
            {
                int peer_port = ntohs(peer.sin_port); // 从网络中拿到的!网络序列
                std::string peer_ip = inet_ntoa(peer.sin_addr); //4字节网络风格的IP -> 点分十进制的字符串风格的IP

                buffer[s] = 0;

                std::string result = _func(buffer);


                // LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port<< "]# " << buffer; // 1. 消息内容 2. 谁发的??

                // 2. 发消息
                // std::string echo_string = "server echo@ ";
                // echo_string += buffer;
                sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr*)&peer, len);
            }
        }
    }
    ~UdpServer()
    {
    }

private:
    int _sockfd;
    uint16_t _port;
    // std::string _ip; // 用的是字符串风格,点分十进制, "192.168.1.1"
    bool _isrunning;

    func_t _func; // 服务器的回调函数,用来进行对数据进行处理
};

这段代码实现了一个 基于UDP协议的网络服务器UdpServer),它接收客户端发来的消息,通过回调函数处理消息后返回响应。以下是详细解析:

1. 核心功能

  • UDP通信:基于无连接的UDP协议(SOCK_DGRAM)实现数据传输。
  • 回调机制:通过 func_t 函数对象处理收到的消息,支持自定义业务逻辑。
  • 多客户端支持:通过 recvfromsendto 实现与多个客户端的交互。

2. 关键组件解析

(1) 类型定义

using func_t = std::function<std::string(const std::string&)>;
  • 作用:定义回调函数类型,接收字符串消息,返回处理后的字符串。

(2) 构造函数

UdpServer(uint16_t port, func_t func) 
    : _sockfd(defaultfd), _port(port), _isrunning(false), _func(func) {}
  • 参数
    • port:服务器绑定的端口号。
    • func:处理消息的回调函数(如业务逻辑处理)。

(3) 初始化 Init()

void Init() {
    // 1. 创建UDP套接字
    _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (_sockfd < 0) {
        LOG(LogLevel::FATAL) << "socket error!";
        exit(1);
    }

    // 2. 绑定IP和端口
    struct sockaddr_in local;
    bzero(&local, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(_port);
    local.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡

    if (bind(_sockfd, (struct sockaddr*)&local, sizeof(local)) < 0) {
        LOG(LogLevel::FATAL) << "bind error";
        exit(2);
    }
}
  • 关键点
    • INADDR_ANY:监听所有可用网卡(无需显式指定IP)。
    • htons:将端口号转为网络字节序。
    • 绑定失败时记录日志并退出。

(4) 主循环 Start()

void Start() {
    _isrunning = true;
    while (_isrunning) {
        char buffer[1024];
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);

        // 接收消息
        ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
        if (s > 0) {
            buffer[s] = 0; // 添加字符串结束符
            std::string peer_ip = inet_ntoa(peer.sin_addr); // 客户端IP
            int peer_port = ntohs(peer.sin_port); // 客户端端口

            // 调用回调函数处理消息
            std::string result = _func(buffer);

            // 返回处理结果
            sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr*)&peer, len);
        }
    }
}
  • 工作流程
    1. 阻塞接收客户端消息(recvfrom)。
    2. 解析客户端地址(IP + 端口)。
    3. 调用回调函数 _func 处理消息。
    4. 将处理结果返回给客户端(sendto)。

(5) 日志记录

  • 使用 LOG 宏记录关键事件(如套接字创建、绑定成功等)。

3. 设计亮点

  1. 回调函数抽象

    • 通过 func_t 将业务逻辑与网络通信解耦,可灵活扩展处理逻辑(如加密、协议解析等)。
    • 示例回调函数:
      std::string ProcessMessage(const std::string& msg) {
          return "Processed: " + msg;
      }
      UdpServer server(8080, ProcessMessage);
      
  2. 无连接通信

    • UDP无需维护连接状态,适合高频小数据包场景(如DNS查询、实时游戏)。
  3. 资源管理

    • 使用 _isrunning 标志控制服务启停,避免资源泄漏。

4. 使用示例

启动服务器

// 定义消息处理函数
std::string EchoHandler(const std::string& msg) {
    return "Server response: " + msg;
}

int main() {
    UdpServer server(8080, EchoHandler);
    server.Init();   // 初始化套接字并绑定
    server.Start();  // 进入主循环
    return 0;
}

客户端交互

# 使用 netcat 测试
echo "Hello" | nc -u 127.0.0.1 8080
# 输出: "Server response: Hello"

总结

  • 功能:实现了一个简单的UDP回声服务器,支持自定义消息处理逻辑。
  • 核心:通过回调函数解耦网络层与业务层,适合轻量级网络服务开发。
  • 扩展性:可在此基础上实现协议解析、多线程处理等高级功能。

UdpSever.cc

#include <iostream>
#include <memory>
#include "UdpServer.hpp"

// 仅仅是用来进行测试的
std::string defaulthandler(const std::string &message)
{
    std::string hello = "hello, ";
    hello += message;
    return hello;
}

// 翻译系统,字符串当成英文单词


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

    Enable_Console_Log_Strategy();

    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, defaulthandler);
    usvr->Init();
    usvr->Start();
    return 0;
}

这段代码实现了一个 基于UDP协议的简易服务器程序,它接收客户端发来的消息,并在消息前添加 "hello, " 后返回给客户端。以下是详细解析:

1. 核心功能

  • UDP服务器:监听指定端口,处理客户端发来的消息。
  • 简单消息处理:对收到的消息添加固定前缀后返回(测试用途)。
  • 日志支持:通过 LogModule 记录运行状态(输出到控制台)。

2. 代码逐层解析

(1) 消息处理函数 defaulthandler

std::string defaulthandler(const std::string &message) {
    std::string hello = "hello, ";
    hello += message;  // 拼接前缀和消息
    return hello;
}
  • 作用:测试用的回调函数,将输入消息转换为 "hello, [message]"
  • 示例
    • 输入:"world" → 输出:"hello, world"

(2) 主函数 main

int main(int argc, char *argv[]) {
    // 参数检查
    if (argc != 2) {
        std::cerr << "Usage: " << argv[0] << " port" << std::endl;
        return 1;
    }
    uint16_t port = std::stoi(argv[1]);  // 从命令行参数获取端口号

    Enable_Console_Log_Strategy();  // 启用控制台日志

    // 创建并启动UDP服务器
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, defaulthandler);
    usvr->Init();  // 初始化套接字和绑定
    usvr->Start(); // 进入主循环
    return 0;
}
  • 关键步骤
    1. 参数验证:要求用户传入端口号(如 ./udpserver 8080)。
    2. 日志配置:设置日志输出到控制台(Enable_Console_Log_Strategy)。
    3. 服务器启动
      • 创建 UdpServer 实例,绑定端口和消息处理函数。
      • 调用 Init() 初始化网络资源。
      • 调用 Start() 进入消息循环。

3. 设计亮点

  1. 模块化设计

    • 网络通信(UdpServer)与业务逻辑(defaulthandler)分离,便于扩展。
    • 可通过替换 defaulthandler 实现不同功能(如翻译、加密等)。
  2. 资源管理

    • 使用 std::unique_ptr 自动管理 UdpServer 生命周期,避免内存泄漏。
  3. 日志集成

    • 通过 LogModule 记录关键事件(如套接字创建、绑定状态等)。

4. 使用示例

编译并运行服务器

# 编译(假设UdpServer.hpp和Log.hpp在本地)
g++ -std=c++17 udpserver.cpp -o udpserver

# 启动服务器(监听8080端口)
./udpserver 8080
**客户端测试**
# 使用 netcat 发送UDP消息
echo "world" | nc -u 127.0.0.1 8080
# 输出: "hello, world"

5. 关键技术点

技术 作用
std::unique_ptr 智能指针管理资源,确保服务器对象安全释放。
UdpServer 封装UDP通信细节(套接字创建、绑定、消息循环)。
回调函数 defaulthandler 定义如何处理收到的消息,支持灵活替换业务逻辑。
命令行参数 argc/argv 动态指定服务器监听端口,提升灵活性。

总结

  • 用途:快速搭建一个测试用的UDP回声服务器。
  • 核心流程:接收消息 → 处理消息 → 返回响应。
  • 扩展性:通过替换回调函数和优化 UdpServer 类,可轻松适配实际业务场景。

UdpClient.cc

#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

// ./udpclient server_ip server_port
int main(int argc, char *argv[])
{
    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]);

    // 1. 创建socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        return 2;
    }

    // 2. 本地的ip和端口是什么?要不要和上面的“文件”关联呢?
    // 问题:client要不要bind?需要bind.
    //       client要不要显式的bind?不要!!首次发送消息,OS会自动给client进行bind,OS知道IP,端口号采用随机端口号的方式
    //   为什么?一个端口号,只能被一个进程bind,为了避免client端口冲突
    //   client端的端口号是几,不重要,只要是唯一的就行!
    // 填写服务器信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());
    while(true)
    {
        std::string input;
        std::cout << "Please Enter# ";
        std::getline(std::cin, input);

        int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&server, sizeof(server));
        (void)n;

        char buffer[1024];
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
        if(m > 0)
        {
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
    }


    return 0;
}

这段代码实现了一个 基于UDP协议的客户端程序,用于向指定的服务器IP和端口发送消息,并接收服务器的响应。以下是详细解析:

1. 核心功能

  • UDP客户端:向服务器发送用户输入的消息,并接收响应。
  • 交互式输入:通过控制台实时输入消息。
  • 自动端口分配:客户端无需显式绑定端口,由操作系统自动分配随机端口。

2. 代码逐层解析

(1) 参数检查

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]);
  • 作用:检查命令行参数,要求用户传入服务器IP和端口(如 ./udpclient 127.0.0.1 8080)。

(2) 创建UDP套接字

int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
    std::cerr << "socket error" << std::endl;
    return 2;
}
  • 关键点
    • AF_INET:使用IPv4协议。
    • SOCK_DGRAM:指定UDP协议。

(3) 配置服务器地址

struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port); // 端口转网络字节序
server.sin_addr.s_addr = inet_addr(server_ip.c_str()); // IP转网络字节序
  • sockaddr_in 结构体:存储服务器的IP和端口信息。

(4) 消息循环

while (true) {
    // 1. 获取用户输入
    std::string input;
    std::cout << "Please Enter# ";
    std::getline(std::cin, input);

    // 2. 发送消息到服务器
    sendto(sockfd, input.c_str(), input.size(), 0, 
          (struct sockaddr*)&server, sizeof(server));

    // 3. 接收服务器响应
    char buffer[1024];
    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);
    int m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, 
                    (struct sockaddr*)&peer, &len);
    if (m > 0) {
        buffer[m] = 0; // 添加字符串结束符
        std::cout << buffer << std::endl; // 打印响应
    }
}
  • 工作流程
    1. 从标准输入读取用户消息。
    2. 通过 sendto 发送消息到服务器。
    3. 通过 recvfrom 阻塞等待服务器响应。
    4. 打印响应内容。

3. 关键设计决策

设计 原因
不显式绑定客户端端口 由OS自动分配随机端口,避免端口冲突(sendto 首次调用时隐式绑定)。
IPv4协议(AF_INET) 兼容性优先,如需IPv6支持可改为 AF_INET6
阻塞式接收(recvfrom) 简单实现,适合单线程交互场景。

4. 技术细节

(1) 客户端端口分配

  • 为什么不需要 bind
    首次调用 sendto 时,操作系统会自动:
    1. 选择未使用的随机端口绑定。
    2. 使用本地主机的IP地址。
  • 优点:避免手动绑定导致的端口冲突。

(2) 字节序转换

  • htons(server_port):将主机字节序的端口号转为网络字节序(大端)。
  • inet_addr(server_ip.c_str()):将点分十进制IP(如 "127.0.0.1")转为32位网络字节序整数。

(3) 缓冲区安全

  • sizeof(buffer)-1:预留1字节空间用于添加字符串结束符 \0
  • buffer[m] = 0:确保接收的数据可作为C字符串安全处理。

5. 使用示例

启动客户端

# 连接到服务器的8080端口
./udpclient 127.0.0.1 8080

# 交互示例
Please Enter# Hello
hello, Hello  # 服务器响应
Please Enter# Quit
hello, Quit

6. 改进建议

  1. 错误处理增强

    • 检查 sendtorecvfrom 的返回值,处理网络错误。
    • 添加超时机制(如 select 或非阻塞模式)。
  2. 协议扩展

    • 支持结构化数据(如JSON协议)。
    • 添加消息编号(seq)匹配请求与响应。
  3. 多线程支持

    • 分离输入和接收线程,提升交互体验。

总结

  • 功能:实现了一个简单的UDP交互式客户端,支持向服务器发送消息并显示响应。
  • 核心机制:通过 sendto/recvfrom 完成无连接通信。
  • 适用场景:测试UDP服务、轻量级网络工具开发。

网站公告

今日签到

点亮在社区的每一天
去签到