socket编程-UDP(1)-设计echo server进行接口使用
socket编程-UDP(1)-设计echo server进行接口使用
简单的回显服务器和客户端代码
Makefile:
.PHONY:all
all:udpclient udpserverudpclient:UdpClient.ccg++ -o $@ $^ -std=c++17 -static
udpserver:UdpServer.ccg++ -o $@ $^ -std=c++17.PHONY:clean
clean:rm -f udpclient udpserver
注释:
用于编译和清理两个 UDP 网络程序(客户端和服务器)
-
.PHONY:all
- 声明
all
是一个伪目标,不代表实际文件
- 声明
-
all:udpclient udpserver
- 默认目标
all
依赖于udpclient
和udpserver
- 执行
make
时会自动构建这两个目标
- 默认目标
-
udpclient:UdpClient.cc
- 定义如何构建
udpclient
可执行文件 - 依赖源文件
UdpClient.cc
- 编译命令:
g++ -o $@ $^ -std=c++17 -static
$@
表示目标文件名(udpclient)$^
表示所有依赖文件(UdpClient.cc)-std=c++17
指定使用 C++17 标准-static
静态链接,生成的可执行文件不依赖动态库
- 定义如何构建
-
udpserver:UdpServer.cc
- 定义如何构建
udpserver
可执行文件 - 依赖源文件
UdpServer.cc
- 编译命令:
g++ -o $@ $^ -std=c++17
- 与客户端类似,但没有
-static
选项,会动态链接
- 与客户端类似,但没有
- 定义如何构建
-
.PHONY:clean
- 声明
clean
是一个伪目标
- 声明
-
clean:
- 清理目标
- 执行命令:
rm -f udpclient udpserver
- 强制删除(
-f
)生成的两个可执行文件
- 强制删除(
使用说明:
- 直接运行
make
会编译生成两个可执行文件 - 运行
make clean
会删除生成的可执行文件
注意:客户端使用了静态链接(-static
),而服务器没有,这可能是为了客户端能在更多环境中运行而不依赖系统库。
动静态库(静态库和动态库)是程序链接和运行的两种不同方式,它们在 链接时机、内存占用、更新维护 等方面有显著区别。以下是详细对比:
1. 静态库(Static Library)
特点
- 扩展名:
.a
(Linux)、.lib
(Windows)。 - 链接时机:在 编译链接阶段 直接嵌入到可执行文件中。
- 内存占用:每个程序独立加载库的副本,占用更多内存。
- 部署:可执行文件独立运行,不依赖外部库文件。
- 更新:需重新编译整个程序才能更新库。
工作原理
优缺点
优点 | 缺点 |
---|---|
无需外部依赖,部署简单 | 可执行文件体积大 |
运行速度快(无动态加载开销) | 更新库需重新编译程序 |
避免动态库版本冲突 | 内存浪费(多进程无法共享库代码) |
使用场景
- 对性能要求极高的场景(如嵌入式系统)。
- 需要避免外部依赖的独立程序。
2. 动态库(Dynamic Library / Shared Library)
特点
- 扩展名:
.so
(Linux)、.dll
(Windows)、.dylib
(macOS)。 - 链接时机:在 程序运行时 由系统动态加载。
- 内存占用:多个进程共享同一库文件,节省内存。
- 部署:需确保目标系统存在匹配的库文件。
- 更新:替换库文件即可生效(需保证接口兼容)。
工作原理
优缺点
优点 | 缺点 |
---|---|
节省磁盘和内存空间 | 部署需携带库文件或确保系统存在 |
库更新无需重新编译程序 | 存在版本冲突风险(如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. 核心设计思想
- 封装原生 pthread 锁:
- 提供更易用的 C++ 接口(如
Lock()
/Unlock()
)。 - 隐藏底层
pthread_mutex_t
的复杂性。
- 提供更易用的 C++ 接口(如
- 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 << 1234template <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) 日志组装与输出 Logger
和 LogMessage
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. 关键优势
- 灵活的输出策略:可动态切换控制台/文件输出。
- 线程安全:所有输出操作受互斥锁保护。
- 易用性:通过宏和流式接口简化调用。
- 自动化:时间戳、PID、文件名等自动填充。
纯虚函数(Pure Virtual Function)的定义
纯虚函数是 C++ 中用于定义抽象基类(Abstract Base Class)的特殊虚函数,它没有具体实现,而是强制派生类必须重写(Override)该函数。纯虚函数的语法是在虚函数声明后加上 = 0
。
基本语法
virtual 返回类型 函数名(参数列表) = 0;
核心特性
-
抽象基类:
- 包含纯虚函数的类称为 抽象类,不能直接实例化对象。
- 派生类必须实现所有纯虚函数,否则也会成为抽象类。
-
接口强制规范:
- 纯虚函数定义了一个接口规范,确保所有派生类遵循统一的接口设计。
-
多态支持:
- 通过基类指针或引用调用纯虚函数时,实际执行的是派生类的实现(动态绑定)。
示例代码
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;
}
关键点
-
抽象类的作用:
- 作为接口规范,定义“能做什么”(What),而不关心“如何做”(How)。
- 例如,
Shape
类规定所有图形必须能计算面积,但具体计算方式由派生类决定。
-
与普通虚函数的区别:
特性 纯虚函数 普通虚函数 实现 无默认实现( = 0
)有默认实现 派生类要求 必须重写 可选重写 类类型 使类成为抽象类 类仍可实例化 -
虚析构函数的重要性:
- 如果基类有虚函数(尤其是纯虚函数),必须声明虚析构函数,确保通过基类指针删除派生类对象时能正确调用派生类的析构函数。
实际应用场景
-
设计模式中的接口(如策略模式、工厂模式):
class Logger { public:virtual void log(const std::string& msg) = 0;virtual ~Logger() {} };class FileLogger : public Logger { /*...*/ }; class ConsoleLogger : public Logger { /*...*/ };
-
框架或库的扩展点:
- 用户通过继承抽象类并实现纯虚函数来扩展功能。
常见问题
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
),从而允许在单行代码中完成多个操作。
核心特点
- 返回对象引用:每个方法调用后返回
*this
(即当前对象)。 - 连续调用:通过
.
或->
运算符串联多个操作。 - 代码简洁:减少临时变量,提高可读性。
示例:实现链式追加
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
,允许链式调用。
实现链式追加的关键
- 返回类型:成员函数需返回当前对象的引用(
ClassName&
)。 - 操作符重载:如
operator<<
、operator+=
等需返回*this
。 - 支持多类型:通过重载支持不同参数类型(如字符串、数字等)。
注意事项
- 避免返回临时对象:错误示例:
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()); // TODOlocal.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 -> 点分十进制的字符串风格的IPbuffer[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
函数对象处理收到的消息,支持自定义业务逻辑。 - 多客户端支持:通过
recvfrom
和sendto
实现与多个客户端的交互。
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); // 客户端IPint peer_port = ntohs(peer.sin_port); // 客户端端口// 调用回调函数处理消息std::string result = _func(buffer);// 返回处理结果sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr*)&peer, len);}}
}
- 工作流程:
- 阻塞接收客户端消息(
recvfrom
)。 - 解析客户端地址(IP + 端口)。
- 调用回调函数
_func
处理消息。 - 将处理结果返回给客户端(
sendto
)。
- 阻塞接收客户端消息(
(5) 日志记录
- 使用
LOG
宏记录关键事件(如套接字创建、绑定成功等)。
3. 设计亮点
-
回调函数抽象:
- 通过
func_t
将业务逻辑与网络通信解耦,可灵活扩展处理逻辑(如加密、协议解析等)。 - 示例回调函数:
std::string ProcessMessage(const std::string& msg) {return "Processed: " + msg; } UdpServer server(8080, ProcessMessage);
- 通过
-
无连接通信:
- UDP无需维护连接状态,适合高频小数据包场景(如DNS查询、实时游戏)。
-
资源管理:
- 使用
_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;
}
- 关键步骤:
- 参数验证:要求用户传入端口号(如
./udpserver 8080
)。 - 日志配置:设置日志输出到控制台(
Enable_Console_Log_Strategy
)。 - 服务器启动:
- 创建
UdpServer
实例,绑定端口和消息处理函数。 - 调用
Init()
初始化网络资源。 - 调用
Start()
进入消息循环。
- 创建
- 参数验证:要求用户传入端口号(如
3. 设计亮点
-
模块化设计:
- 网络通信(
UdpServer
)与业务逻辑(defaulthandler
)分离,便于扩展。 - 可通过替换
defaulthandler
实现不同功能(如翻译、加密等)。
- 网络通信(
-
资源管理:
- 使用
std::unique_ptr
自动管理UdpServer
生命周期,避免内存泄漏。
- 使用
-
日志集成:
- 通过
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. 创建socketint 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; // 打印响应}
}
- 工作流程:
- 从标准输入读取用户消息。
- 通过
sendto
发送消息到服务器。 - 通过
recvfrom
阻塞等待服务器响应。 - 打印响应内容。
3. 关键设计决策
设计 | 原因 |
---|---|
不显式绑定客户端端口 | 由OS自动分配随机端口,避免端口冲突(sendto 首次调用时隐式绑定)。 |
IPv4协议(AF_INET) | 兼容性优先,如需IPv6支持可改为 AF_INET6 。 |
阻塞式接收(recvfrom) | 简单实现,适合单线程交互场景。 |
4. 技术细节
(1) 客户端端口分配
- 为什么不需要
bind
?
首次调用sendto
时,操作系统会自动:- 选择未使用的随机端口绑定。
- 使用本地主机的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. 改进建议
-
错误处理增强:
- 检查
sendto
和recvfrom
的返回值,处理网络错误。 - 添加超时机制(如
select
或非阻塞模式)。
- 检查
-
协议扩展:
- 支持结构化数据(如JSON协议)。
- 添加消息编号(
seq
)匹配请求与响应。
-
多线程支持:
- 分离输入和接收线程,提升交互体验。
总结
- 功能:实现了一个简单的UDP交互式客户端,支持向服务器发送消息并显示响应。
- 核心机制:通过
sendto
/recvfrom
完成无连接通信。 - 适用场景:测试UDP服务、轻量级网络工具开发。