手写muduo网络库(一):项目构建和时间戳、日志库
引言
本文作为手写 muduo 网络库系列开篇,聚焦项目基础框架搭建与核心基础工具模块设计。通过解析 CMake 工程结构设计、目录规划原则,结合时间戳与日志系统的架构,为后续网络库开发奠定工程化基础。文中附完整 CMake 配置示例及模块代码。
代码参考自:https://github.com/youngyangyang04/muduo-core
部分代码经过修改
一、项目工程化构建:从目录规划到编译配置
1. 分层目录结构设计
mymuduo/
├─ src/ # 核心库源代码(实现文件) ├─ CMakeLists.txt #
├─ include/ # 公共头文件(供外部引用)
├─ build/ # 编译输出目录(生成库文件与可执行程序)
├─ example/ # 示例程序(验证库功能) ├─ CMakeLists.txt #
├─ CMakeLists.txt # 根目录编译配置
└─ lib/ # 静态库/动态库输出目录(自动生成)
设计说明:
- src 与 include 分离:遵循 “接口与实现分离” 原则,头文件仅暴露必要 API,隐藏实现细节
- build 独立输出:避免编译产物污染源码目录,支持
cmake .. -B build
的外部构建模式 - example 验证层:通过具体场景测试库功能,便于快速调试与功能迭代
2. 根目录 CMake 配置解析
cmake_minimum_required(VERSION 3.0)
project(mymuduo) # 设置C++11标准(muduo原生基于C++03,此处升级为现代C++)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON) # 统一库文件输出路径
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib) # 全局依赖库(如多线程支持)
set(LIBS pthread) # 模块化构建:递归编译子目录
add_subdirectory(src)
add_subdirectory(example)
关键配置点:
- CMAKE_CXX_STANDARD:强制要求 C++11 编译环境,支持 Lambda、智能指针等现代特性
- LIBRARY_OUTPUT_PATH:集中管理库文件,便于后续集成时统一引用
- add_subdirectory:通过分模块编译,实现 “核心库 - 示例程序” 的解耦构建
3. 核心库模块(src 目录)编译配置
# 自动收集当前目录所有.cpp文件
file(GLOB SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp) # 生成动态库(可通过SHARED/STATIC切换库类型)
add_library(mymuduo SHARED ${SRC_FILES}) # 暴露头文件路径(供外部target引用)
target_include_directories(mymuduo PUBLIC ${CMAKE_SOURCE_DIR}/include)
设计考量:
- 动态库优先:SHARED 模式便于运行时动态加载,适合需要频繁升级的库开发
- PUBLIC 头文件:通过 PUBLIC 关键字,确保依赖 mymuduo 库的目标自动包含头文件路径
4. 示例程序(example 目录)编译配置
file(GLOB EXAMPLE_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
add_executable(testserver ${EXAMPLE_SRCS}) # 链接核心库与全局依赖
target_link_libraries(testserver mymuduo ${LIBS}) # 编译选项配置
target_compile_options(testserver PRIVATE -std=c++11 -Wall) # 可执行文件输出到当前目录
set_target_properties(testserver PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
验证流程设计:
- 独立编译目标:testserver 可执行程序直接依赖 mymuduo 库,方便单步调试
- 编译选项增强:-Wall 开启严格编译警告,帮助提前发现潜在问题
二、时间戳
1. 概述
时间戳(Timestamp)是一个表示特定时刻的数值,通常用于记录事件发生的时间。在本代码库中,Timestamp
类提供了一种简单的方式来处理时间戳,它可以获取当前时间的时间戳,并将时间戳转换为可读的字符串格式。
2. 类定义(Timestamp.h
)
#pragma once#include <iostream>
#include <string>
namespace mymuduo
{namespace base{class Timestamp{public:Timestamp();explicit Timestamp(int64_t microSecondsSinceEpoch);static Timestamp now();std::string toString() const;private:int64_t microSecondsSinceEpoch_;};}
}
- 命名空间:代码使用了嵌套命名空间
mymuduo::base
,这样可以避免命名冲突,提高代码的可维护性。 - 构造函数:
Timestamp()
:默认构造函数,将microSecondsSinceEpoch_
初始化为 0。explicit Timestamp(int64_t microSecondsSinceEpoch)
:带参数的构造函数,使用传入的微秒数初始化microSecondsSinceEpoch_
。explicit
关键字用于防止隐式类型转换。
- 静态成员函数:
static Timestamp now()
:静态函数,用于获取当前时间的Timestamp
对象。
- 成员函数:
std::string toString() const
:将时间戳转换为可读的字符串格式。
3. 类实现(Timestamp.cpp
)
#include <time.h>#include "Timestamp.h"
using namespace mymuduo::base;
Timestamp::Timestamp() : microSecondsSinceEpoch_(0)
{
}Timestamp::Timestamp(int64_t microSecondsSinceEpoch): microSecondsSinceEpoch_(microSecondsSinceEpoch)
{
}Timestamp Timestamp::now()
{return Timestamp(time(NULL));
}
std::string Timestamp::toString() const
{char buf[128] = {0};tm *tm_time = localtime(µSecondsSinceEpoch_);snprintf(buf, 128, "%4d/%02d/%02d %02d:%02d:%02d",tm_time->tm_year + 1900,tm_time->tm_mon + 1,tm_time->tm_mday,tm_time->tm_hour,tm_time->tm_min,tm_time->tm_sec);return buf;
}//g++ -o test timestamp.cpp -I ../include //g++编译命令
//测试代码
// #include <iostream>
// int main() {
// std::cout << Timestamp::now().toString() << std::endl;
// return 0;
// }
- 构造函数实现:
Timestamp()
:将microSecondsSinceEpoch_
初始化为 0。Timestamp(int64_t microSecondsSinceEpoch)
:使用传入的微秒数初始化microSecondsSinceEpoch_
。
now()
函数实现:- 使用
time(NULL)
函数获取当前时间的秒数,并创建一个Timestamp
对象返回。
- 使用
toString()
函数实现:- 使用
localtime()
函数将时间戳转换为本地时间的tm
结构体。 - 使用
snprintf()
函数将tm
结构体中的年、月、日、时、分、秒格式化为字符串。 - 返回格式化后的字符串。
- 使用
4. 代码示例及编译指令
代码文件中提供了一个简单的测试示例,用于验证 Timestamp
类的功能:
#include <iostream>
int main() {std::cout << Timestamp::now().toString() << std::endl;return 0;
}
编译指令为:
g++ -o test timestamp.cpp -I ../include
这个指令将 timestamp.cpp
文件编译成可执行文件 test
,并指定头文件搜索路径为 ../include
。
结果输出:
三、日志
1. 整体概述
本日志系统采用了 iostream
风格,这与 muduo 原版日志有所不同。iostream
风格提供了一种直观且易于使用的方式来格式化和输出日志信息,通过重载 <<
运算符,使得日志记录代码更加简洁和直观。
2. 核心组件
LogStream 类
LogStream
类是日志系统的核心,它负责格式化日志信息并将其输出。以下是 LogStream
类的关键特性:
- 构造函数:在构造时,它会添加日志的基本信息,如时间戳、线程 ID、日志级别、文件名、行号和函数名。
LogStream::LogStream(Logger *loger, const char* file, int line, LogLevel l, const char* func)
:logger_(loger)
{const char* file_name = strrchr(file,'/');if(file_name){file_name = file_name + 1;}else{file_name = file;}stream_ << Timestamp::now().toString() << "[pid]:";if(thread_id == 0){thread_id = static_cast<pid_t>(::syscall(SYS_gettid));}stream_ << thread_id;stream_ << log_string[l];stream_ << "[" << file_name << ":" << line << "]";if(func){stream_ << "[" << func << "]";}
}
- 析构函数:在析构时,它会添加换行符,并将格式化好的日志信息传递给
Logger
类进行输出。
LogStream::~LogStream()
{stream_ << "\n";if(logger_){logger_->Write(stream_.str());}else{std::cout << stream_.str() << std::endl ;}
}
- 重载
<<
运算符:通过模板函数重载<<
运算符,允许用户以iostream
风格添加任意类型的数据到日志流中。
template<class T> LogStream& operator<<(const T& value)
{stream_ << value;return *this;
}
Logger 类(和muduo相同只实现最简单控制台的日志输出,可以修改为带有日志旋转的文件日志)
Logger
类负责管理日志级别和输出日志信息。它提供了以下接口:
SetLogLevel
:设置日志级别。
void Logger::SetLogLevel(const LogLevel & level)
{level_ = level;
}
GetLogLevel
:获取当前日志级别
LogLevel Logger::GetLogLevel() const
{return level_;
}
Write
:将格式化好的日志信息输出到标准输出。
void Logger::Write(const std::string &msg)
{std::cout << msg;
}
3. 日志宏定义
为了方便使用,日志系统提供了一系列宏定义,用于不同级别的日志记录:
#define LOG_TRACE \if(g_logger&&mymuduo::base::g_logger->GetLogLevel() <= mymuduo::base::kTrace) \mymuduo::base::LogStream(mymuduo::base::g_logger,__FILE__,__LINE__,mymuduo::base::kTrace,__func__)#define LOG_DEBUG \if(g_logger&&mymuduo::base::g_logger->GetLogLevel() <= mymuduo::base::kDebug) \mymuduo::base::LogStream(mymuduo::base::g_logger,__FILE__,__LINE__,mymuduo::base::kDebug,__func__)#define LOG_INFO \if(g_logger&&mymuduo::base::g_logger->GetLogLevel() <= mymuduo::base::kInfo) \mymuduo::base::LogStream(mymuduo::base::g_logger,__FILE__,__LINE__,mymuduo::base::kInfo)#define LOG_WARN \mymuduo::base::LogStream(mymuduo::base::g_logger,__FILE__,__LINE__,mymuduo::base::kWarn)#define LOG_ERROR mymuduo::base::LogStream(mymuduo::base::g_logger,__FILE__,__LINE__,mymuduo::base::kError)
4. 使用示例
以下是一个简单的使用示例,展示了如何使用日志系统:
int main()
{g_logger = new Logger();g_logger->SetLogLevel(kInfo);LOG_INFO << "test info";return 0;
}
编译指令:
g++ -o testlog Logger.cpp Timestamp.cpp LogStream.cpp -I ../include
结果输出:
2025/06/06 17:38:15[pid]:8294 INFO [LogStream.cpp:66]test info
附录
Timestamp.h
#pragma once#include <iostream>
#include <string>
namespace mymuduo
{namespace base{class Timestamp{public:Timestamp();explicit Timestamp(int64_t microSecondsSinceEpoch);static Timestamp now();std::string toString() const;private:int64_t microSecondsSinceEpoch_;};}
}
Timestamp.cpp
#include <time.h>#include "Timestamp.h"
using namespace mymuduo::base;
Timestamp::Timestamp() : microSecondsSinceEpoch_(0)
{
}Timestamp::Timestamp(int64_t microSecondsSinceEpoch): microSecondsSinceEpoch_(microSecondsSinceEpoch)
{
}Timestamp Timestamp::now()
{return Timestamp(time(NULL));
}
std::string Timestamp::toString() const
{char buf[128] = {0};tm *tm_time = localtime(µSecondsSinceEpoch_);snprintf(buf, 128, "%4d/%02d/%02d %02d:%02d:%02d",tm_time->tm_year + 1900,tm_time->tm_mon + 1,tm_time->tm_mday,tm_time->tm_hour,tm_time->tm_min,tm_time->tm_sec);return buf;
}//g++ -o test timestamp.cpp -I ../include
// #include <iostream>
// int main() {
// std::cout << Timestamp::now().toString() << std::endl;
// return 0;
// }
LogStream.h
#pragma once #include "Logger.h"#include <sstream>namespace mymuduo
{namespace base{extern Logger* g_logger;class LogStream{public:LogStream(Logger *loger, const char* file, int line, LogLevel l, const char* func=nullptr);~LogStream();template<class T> LogStream& operator<<(const T& value){stream_ << value;return *this;}private:std::ostringstream stream_;Logger* logger_{nullptr};};}
}#define LOG_TRACE \if(g_logger&&mymuduo::base::g_logger->GetLogLevel() <= mymuduo::base::kTrace) \mymuduo::base::LogStream(mymuduo::base::g_logger,__FILE__,__LINE__,mymuduo::base::kTrace,__func__)#define LOG_DEBUG \if(g_logger&&mymuduo::base::g_logger->GetLogLevel() <= mymuduo::base::kDebug) \mymuduo::base::LogStream(mymuduo::base::g_logger,__FILE__,__LINE__,mymuduo::base::kDebug,__func__)#define LOG_INFO \if(g_logger&&mymuduo::base::g_logger->GetLogLevel() <= mymuduo::base::kInfo) \mymuduo::base::LogStream(mymuduo::base::g_logger,__FILE__,__LINE__,mymuduo::base::kInfo)#define LOG_WARN \mymuduo::base::LogStream(mymuduo::base::g_logger,__FILE__,__LINE__,mymuduo::base::kWarn)#define LOG_ERROR mymuduo::base::LogStream(mymuduo::base::g_logger,__FILE__,__LINE__,mymuduo::base::kError)
LogStream.cpp
#include "LogStream.h"
#include "Timestamp.h"#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <iostream>using namespace mymuduo::base;
Logger* mymuduo::base::g_logger = nullptr;static thread_local pid_t thread_id = 0;
const char* log_string[] = {" TRACE "," DEBUG "," INFO "," WARN "," ERROR ",
};LogStream::LogStream(Logger *loger, const char* file, int line, LogLevel l, const char* func)
:logger_(loger)
{const char* file_name = strrchr(file,'/');if(file_name){file_name = file_name + 1;}else{file_name = file;}stream_ << Timestamp::now().toString() << "[pid]:";if(thread_id == 0){thread_id = static_cast<pid_t>(::syscall(SYS_gettid));}stream_ << thread_id;stream_ << log_string[l];stream_ << "[" << file_name << ":" << line << "]";if(func){stream_ << "[" << func << "]";}
}LogStream::~LogStream()
{stream_ << "\n";if(logger_){logger_->Write(stream_.str());}else{std::cout << stream_.str() << std::endl ;}
}// //g++ -o testlog Logger.cpp Timestamp.cpp LogStream.cpp -I ../include
// int main()
// {
// g_logger = new Logger();
// g_logger->SetLogLevel(kInfo);
// LOG_INFO << "test info";
// return 0;
// }
Logger.h
#pragma once
#include "NonCopyable.h"#include <string>namespace mymuduo
{namespace base{enum LogLevel{kTrace,kDebug,kInfo,kWarn,kError,kMaxNumOfLogLevel,};class Logger: public NonCopyable{public:Logger() = default;~Logger() = default;void SetLogLevel(const LogLevel & level);LogLevel GetLogLevel() const;void Write(const std::string &msg);private:LogLevel level_ {kDebug};};}
}
Logger.cpp
#include "Logger.h"#include <iostream>using namespace mymuduo::base;void Logger::SetLogLevel(const LogLevel & level)
{level_ = level;
}LogLevel Logger::GetLogLevel() const
{return level_;
}void Logger::Write(const std::string &msg)
{std::cout << msg;
}
相关文章:

手写muduo网络库(一):项目构建和时间戳、日志库
引言 本文作为手写 muduo 网络库系列开篇,聚焦项目基础框架搭建与核心基础工具模块设计。通过解析 CMake 工程结构设计、目录规划原则,结合时间戳与日志系统的架构,为后续网络库开发奠定工程化基础。文中附完整 CMake 配置示例及模块代码。 …...
每日算法刷题Day25 6.7:leetcode二分答案3道题,用时1h40min(遇到两道动态规划和贪心时间较长)
3. 1631.最小体力消耗路径(中等,dfs不熟练) 1631. 最小体力消耗路径 - 力扣(LeetCode) 思想 1.你准备参加一场远足活动。给你一个二维 rows x columns 的地图 heights ,其中 heights[row][col] 表示格子 (row, col) 的高度。一开始你在最左…...

14-Oracle 23ai Vector Search 向量索引和混合索引-实操
一、Oracle 23ai支持的2种主要的向量索引类型: 1.1 内存中的邻居图向量索引 (In-Memory Neighbor Graph Vector Index) HNSW(Hierarchical Navigable Small World :分层可导航小世界)索引 是 Oracle AI Vector Search 中唯一支持的内存邻居图向量索引类…...
kubeadm安装k8s
1、环境准备 1.1、升级系统内核 参考另一篇文章:https://blog.csdn.net/u012533920/article/details/148457715?spm1011.2415.3001.5331 1.2、设置Hostname cat <<EOF > /etc/hosts 127.0.0.1 localhost localhost.localdomain localhost4 localhos…...
服务器新建用户无法使用conda
服务器新建用户无法使用conda 1.将.bashrc文件复制到新用户家目录下 sudo cp .bashrc /home/newuser/.bashrc2.source命令激活该文件 source ~/.bashrc3.将.condarc文件复制到新用户家目录下 sudo cp .condarc/home/newuser/.condarc...

Web前端基础:JavaScript
1.JS核心语法 1.1 JS引入方式 第一种方式:内部脚本,将JS代码定义在HTML页面中 JavaScript代码必须位于<script></script>标签之间在HTML文档中,可以在任意地方,放置任意数量的<script></script>一般会把…...
基于对比学习的带钢表面缺陷分类研究,整合SimCLR自监督预训练与YOLOv8目标检测框架的技术解析及Python实现方案
以下基于对比学习的带钢表面缺陷分类研究,整合SimCLR自监督预训练与YOLOv8目标检测框架的技术解析及Python实现方案: 基于对比学习的带钢表面缺陷分类研究 ——SimCLR与YOLOv8算法融合应用 #mermaid-svg-VqDPIOfR5WJcGtD7 {font-family:"trebuchet ms",verdana,ar…...

基于AWS Serverless架构:零运维构建自动化SEO内容生成系统
作者:[Allen] 技术专栏 | 深度解析云原生SEO自动化 在流量为王的时代,持续产出高质量SEO内容成为技术运营的核心痛点。传统方案面临开发成本高、扩展性差、关键词响应滞后三大难题。本文将分享如何用AWS Serverless技术栈,构建一套零服务器运…...
【.net core】天地图坐标转换为高德地图坐标(WGS84 坐标转 GCJ02 坐标)
类文件 public static class WGS84ToGCJ02Helper {// 定义一些常量private const double PI 3.14159265358979324;private const double A 6378245.0;private const double EE 0.00669342162296594323;// 判断坐标是否在中国范围内(不在国内则不进行转换&#x…...
Linux操作系统故障应急场景及对应排查方法
001:系统CPU负载高并触发监控报警 005 查看系统CPU使用情况,,确认CPU数量,确认系统负载,确认CPU高对系统的影响 006 定位占用CPU资源最多的进程,根据进程判断是应用进程还是系统进程还是第三方工具进程。 014 查看…...

电镀机的阳极是什么材质?
知识星球(星球名:芯片制造与封测技术社区,点击加入)里的学员问:电镀的阳极有什么讲究?什么是可溶性阳极和非可溶性阳极? 什么是可溶性阳极与非可溶性阳极? 可溶性阳极 阳极本身就是…...

vscode调试deepspeed的方法之一(无需调整脚本)
现在deepspeed的脚本文件是: # 因为使用 RTX 4000 系列显卡时,不支持通过 P2P 或 IB 实现更快的通信宽带,需要设置以下两个环境变量 # 禁用 NCCL 的 P2P 通信,以避免可能出现的兼容性问题 export NCCL_P2P_DISABLE"1" …...
神经网络-Day44
import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader import matplotlib.pyplot as plt# 设置中文字体支持 plt.rcParams["font.family"] ["SimHei"…...
创客匠人:如何通过精准定位实现创始人IP打造与知识变现
在当今知识经济时代,越来越多的专业人士希望通过个人品牌实现知识变现,但许多人面临一个共同困境:明明很努力,却收效甚微。创客匠人作为深耕知识付费赛道9年的专业机构,揭示了这一现象背后的关键原因——90%的IP失败源…...

Codeforces Round 509 (Div. 2) C. Coffee Break
题目大意: 给你n、m、d n为元素个数,m为数列长度,d为每个元素之间的最短间隔 问最少需要多少个数列可以使得元素都能装进数列,并且满足每个元素之间的间隔大于等于d 核心思想 使用贪心的思想,将元素的大小进行排序,问题出在必…...

榕壹云健身预约系统:多门店管理的数字化解决方案(ThinkPHP+MySQL+UniApp实现)
随着全民健身热潮的兴起,传统健身房在会员管理、课程预约、多门店运营等方面面临诸多挑战。针对这一需求,我们开发了一款基于ThinkPHPMySQLUniApp的榕壹云健身预约系统,为中小型健身机构及连锁品牌提供高效、灵活的数字化管理工具。本文将详细…...

QUIC——UDP实现可靠性传输
首先我们要知道TCP存在什么样的痛点问题 TCP的升级很困难TCP建立连接的延迟网络迁移需要重新建立连接TCP存在队头阻塞问题 QUIC就是为了解决以上的问题而诞生了, 下面我会介绍QUIC的一些特性和原理 QUIC对比TCP优势: 握手建连更快 QUIC内部包含了TLS, 它在自己的帧会携带TL…...
提高Python编程效率的工具推荐
在 Python 开发中,选择合适的工具可以显著提升编程效率。以下是一些经过精心挑选的工具,涵盖代码编辑、调试、数据分析等多个方面,希望能帮助你在 Python 开发中事半功倍。 一、集成开发环境(IDE) 1. PyCharm PyCha…...
React Native图片预加载:让你的应用图片预览像德芙一样丝滑
写在前面:一张图片引发的性能血案 你有没有遇到过这种情况?——用户疯狂滑动你的React Native图片列表,结果图片加载慢得像蜗牛,甚至出现空白闪烁?等到图片终于加载出来,用户早就失去耐心,愤然退出…… 但你知道吗?这个问题只需要几行代码就能解决! 比如,使用reac…...

快速上手shell脚本运行流程控制
一、条件运行流程控制 1.if单分支结构 #!/bin/bash if [ 条件 ] then动作1动作2... fi 2.if双分支结构 #!/bin/bash if [ 条件 ] then动作1动作2... else动作1动作2... fi 3.if多分支结构 二、循环运行流程控制 1.无判定for循环 给网卡一键添加5个IP 2.判断循环 while…...

10.Linux进程信号
1. 理解信号 信号VS信号量 老婆:老婆饼-》没有任何关系!信号:闹钟,上课铃声,脸色...人-》进程;信号中断人正在做的事,是一种事件的异步通知机制; 我们自习一会,等张三回…...
Python 函数全攻略:函数基础
函数(Functions)基础 什么是函数? 一个命名的代码块,代指一大堆代码。 定义: def function_name(): (使用def关键字,英文括号,冒号,缩进代码块)。 执行/调用: function…...

机器学习基础(四) 决策树
决策树简介 决策树结构: 决策树是一种树形结构,树中每个内部节点表示一个特征上的判断,每个分支代表一个判断结果的输出,每个叶子节点代表一种分类结果 决策树构建过程(三要素): 特征选择 选…...
DDPM优化目标公式推导
DDPM优化目标公式推导 DDPM优化目标公式推导**1. 问题定义****2. 优化目标:最大化对数似然****3. 变分下界的分解****4. 关键步骤:简化 KL 散度项****(a) 后验分布 q ( x t − 1 ∣ x t , x 0 ) q(\mathbf{x}_{t-1} | \mathbf{x}_t, \mathbf{x}_0) q(xt…...

CentOS 7如何编译安装升级gcc至7.5版本?
CentOS 7如何编译安装升级gcc版本? 由于配置CentOS-SCLo-scl.repo与CentOS-SCLo-scl-rh.repo后执行yum install -y devtoolset-7安装总是异常,遂决定编译安装gcc7.5 # 备份之前的yum .repo文件至 /tmp/repo_bak 目录 mkdir -p /tmp/repo_bak && cd /etc…...

为什么React列表项需要key?(React key)(稳定的唯一标识key有助于React虚拟DOM优化重绘大型列表)
文章目录 1. **帮助 React 识别列表项的变化**2. **性能优化**3. **避免组件状态混乱**4. **为什么使用 rpid 作为 key**5. **不好的做法示例**6. **✅ 正确的做法** 在 React 中添加 key{item.rpid} 是非常重要的,主要有以下几个原因: 1. 帮助 React 识…...
Playwright自动化测试全栈指南:从基础到企业级实践(2025终极版)
引言 在Web应用复杂度指数级增长的今天,传统自动化测试工具面临动态渲染适配难、多浏览器兼容差、测试稳定性低三大挑战。微软开源的Playwright凭借跨浏览器支持、自动等待机制和原生异步架构,成为新一代自动化测试的事实标…...

飞牛云一键设置动态域名+ipv6内网直通访问内网的ssh服务-家庭云计算专家
IPv6访问SSH的难点与优势并存。难点主要体现在网络环境支持不足:部分ISP未完全适配IPv6协议,导致客户端无法直接连通;老旧设备或工具(如Docker、GitHub)需额外配置才能兼容IPv6,技术门槛较高;若…...
虚实共生时代的情感重构:AI 恋爱陪伴的崛起、困局与明日图景
一、虚拟恋人:从技术幻想到情感刚需的跨越 在 5G 网络编织的数字浪潮里,AI 驱动的虚拟恋人正打破次元界限。深度学习算法剖析 3000 万段真实对话语料库,搭配 VR 设备带来的多维度交互体验,如今的虚拟对象已能精准模拟瞳孔微表情&…...
嵌入式面试高频(5)!!!C++语言(嵌入式八股文,嵌入式面经)
一、C有几种传值方式之间的区别 一、值传递(Pass by Value) 机制:创建参数的副本,函数内操作不影响原始数据语法:void func(int x)特点: 数据安全:原始数据不受影响性能开销:需要复…...