-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.json
More file actions
1 lines (1 loc) · 92.8 KB
/
content.json
File metadata and controls
1 lines (1 loc) · 92.8 KB
1
{"meta":{"title":"Hexo","subtitle":"","description":"","author":"DxBlackSmith","url":"https://dxblacksmith.github.io","root":"/"},"pages":[{"title":"categories","date":"2026-01-13T10:42:48.000Z","updated":"2026-01-13T10:46:21.352Z","comments":false,"path":"categories/index.html","permalink":"https://dxblacksmith.github.io/categories/index.html","excerpt":"","text":""},{"title":"about","date":"2026-01-13T09:57:44.000Z","updated":"2026-01-13T09:58:17.610Z","comments":false,"path":"about/index.html","permalink":"https://dxblacksmith.github.io/about/index.html","excerpt":"","text":""}],"posts":[{"title":"Java 分层架构","slug":"Java-分层架构","date":"2026-02-04T14:13:31.798Z","updated":"2026-02-04T14:13:32.194Z","comments":true,"path":"2026/02/04/Java-分层架构/","permalink":"https://dxblacksmith.github.io/2026/02/04/Java-%E5%88%86%E5%B1%82%E6%9E%B6%E6%9E%84/","excerpt":"","text":"Java 分层架构一、Controller/ Service / Dao 这是现代企业级应用(尤其是 Java Web 应用)中常见的 分层架构模式,目的是解耦、提高可维护性和可测试性。 整体的架构图如下所示: 123456789[客户端] ↓ (HTTP 请求)@Controller(控制器) ↓ 调用@Service(业务逻辑层) ↓ 调用@Mapper / @Repository(数据访问层,即 DAO) ↓[数据库] 1. DAO(Data Access Object)—— 数据访问层职责: 封装对数据库的 CRUD 操作 只负责数据存取,不包含业务逻辑。 实现可以是原生 JDBC -> UserDao 接口 + 实现类或者 MyBatis -> @Mapper 接口 123456@Mapperpublic interface UserMapper { User selectById(Long id); void insert(User user); List<User> findAll();} ✅DAO = 数据的“搬运工”,只管“拿”和“存”。 2. Service(业务逻辑层)职责: 实现 核心业务逻辑(如注册、支付、订单处理)。 协调多个 DAO 操作(例如:注册用户 + 发送邮件 + 记录日志)。 无状态,可被多个 Controller 复用。 1234567891011121314151617@Service@Transactionalpublic class UserService { @Autowired private UserMapper userMapper; public Long register(String name, String email) { // 1. 校验逻辑(业务规则) if (userMapper.existsByEmail(email)) { throw new BusinessException("邮箱已存在"); } // 2. 调用 DAO 保存数据 User user = new User(name, email); userMapper.insert(user); return user.getId(); }} ✅Service = 业务的“大脑”,决定“做什么”和“怎么做”。 3. Controller(控制层)职责: 接收 HTTP 请求 解析请求参数(JSON、路径变量、表单) 调用 Service 执行业务 封装响应结果(返回 JSON、跳转页面) 技术实现上一般是 Spring MVC 的 @RestController 或 @Controller 123456789101112@RestController@RequestMapping("/api/users")public class UserController { @Autowired private UserService userService; @PostMapping public ResponseEntity<Long> register(@RequestBody RegisterRequest req) { Long userId = userService.register(req.getName(), req.getEmail()); return ResponseEntity.ok(userId); }} ✅ Controller = 系统的“门面”,负责“接客”和“传话”。 Ps. 传统 MVC (如早期 Spring MVC) ,Model 相当与这里的 Service + Dao + Entity, View 则是被独立出去,变成了前端(展示数据),Controller 上职责有一些变化(传统是协调 Model 和 View,而现在 Java web 中是负责接收 + 调用 Services)","categories":[{"name":"Java","slug":"Java","permalink":"https://dxblacksmith.github.io/categories/Java/"}],"tags":[]},{"title":"JDBC","slug":"JDBC","date":"2026-02-04T12:36:32.479Z","updated":"2026-02-04T13:49:51.042Z","comments":true,"path":"2026/02/04/JDBC/","permalink":"https://dxblacksmith.github.io/2026/02/04/JDBC/","excerpt":"","text":"JDBC JDBC 是 Java 提供的一套用于连接和操作关系型数据库的标准 API。允许 Java 程序通过 SQL 语句与数据库进行交互,而无需关心底层数据库的具体实现(只要数据库提供对应的 JDBC 驱动) 一、核心接口1. Connection(连接)作用: 表示应用程序与数据库之间的会话连接。 是所有数据库操作的起点:通过 Connection 可以创建 Statement,PreparedStatement 等对象。 负责管理事务(比如提交 Commit,回滚 rollback()) 12sql::mysql::MySQL_Driver* driver = sql::mysql::get_mysql_driver_instance();auto* con = driver->connect(url_, user_, pass_); 2. Statement(语句)作用:用于执行静态 SQL 语句(即不带参数的 SQL),适用于执行 DDL(如 CREATE TABLE)或简单查询 12std::unique_ptr<sql::Statement> stmt(con->_con->createStatement());stmt->executeQuery("SELECT 1"); 该接口容易发生 SQL 注入,每次执行都需要重新编译 SQL(效率较低) 3. PreparedStatement(预编译语句)继承自 Statement,作用: 执行带参数的预编译 SQL 语句。 SQL 语句中使用 ? 作为占位符,参数通过 setXxx() 方法设置 123456// 准备调用存储过程unique_ptr <sql::PreparedStatement> stmt(con->prepareStatement("CALL reg_user(?,?,?,@result)"));// 设置输入参数stmt->setString(1, name);stmt->setString(2, email);stmt->setString(3, pwd); 解决了 statement 的问题,防止 SQL 注入,同时性能更高:SQL 语句在数据库端预编译,多次执行时只需替换参数,无需重复解析。 4. ResultSet(结果集)作用: 表示执行查询(SELECT)后返回的数据结果集合。 类似一个“游标”,初始位置在第一行之前,通过 next() 向下移动 1234567unique_ptr<sql::ResultSet> res(stmtResult->executeQuery("SELECT @result AS result"));if (res->next()) { int result = res->getInt("result"); cout << "Result: " << result << endl; pool_->returnConnection(std::move(con)); return result;} 只能向前遍历(默认情况下),同时必须在 Connection 和 Statement 关闭前读取数据。","categories":[{"name":"数据库","slug":"数据库","permalink":"https://dxblacksmith.github.io/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/"}],"tags":[]},{"title":"JavaScript","slug":"Js","date":"2026-02-04T11:44:35.813Z","updated":"2026-02-04T14:34:41.290Z","comments":true,"path":"2026/02/04/Js/","permalink":"https://dxblacksmith.github.io/2026/02/04/Js/","excerpt":"","text":"JavaScript 基础一、Nodejs, Js, npm 的关系JavaScript 是一种动态、弱类型、解释型的脚本语言(基于 C++ 封装) Node.js 是基于 Chrome V8 引擎构建的 JavaScript Runtime,目的是能 js 在服务端上运行 npm(Node Package Manager) 是 Nodejs 的官方包管理工具,目的是管理第三方代码依赖 三者关系是: 12345JavaScript(语言) ↑Node.js(运行环境:提供 fs/http/crypto 等 API + V8 引擎) ↑npm(包管理器:安装/发布/管理 JS 库) 二、隔离不同项目依赖不像 python 有 venv, Js 是通过本地 node_modules + 锁定文件来实现逻辑上的隔离 每个项目有自己的 node_modules 目录比如执行 npm install ioredis 的时候,包会被安装到当前项目目录下的 ./node_modules。 不同项目的 node_modules 互不影响 依赖解析规则(当代码中写 const Redis = require('ioredis') 的时候,Node.js 会: 优先在 当前项目 node_modules 中找; 如果没有,逐级向上查找父目录的 node_modules,直到根目录; 不会自动使用全局安装的包(除非显式指定)。 锁定依赖版本npm install 下载生成的 package-lock.json 记录精确版本号 + 依赖树结构。 团队成员执行 npm ci 可重建完全一致的 node_modules。 三、JavaScript 的内存管理内存分配机制Js 引擎在运行时自动推断值的类型,并为其分配合适内存: 1234let a = 42; // → 存为 32 位整数(Smis,V8 优化)a = 3.14; // → 改为 64 位双精度浮点数(HeapNumber)a = "hello"; // → 分配字符串对象(在堆上)a = { x: 1 }; // → 分配对象(在堆上) 其中基本类型(number, string, boolean, symbol)小整数或短字符串可能直接内联存储(栈或对象内),大值存在堆上 引用类型(Object, Array, Function) 总是分配在堆上, 垃圾回收机制Js 的内存管理机制是自动垃圾回收(GC),C++ 现在则是利用 RAII 思想来析构对象,可以直接用智能指针来管理对象 后现代语言通常采用 GC 机制回收变量,当创建的对象不再被使用的时候就会变成 “垃圾”,被 GC 自动回收。 识别核心也是: 引用计数(存在循环引用问题):每个对象有计数器,被引用一次,计数就 + 1,引用消失,计数就 -1,为 0 时就被回收 可达性分析:从 “根对象”(全局变量,栈变量,当前执行上下文)开始遍历,能遍历到的 = 活着,遍历不到 = 垃圾 回收核心则是: 分代回收(Java/JS v8 核心):把对象分为 ”新对象“/”老对象“,其中新生代对象死得快,GC 频繁,老生代对象活得久,GC 少(效率高) Mark-Sweep:先标记所有可达对象,再清除未标记的垃圾 Mark-Compact:标记 → 把存活对象往一边挤 → 清除剩下的垃圾,这样能够解决碎片问题 GC 的缺点很明显: GC 工作时,会有停顿(STW),影响系统性能 GC 本身会有开销,占 CPU,内存","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://dxblacksmith.github.io/categories/JavaScript/"}],"tags":[]},{"title":"池化","slug":"池化","date":"2026-02-03T08:49:37.292Z","updated":"2026-02-03T08:49:37.575Z","comments":true,"path":"2026/02/03/池化/","permalink":"https://dxblacksmith.github.io/2026/02/03/%E6%B1%A0%E5%8C%96/","excerpt":"","text":"池化 池化是一种资源复用计数,通过预先创建一组可复用的资源对象,避免频繁申请/释放带来的开销(如 TCP 连接建立、线程上下文切换、内存分配等),从而提升系统性能和稳定性。 一. 分配策略特点:分配策略 + 无状态共享 核心设计: 预先创建多个 io_context 对象(默认 2 个),每个运行在独立线程中。 使用 轮询(Round-Robin)策略 分配 io_context 给新连接。 每个 io_context 绑定一个 work guard,防止因无任务而退出。 不涉及借还,分配后 socket 生命周期内独占该 io_context。 Asio 连接池:IOContextPool.h 123456789101112131415161718// 创建 IOService 池子, acceptor 接收到连接以后取出一个 IOService 来创建 Http 管理(含通信套接字), 该 ioc 就负责通信套接字上的 IO 读写class CAsioIoServicePool : public Singleton<CAsioIoServicePool> { friend class Singleton<CAsioIoServicePool>;public: using IOService = boost::asio::io_context; // 创建一个 work guard 来防止 io_context 因为没有任务而提前结束 using Work = boost::asio::executor_work_guard<boost::asio::io_context::executor_type>; ~CAsioIoServicePool(); // round-robin 策略: 分配策略 boost::asio::io_context& GetIOService(); void Stop();private: CAsioIoServicePool(size_t size = 2); std::vector<IOService> m_IOServices; std::vector<Work> m_Works; std::vector<std::thread> m_Threads; std::size_t _nextIOService;}; 二. 借还策略特点:借还策略 + 有状态独占 核心设计: 预先创建多个 VerifyService::Stub(每个 stub 内含一个 gRPC channel,即 TCP 连接)。 线程通过 getConnection() 借出 一个 stub,使用完毕必须调用 pushConnection() 归还。 使用 std::mutex + std::condition_variable 保护连接队列,支持等待空闲连接。 同一 stub 在被借出期间只能由一个线程使用(gRPC stub 非线程安全)。 grpc 连接池:RPConPoo.h 1234567891011121314151617181920// 借还策略:池化多个存根 stub 创建多个通信通道 channel 谁用就来取一个, 用完之后要还给池子class RPConPool {public: RPConPool(size_t poolSize, std::string host, std::string port); ~RPConPool(); std::unique_ptr<VerifyService::Stub> getConnection(); void pushConnection(std::unique_ptr<VerifyService::Stub> context); void Close();private: std::atomic<bool> m_stop_; std::queue<std::unique_ptr<VerifyService::Stub>> m_conn_; std::mutex m_mtx_; std::condition_variable m_cond_; size_t m_poolSize_; std::string m_host_; std::string m_port_;}; redis 连接池:123456789101112131415161718class RedisConPool {public: RedisConPool(size_t poolSize, const char* host, int port, const char* pwd); ~RedisConPool(); redisContext* getConnection(); void returnConnection(redisContext* context); void Close();private: // 连接元数据 const char* m_host_; int m_port_; size_t m_poolSize_; // 连接池 std::mutex m_mtx_; std::condition_variable m_cond_; std::queue<redisContext*> m_conns_; std::atomic<bool> m_stop_;};","categories":[{"name":"项目经验","slug":"项目经验","permalink":"https://dxblacksmith.github.io/categories/%E9%A1%B9%E7%9B%AE%E7%BB%8F%E9%AA%8C/"}],"tags":[]},{"title":"redis 的使用","slug":"redis 的使用","date":"2026-02-03T07:11:04.710Z","updated":"2026-02-03T07:11:04.960Z","comments":true,"path":"2026/02/03/redis 的使用/","permalink":"https://dxblacksmith.github.io/2026/02/03/redis%20%E7%9A%84%E4%BD%BF%E7%94%A8/","excerpt":"","text":"Redisredis(Remote Directionary Server)是一个开源的、基于内存的键值存储系统,常被用作数据库、缓存和消息中间件。支持多种数据结构,每种结构都有对应的命令操作。 一、Redis 的对象类型Redis 的值(value)可以是以下五种基本数据类型之一,前三种比较常用: 1. String (字符串) 最基本的数据类型。 可以存储字符串、整数或浮点数(最大 512MB)。 常用于缓存简单数据,如用户信息、计数器等。 常用操作: 123456789SET key value # 设置键值GET key # 获取值INCR key # 自增 1(值必须为整数)DECR key # 自减 1INCRBY key increment # 增加指定整数APPEND key value # 追加字符串STRLEN key # 获取字符串长度MSET key1 val1 key2 val2 ... # 批量设置MGET key1 key2 ... # 批量获取 Ps. Redis 的 key 总是字符串类型 2. Hash(哈希)类似二级缓存,【WebSite】【blogSite】 = example.com 类似于一个“字典”或“对象”,存储字段(field)和值(value)的映射。 适合存储对象(如用户资料:name、age、email 等字段)。 常用操作: 12345678910HSET key field value # 设置哈希字段的值HGET key field # 获取哈希字段的值HMSET key field1 val1 field2 val2 ... # 批量设置(Redis 4.0+ 推荐用 HSET)HMGET key field1 field2 ... # 批量获取HGETALL key # 获取所有字段和值(慎用,大数据量会阻塞)HDEL key field [field...] # 删除一个或多个字段HEXISTS key field # 判断字段是否存在HLEN key # 获取哈希中字段数量HKEYS key # 获取所有字段名HVALS key # 获取所有字段值 3. List(列表) 有序、可重复的字符串列表,底层是双向链表。 常用于消息队列、最新 N 条记录等场景。 常用操作: 1234LPUSH key value [value...] # 从左边插入RPUSH key value [value...] # 从右边插入LPOP key # 弹出左边第一个元素RPOP key # 弹出右边第一个元素 4. Set(集合) 类似 Set,但每个成员关联一个分数(score),按分数排序。 常用于排行榜、带权重的任务队列等。 1234SADD key member [member...] # 添加成员SMEMBERS key # 获取所有成员SISMEMBER key member # 判断是否是成员SCARD key # 获取集合大小 5. Sorted Set(有序集合 / ZSet) 类似 Set,但是每个成员关联一个分数(score),按分数排序。 常用于排行榜、带权重的任务队列等。 1234ZADD key score member [score member...] # 添加成员及分数ZRANGE key start stop [WITHSCORES] # 按分数升序返回成员ZREVRANGE key start stop [WITHSCORES] # 按分数降序ZSCORE key member # 获取成员的分数 二、HiRedisHiRedis 是 C++ 的一种库,C++ 还有其他库(redis-plus-plus)等等。源码这里不去介绍,不过通常我们会对 Redis 提供的操作进行一些封装,比如 1. Connect 操作12345678bool CRedisMgr::Connect(const std::string& host, int port) { this->m_conn_ = redisConnect(host.c_str(), port); if (this->m_conn_ == nullptr) return false; if (this->m_conn_ != nullptr && this->m_conn_->err) { return false; } return true;} 2. Auth 操作1234567891011bool CRedisMgr::Auth(const std::string& password) { this->m_reply_ = (redisReply*)redisCommand(this->m_conn_, "AUTH %s", password.c_str()); if (this->m_reply_->type == REDIS_REPLY_ERROR) { freeReplyObject(this->m_reply_); return false; } else { freeReplyObject(this->m_reply_); return false; } return 3. Get 操作123456789101112131415bool CRedisMgr::Get(const std::string& key, std::string& value) { this->m_reply_ = (redisReply*)redisCommand(this->m_conn_, "GET %s", key.c_str()); if (this->m_reply_ == nullptr) { freeReplyObject(this->m_reply_); return false; } if (this->m_reply_->type == REDIS_REPLY_ERROR) { freeReplyObject(this->m_reply_); return false; } value = this->m_reply_->str; freeReplyObject(this->m_reply_); std::cout << "Succeed to execute command [ GET " << key << " ]" << '\\n'; return false;} 4. HSet 操作1234567891011121314151617181920bool CRedisMgr::HSet(const char* key, const char* hkey, const char* hvalue, size_t hvalueLen) { // 采用 redisCommandArgv 方法 const char* argv[4]; size_t argvlen[4]; argv[0] = "HSET"; argvlen[0] = 4; argv[1] = key; argvlen[1] = strlen(key); argv[2] = hkey; argvlen[2] = strlen(hkey); argv[3] = hvalue; argvlen[3] = hvalueLen; this->m_reply_ = (redisReply*)redisCommandArgv(this->m_conn_, 4, argv, argvlen); if (this->m_reply_ == nullptr || m_reply_->type != REDIS_REPLY_INTEGER) { freeReplyObject(this->m_reply_); return false; } freeReplyObject(this->m_reply_); return true;} 上面的操作基本是遵循一个模板: 123456789101112bool CRedisMgr::xxx(const std::string& key, std::string& value) { redisContext *context; redisReply *reply = (redisReply*)redisCommand(context, "xxx %s %s", key.c_str(), value.c_str()); if(reply == nullptr || reply == REDIS_REPLY_ERROR) { ~~Debug(); freeReplyObject(reply); return false; } ~~Debug(); freeReplyObject(reply); return true;} 不过比较蛋疼的是不同操作如果出错对应的不同种的宏,所以还得自己去查,这里给个大概的分类 “查询值” 型命令 -> 返回 STRING 或者 NIL(Get, HGet, ZScore) “操作计数” 型命令-> 返回 INTEGER(DEL, HSET, RPUSH, EXISTS, INCR) “状态反馈” 型命令 -> 返回 STATUS(SET, FLUSHDB, SAVE) “集合/列表” 型命令 -> 返回 ARRAY(HGETALL , SMEMBERS)","categories":[{"name":"第三方库","slug":"第三方库","permalink":"https://dxblacksmith.github.io/categories/%E7%AC%AC%E4%B8%89%E6%96%B9%E5%BA%93/"}],"tags":[]},{"title":"nodemailer 的使用","slug":"nodemailer","date":"2026-01-31T13:44:19.253Z","updated":"2026-01-31T13:47:32.980Z","comments":true,"path":"2026/01/31/nodemailer/","permalink":"https://dxblacksmith.github.io/2026/01/31/nodemailer/","excerpt":"","text":"NodeMailer概念介绍NodeMailer 是一个用于 Node.js 的邮件发送库, 通过连接 SMTP 协议,与指定的邮件服务商(QQ、163、Gmail 等)的 SMTP 服务器建立安全连接,并 代理 开发者完成邮件的提交与投递(先提交再投递) 123456789let transport = nodemailer.createTransport({ host: 'smtp.qq.com', // ← 1. 指定 SMTP 服务器地址 port: 465, // ← 2. 使用 SMTPS(SMTP over SSL)端口 secure: true, // ← 3. 启用 TLS/SSL 加密 auto: { user: 'your_email@example.com', // ← 4. 身份认证:邮箱账号 pass: 'your_smtp_auth_code' // ← 5. 身份认证:授权码(非登录密码!) }}) 当执行: 1await transport.sendMail(mailOptiion) nodemailer 会: 建立 TCP 连接 到 smtp.qq.com:465 自动协商 SSL/TLS 加密通道(因为 secure: true) 发送 SMTP 命令序列 EHLO → 握手 AUTH LOGIN → 使用 Base64 编码发送 user 和 pass MAIL FROM:<from@example.com → 声明发件人 RCPT TO:<to@example.com> → 声明收件人 DATA → 发送邮件头 + 正文 QUIT → 断开连接 将邮件提交给 QQ 邮箱的 SMTP 服务器 由 QQ 邮箱系统负责最终投递到目标邮箱(如 163、Gmail、Outlook 等) nodemailer 不直接投递到收件人邮箱,而是把邮件“交给”你配置的 SMTP 服务商(这里是 QQ),由它完成后续路由和投递。 一句话总结就是: ✅ **nodemailer 是一个 SMTP 客户端库,它封装了 SMTP 协议的底层通信细节,让你能用简单的 JavaScript 对象配置,即可通过任意支持 SMTP 的邮件服务商(如 QQ、163、SendGrid、Amazon SES 等)发送邮件。 Nodemailer 使用流程(5 步)第 1 步:安装依赖1npm install nodemailer ⚠️ 注意:仅用于服务端(Node.js),不能在浏览器中使用(涉及 SMTP 密钥,且浏览器无 TCP 权限)。 第 2 步:创建传输器(Transporter)配置 SMTP 服务器连接信息: 123456789let transport = nodemailer.createTransport({ host: 'smtp.qq.com', // ← 1. 指定 SMTP 服务器地址 port: 465, // ← 2. 使用 SMTPS(SMTP over SSL)端口 secure: true, // ← 3. 启用 TLS/SSL 加密 auto: { user: 'your_email@example.com', // ← 4. 身份认证:邮箱账号 pass: 'your_smtp_auth_code' // ← 5. 身份认证:授权码(非登录密码!) }}) 🔐 授权码获取方式(以 QQ 邮箱为例): 登录 QQ 邮箱 → 设置 → 账户 开启 “POP3/SMTP 服务” → 按提示生成 16 位授权码 第 3 步:定义邮件内容(Mail Options)1234567const mailOptions = { from: '"Your App" <your_email@qq.com>', // 发件人(必须与 auth.user 一致或别名) to: 'recipient@example.com', // 收件人(可多个:'a@x.com, b@y.com') subject: '验证码', // 邮件标题 text: '您的验证码是:123456', // 纯文本正文 // html: '<b>您的验证码是:123456</b>' // HTML 正文(可选,优先级高于 text)}; 📌 关键规则: from 中的邮箱地址 必须与 auth.user 一致,否则会被 SMTP 服务器拒绝。 可同时提供 text 和 html,客户端会优先显示 html。 第 4 步:发送邮件(异步)123456789101112async function sendEmail() { try { const info = await transporter.sendMail(mailOptions); console.log('Message sent: %s', info.messageId); // 示例输出: Message sent: <b658f8ca-4d9e-11ea-8f00-0a0b12345678@qq.com> } catch (error) { console.error('Send email failed:', error); // 常见错误:认证失败、网络超时、配额超限 }}sendEmail(); 💡 返回的 info 包含邮件 ID、响应状态等,可用于日志或追踪。 第 5 步(可选):验证 SMTP 配置是否有效12345678// 测试连接(不发邮件)transporter.verify((error, success) => { if (error) { console.log('SMTP config error:', error); } else { console.log('SMTP server is ready to take messages'); }});","categories":[{"name":"第三方库","slug":"第三方库","permalink":"https://dxblacksmith.github.io/categories/%E7%AC%AC%E4%B8%89%E6%96%B9%E5%BA%93/"}],"tags":[]},{"title":"rpc 的使用","slug":"rpc 的使用","date":"2026-01-30T11:43:34.449Z","updated":"2026-01-31T09:53:40.534Z","comments":true,"path":"2026/01/30/rpc 的使用/","permalink":"https://dxblacksmith.github.io/2026/01/30/rpc%20%E7%9A%84%E4%BD%BF%E7%94%A8/","excerpt":"","text":"RPC 介绍核心思想: 让调用远程服务像调用本地函数一样简单 1234// 本地函数调用int result = local_add(1, 2);// RPC 调用 - 看起来一样,但实际在远程执行int result = remote_add(1, 2); // 这个函数在另一台机器上执行 解决了分布式系统中的服务间通信,同时隐藏了网络通信的复杂性,提供了类型安全的远程调用 RPC 基本组件一个完整的 RPC 系统通常包含以下三个层次: 接口定义语言(IDL, Interface Definition Language)—— 描述服务方法和数据结构。 数据序列化格式—— 将结构化数据转换为可传输的字节流。 网络通信协议—— 负责请求/响应的传输、连接管理、错误处理等。 protobuf 能解决前两个问题,比如 123456// 定义接口service UserService { // 服务接口定义 rpc GetUser(GetUserReq) returns (UserRsp); // 远程方法定义 rpc CreateUser(CreateUserReq) returns (UserRsp);}message GetUserReq { int32 user_id = 1; } 12345678// Protobuf 提供序列化方法GetUserReq request;request.set_user_id(123);std::string serialized_data = request.SerializeAsString();// 反序列化GetUserReq new_request;new_request.ParseFromString(serialized_data); 而 rpc 框架通常能自己解决第三层网络协议 RPC 系统分层 第一层:接口描述文件层(IDL Layer),通常用 .proto 来定义 1234567891011121314151617// user_service.proto - 接口描述层syntax = "proto3";service UserService { rpc GetUser(GetUserRequest) returns (UserResponse); rpc CreateUser(CreateUserRequest) returns (UserResponse);}message GetUserRequest { int32 user_id = 1;}message UserResponse { int32 user_id = 1; string name = 2; string email = 3;} 第二层:RPC 协议层(Protocol Layer),通常由 protoc.exe配合 RPC 的插件使用,生成的是比如 .grpc.pb.cc 和 .grpc.pb.h文件 123456789101112131415// 由 protoc 生成的代码 - RPC 协议层class UserService_Stub : public ::google::protobuf::Service {public: // 客户端存根 - 封装调用细节 void GetUser(::google::protobuf::RpcController* controller, const GetUserRequest* request, UserResponse* response, ::google::protobuf::Closure* done); // 服务端骨架 - 提供实现基类 virtual void GetUser(::google::protobuf::RpcController* controller, const GetUserRequest* request, UserResponse* response, ::google::protobuf::Closure* done) = 0;}; 第三层:网络通信层(Transport Layer),这一层通常自己实现 12345678910111213// gRPC 网络通信层class GrpcTransport {public: // 建立连接 bool Connect(const std::string& endpoint); // 发送请求 bool SendRequest(const std::string& method_name, const std::string& serialized_data); // 接收响应 std::string ReceiveResponse(); // 处理网络错误、超时、重试等 void HandleErrors();}; 使用 grpc + protobuf 的完整流程Protocol Buffers 是一种接口定义语言(IDL)和高效的序列化框架,它提供了跨语言的数据结构和服务接口定义能力。 123Protocol Buffers = ├── 接口定义语言 (IDL) // 定义数据结构和服务契约└── 序列化框架 // 提供高效的二进制序列化 gRPC 是基于 Protobuf 构建的一个高性能 RPC 框架,基于 HTTP/2 协议构建,但不仅仅 “协议” 本身,而是一个 “面向服务的通信框架”。专为微服务设计。 层级 技术 传输层 TCP 应用层协议 HTTP/2 序列化格式 Protocol Buffers RPC 框架 gRPC(封装了上述三层) 而 Protobuf 本身还可以作为许多其他系统(如消息队列、API 网关、配置管理等)的基础数据定义格式,基础用法如下: 1231. 用 .proto 定义接口以后,2. 使用 .protoc 配合对应框架的插件直接生成代码3. 将代码代码添加进项目中,在项目中直接使用生成的类。 步骤 1:编写 .proto 文件(定义服务契约) 首先序列化生成数据结构(.proto文件不依赖任何特定编程语言),通常写作: 123456789syntax = "proto3";package example;service Calculator { rpc Add(AddRequest) returns (AddResponse);}message AddRequest { int32 a = 1; int32 b = 2; }message AddResponse { int32 result = 1; } 步骤 2:用 protoc 生成代码 1protoc --cpp_out=. calculator.proto -> calculator.pb.h, calculator.pb.cc 再利用插件生成 grpc 服务框架(插件保证了 protobuf 可以为不同用途生成不同代码) 12protoc --grpc_out=. --plugin=protoc-gen-grpc=grpc_cpp_plugin.exe calculator.proto -> calculator.grpc.pb.h, calculator.grpc.pb.cc 输出文件: calculator.pb.h/cc → 数据结构 calculator.grpc.pb.h/cc →服务接口(Stub + Service) 步骤 3 :在项目中使用生成代码: 客户端:通过 Stub 发起远程调用; 服务端:继承 Service 类并实现具体方法;","categories":[{"name":"第三方库","slug":"第三方库","permalink":"https://dxblacksmith.github.io/categories/%E7%AC%AC%E4%B8%89%E6%96%B9%E5%BA%93/"}],"tags":[]},{"title":"shared_from_this","slug":"shared_from_this","date":"2026-01-30T08:58:58.082Z","updated":"2026-01-30T08:58:58.367Z","comments":true,"path":"2026/01/30/shared_from_this/","permalink":"https://dxblacksmith.github.io/2026/01/30/shared_from_this/","excerpt":"","text":"Shared_from_this1. 异步模型1.1 用法主要用于防止对象过早析构 在 Asio 的异步模型中,调用 async_read 或者 async_write 这种异步操作时,函数会立即返回。当 IO 完成时,Asio 再调用绑定的回调。同时回调可能需要访问 this 的一些成员,但是如果没有任何东西持有 CHttpConnection 的引用,它可能在 I/O 完成前就被析构了! 所以为了解决这个问题,需要在回调中捕获 shared_from_this 12345678void CHttpConnection::ReadRequest() { auto self = shared_from_this(); http::async_read(m_socket_, m_buffer_, m_request_, [self](beast::error_code ec, std::size_t bytes_transferred) { // 即使外部不再持有 self,这个 lambda 也会 keep-alive 整个连接对象 self->HandleRequest(); // 安全调用成员函数 });} 1.2 原理shared_from_this() 是 std::enable_shared_from_this<T> 提供的一个成员函数。调用 shared_from_this() 返回的 shared_ptr 与原始 shared_ptr 共享同一块控制块(control block),因此: 引用计数 +1; 只要还有任何一个 shared_ptr(包括 lambda 捕获的那个 self)未被销毁,对象就不会析构; 为什么不直接写 auto self = std::shared_ptr(this);。因为会造成双重析构的风险: 1auto bad_self = std::shared_ptr<CHttpConnection>(this); // 危险! 这会创建一个全新的、独立的 shared_ptr,它拥有自己的引用计数器。但 this 已经被另一个 shared_ptr(比如 make_shared 创建的那个)管理了。 → 当这两个 shared_ptr 各自析构时,都会尝试 delete this,导致 重复释放同一块内存,引发 崩溃或未定义行为。","categories":[{"name":"C++ 新特性","slug":"C-新特性","permalink":"https://dxblacksmith.github.io/categories/C-%E6%96%B0%E7%89%B9%E6%80%A7/"}],"tags":[]},{"title":"Boost.Asio 的使用","slug":"Boost.Asio","date":"2026-01-29T12:43:37.528Z","updated":"2026-01-29T12:43:37.842Z","comments":true,"path":"2026/01/29/Boost.Asio/","permalink":"https://dxblacksmith.github.io/2026/01/29/Boost.Asio/","excerpt":"","text":"Boost.Asio 库io_context1. 概念io_context 是 Asio 库的核心事件循环和 IO 调度器,也是所有异步 I/O 事件处理的基础。工作逻辑为 将需要监听的 IO 事件(如 socket 的读/写就绪、连接请求就绪)和对应的处理回调注册到 io_context 的事件管理体系中 启用 io_context 的事件循环以后,会持续通过底层多路复用的机制检测已注册的事件状态…… 当检测到某个 IO 事件就绪以后,会自动调用执行该事件对应的上层回调函数,完成 IO 事件的处理 2. 和 epoll/iocp 的关系io_context 是跨平台抽象层,epoll 只是其在 Linux 下的一种底层实现,类似 1【上层:开发者代码】→ 【中间层:io_context(事件调度)】→ 【底层:操作系统IO多路复用(epoll/IOCP)】 用代码实现的话 注册:将代码中 tcp::acceptor(监听套接字)、tcp::socket(通信套接字)和各种事件(端口监听,socket 读/写)等事件注册到 io_context,同时绑定自定义回调; 传递:io_context 接收到注册请求后,会将对应的 socket 句柄、事件类型(读 / 写 / 连接)传递给底层的 epoll,完成 epoll 的事件注册; 启动:调用 io_context.run() 启动事件循环,io_context 会调用 epoll 的 epoll_wait 方法,阻塞等待内核的事件就绪通知 响应处理:有事件就绪,内核通过 epoll 向 io_context 发送就绪通知,然后调度执行开发者注册的上层回调函数 3. 核心特点跨平台性:若你的服务端移植到 Windows,io_context 会自动切换为基于 IOCP 实现,你的上层代码无需任何修改,这是 io_context 最核心的价值(屏蔽了不同系统的底层 IO 差异); 不止封装 epoll:io_context 除了封装 IO 多路复用,还实现了事件调度、回调管理、线程池适配等功能,而 epoll 仅负责内核态的 IO 事件检测,无上层调度能力;","categories":[{"name":"第三方库","slug":"第三方库","permalink":"https://dxblacksmith.github.io/categories/%E7%AC%AC%E4%B8%89%E6%96%B9%E5%BA%93/"}],"tags":[]},{"title":"Json 序列化","slug":"Json 序列化","date":"2026-01-28T14:07:01.444Z","updated":"2026-01-30T09:17:21.042Z","comments":true,"path":"2026/01/28/Json 序列化/","permalink":"https://dxblacksmith.github.io/2026/01/28/Json%20%E5%BA%8F%E5%88%97%E5%8C%96/","excerpt":"","text":"Qt序列化:内存中的结构化数据 -> 可存储/网络传输的字符串/字节流(Json 格式或者其它格式) 反序列化:文件/网络中读取的 Json 字符串/字节流 -> 还原成内存中的结构化数据 Qt 内置了 Json 的处理,核心流程可以概括为: 1内存对象(QJsonObject)⇌ Json 文档(QJsonDocument)⇌ 字节数组(QByteArray)⇌ 文件/网络 Json 序列化:123456781. QJsonObject: 通过 insert 修改 QJsonObject 里面的键值对 m_config.insert("user", "test"); m_config.insert("auto", true);2. QJSonDocument: 直接作为参数构造 QJsonDocument doc(m_config)3. QByteArray: 通过 toJson 将文档转换为 JSON 格式的字符串 QByteArray jsonData = doc.toJson();4. 文件或者网络处理 Json 反序列化:123456781. 文件或者网络处理: 文件的话通过 readAll 来提取字符串 QByteArray data = file.readAll();2. QJsonDocument: 通过 fromJson 提取文档内容 QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(data, &error);3. QJsonObject: 通过 object() 直接提取 QJsonObject m_config = doc.object();4. 提取完毕 JsoncppJsoncpp 是第三方库,给他编译配置项目中即可,主要介绍一下使用的方法 首先 Jsoncpp 里面比较重要的是 Json::Value 和 Json::Reader Json 序列化主要利用 Json::value.toStyleString 接口 123jsonSend["email"] = email;jsonSend["error"] = ErrorCodes::SUCCESS;std::string jsonstr = jsonSend.toStyledString(); Json 反序列化主要是利用 Json::Reader.parse 接口 12345std::string body_str = beast::buffers_to_string(m_request.body().data());Json::Value jsonRecv;Json::Reader reader;// 反序列化bool success = reader.parse(body_str, jsonRecv);","categories":[{"name":"第三方库","slug":"第三方库","permalink":"https://dxblacksmith.github.io/categories/%E7%AC%AC%E4%B8%89%E6%96%B9%E5%BA%93/"}],"tags":[]},{"title":"网络通信协议","slug":"网络通信协议","date":"2026-01-28T11:10:44.171Z","updated":"2026-01-28T13:56:31.603Z","comments":true,"path":"2026/01/28/网络通信协议/","permalink":"https://dxblacksmith.github.io/2026/01/28/%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1%E5%8D%8F%E8%AE%AE/","excerpt":"","text":"网络通信协议Tcp 协议TCP 是传输层的可靠字节流通信协议,为 HTTP/HTTPS/WebSocket 等应用层协议提供底层传输支撑,核心解决「数据如何有序、无丢失、无重复地在两台设备间传输」的问题。 其连接的持续时间完全由上层应用(开发者)决定 核心特点 面向连接:通信前需通过「三次握手」建立连接,通信结束后通过「四次挥手」关闭连接; 可靠传输:通过序列号、确认应答(ACK)、重传机制保证数据不丢失、不重复、有序到达; 字节流传输:无固定应用层格式,仅传输连续字节流,需上层协议自定义数据边界; 拥塞控制:采用慢启动、拥塞避免、快重传、快恢复来防止网络拥塞 数据传输格式 TCP 本身无固定报文格式,需开发者自定义「消息头 + 消息体」区分数据边界(解决字节流粘包 / 拆包问题): 123// 自定义TCP报文结构(通用设计)1. 消息头(固定长度):4字节(消息总长度) + 2字节(消息类型) + 1字节(版本号)2. 消息体(可变长度):实际传输的业务数据(长度由消息头指定) 使用场景 需自定义协议的高性能场景(如游戏服务器、物联网设备) 作为 HTTP/HTTPS/WebSocket 的底层传输基础; Http 协议Http 是客户端和服务器之间通信的协议。一个完整的 Http 请求包含: 1231. 请求行:方法 + URL + 协议版本2. 请求头:元数据3. 请求体:实际传输的数据(GET 请求通常没有) 应用层协议,Http 1.0 默认是短连接,1.1 就变成了默认的长连接(核心改进) Http 请求如下所示,一般来说可以用 url 来构造 http 请求(Content-Type/Content-Length 这种没在 url 里面的就手动设置): 123456789101112131415161718<!-- 请求行 -->POST /api/orders HTTP/1.1 <!-- 请求头 --> Host: api.shop.comContent-Type: application/jsonAuthorization: Bearer token123User-Agent: MyApp/1.0Content-Length: 87<!-- 请求体 Json 格式 --> { "productId": "12345", "quantity": 2, "customer": { "name": "张三", "address": "北京市" }} Http 响应: 123456789101112<!-- 响应行 -->HTTP/1.1 201 Created<!-- 响应头 -->Content-Type: application/jsonDate: Mon, 23 Oct 2023 10:00:00 GMT<!-- 响应头 -->{ "id": "order_67890", "status": "created", "createdAt": "2023-10-23T10:00:00Z"} 请求类型 Get- 获取数据 12GET /api/users/123 HTTP/1.1Host: example.com 用于获取服务器的资源 Post - 创建数据 12345POST /api/users HTTP/1.1Host: example.comContent-Type: application/json{"name": "张三", "email": "zhang@example.com"} 用于在服务器当中创建新资源 PUT:更新完整资源 Delete: 删除资源 Https 协议HTTPS 是「HTTP + TLS/SSL 加密」的组合协议,本质是加密版的 HTTP,基于 TCP 传输,核心解决 HTTP 明文传输的安全问题(防窃听、防篡改、防冒充)。 应用层协议,Http 1.0 默认是短连接,1.1 就变成了默认的长连接(核心改进) 核心特性 加密传输:通过 TLS/SSL 对 HTTP 数据加密,传输过程中无法被破解; 身份验证:通过数字证书验证服务器合法性,防止访问钓鱼网站; 兼容 HTTP:请求 / 响应格式与 HTTP 一致,仅增加 TLS 加密层; 基于 TCP:先完成 TCP 三次握手,再完成 TLS 握手,最后传输加密数据; 默认端口:443(HTTP 默认 80)。 TLS 握手核心流程 客户端发送:TLS 版本、加密套件列表、随机数; 服务器返回:数字证书、选定的加密套件、随机数; 客户端验证证书,生成「预主密钥」并加密发送给服务器; 双方基于随机数 + 预主密钥生成「会话密钥」; 确认加密通道建立,后续 HTTP 数据均用会话密钥加密。 Https 请求/响应示例 HTTPS 报文结构与 HTTP 一致,仅传输过程为密文,以下是「明文视角」的示例: 请求: 12345678910111213<!-- 请求行 -->POST /api/pay HTTP/1.1<!-- 请求头(明文传输,用于TLS握手和基础标识) -->Host: pay.example.comContent-Type: application/jsonContent-Length: 78User-Agent: MyApp/1.0<!-- 请求体(传输时为密文,明文如下) -->{ "orderId": "order_67890", "amount": 99.00, "payType": "wechat"} 响应: 12345678910HTTP/1.1 200 OKContent-Type: application/jsonDate: Mon, 23 Oct 2023 10:05:00 GMTContent-Length: 65<!-- 响应体(传输时为密文,明文如下) -->{ "payId": "pay_123456", "status": "success", "payTime": "2023-10-23T10:05:00Z"} 数据加密避免请求 / 响应被中间人窃听 WebSocketWebSocket 是应用层的双向实时通信协议,基于 TCP 构建,通过 HTTP 握手升级为持久化长连接,核心解决 HTTP「客户端主动请求、服务端被动响应」的单向通信限制。 应用层协议,默认且唯一的使用方式是长连接 核心特性 双向通信:连接建立后,客户端 / 服务器可主动、实时发送数据(全双工); 持久化长连接:一次握手后连接保持,直到主动关闭(无需重复建立 TCP 连接); 轻量级:数据帧格式简洁,无 HTTP 冗余头信息,传输开销远低于 HTTP; 基于 HTTP 握手:首次连接兼容现有 HTTP 服务器,便于穿透防火墙; 默认端口:80(WebSocket)/443(WSS,加密版 WebSocket)。 通信流程 客户端发送 HTTP 升级请求(握手); 服务器响应升级确认,连接转为 WebSocket; 双方通过 WebSocket 帧双向传输数据; 任意一方发送关闭帧,结束连接。 报文格式示例 WebSocket 握手请求(客户端 -> 服务器): 1234567GET /ws/chat HTTP/1.1Host: chat.example.comUpgrade: websocket <!-- 核心:请求升级为WebSocket -->Connection: Upgrade <!-- 确认连接升级 -->Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== <!-- 随机密钥,用于验证 -->Sec-WebSocket-Version: 13 <!-- 固定版本 -->User-Agent: MyChatApp/1.0 WebSocket 握手响应 1234HTTP/1.1 101 Switching Protocols <!-- 101状态码表示协议切换 -->Upgrade: websocketConnection: UpgradeSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= <!-- 密钥验证结果 --> WebSocket 数据帧 12345// 客户端发送文本帧{"type":"chat","content":"你好,服务器!","userId":123}// 服务器推送文本帧{"type":"broadcast","content":"用户123发送了新消息","time":"2023-10-23 10:10:00"} 使用场景 实时聊天(网页版微信、直播弹幕) 实时数据监控(股票行情、物联网设备数据推送)","categories":[{"name":"计算机网络","slug":"计算机网络","permalink":"https://dxblacksmith.github.io/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"}],"tags":[]},{"title":"银行科技岗一览","slug":"银行科技岗一览","date":"2026-01-21T13:56:44.643Z","updated":"2026-01-21T13:56:44.879Z","comments":true,"path":"2026/01/21/银行科技岗一览/","permalink":"https://dxblacksmith.github.io/2026/01/21/%E9%93%B6%E8%A1%8C%E7%A7%91%E6%8A%80%E5%B2%97%E4%B8%80%E8%A7%88/","excerpt":"","text":"银行科技岗工作类型 金融科技 工作内容不涉及技术相关工作,主要写材料,项目管理(算是大甲方) 岗位集中在总行,省行有少数岗位 软件开发 典型的开发(前后端,测试等产品开发工作) 岗位于省行(比较少),软件中心/研发中心,金科(外包) ,都有开放 数据中心运维 工作内容和名字相同,就是做系统维护的工作 岗位于省行,数据中心,市分科技岗都有开放 Ps. 金融科技 > 运维 > 测试 > 开发 工作部门 T0 级 六大国有行和三大政策行总行,六大国有行就是中国银行、中国工商银行、中国建设银行、交通银行、中国邮政储蓄银行、中国农业银行。 三大政策行就是国家开放银行、中国进出口银行、中国农业发展银行。总行都在北京,毋庸置疑好, T0 级别 T0.5 级 二线城市地位高,待遇好,人民银行和公务员都差不多了。T0.5 级别 头部城商行(成都银行)、股份制银行(招商银行)的总行(在地方来说也算是待遇非常好的了) T1 级 农邮,政策行的直属中心(数据中心、软开中心、研发中心) 工农建的省行科技岗、管培岗,没有直属中心待遇那么好,但是也没那么卷 T2 级 中交直属中心 中交邮省行科技岗(真科技) 招商分行,地方城商行总行,农商行分行 T3 级 金科公司,招银网络、民生科技等银行子公司 地方城商分行,农商行分行等等 市分行后台,营销等(柜员等等) 备考建议graph LR 网申 --> 笔试 --> 机试 --> 无领导讨论 --> 半结构面试 网申:注意提前批,根据学历例行投递 笔试:提前在 App 上刷题,2 h 能做完就行 机试:直属中心一般都要机试,hot100 难度(可能没有) 无领导讨论:准备一下,政策行、管培、省行都有这个环节(可能没有) 半结构面试:根据个人履历进行提问","categories":[{"name":"计算机求职","slug":"计算机求职","permalink":"https://dxblacksmith.github.io/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E6%B1%82%E8%81%8C/"}],"tags":[]},{"title":"烤面筋 Day 04","slug":"烤面筋_Day04","date":"2026-01-16T08:08:00.930Z","updated":"2026-01-16T08:33:32.151Z","comments":true,"path":"2026/01/16/烤面筋_Day04/","permalink":"https://dxblacksmith.github.io/2026/01/16/%E7%83%A4%E9%9D%A2%E7%AD%8B_Day04/","excerpt":"","text":"烤面筋 Day 04单例模式1. 单例模式是什么?有哪些单例模式?答:单例模式是一种设计模式,旨在确保一个类在整个应用程序的生命周期当中只有一个实例,提供一个全局访问点来获取该实例。 有一般的单例模式(用 static 修饰成员函数和 static 的局部变量),饿汉式单例模式,懒汉式单例模式,还有就是个人比较常用的 CRTP (声明单例的通用模板类) 2. 分别介绍一下这几种单例模式? 一般单例模式:静态成员函数 + 静态局部变量 1234567891011121314class Singleton {public: static Singleton& GetInstance() { // 静态局部变量只会被初始化一次 static Singleton instance; return instance; } ~Singleton() = default;private: Singleton() = default; Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete;}; 饿汉式单例模式:静态成员指针变量 + cpp 文件定义 1234567891011121314class Singleton {public: ~Singleton() = default; static Singleton* GetInstance() { if(instance == nullptr) instance = new Singleton(); return instance; }private: static Singleton* instance; Singleton() = default; Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete;}; 同时在 cpp 文件中定义 instance 实例 1Singleton* Singleton::instance = Singleton::GetInstance(); 懒汉式单例模式: 智能指针 + once_flag 12345678910111213141516171819202122#include <mutex>#include <memory>class Singleton {public: ~Singleton() = default; static std::shared_ptr<Singleton> GetInstance() { static std::once_flag flag; // flag 底层采用了原子性的原理,紧接的线程如果发现了 flag 已经被初始化, // 就不会执行 call_once 后面的可调用对象 std::call_once(flag, [](){ // 这里不能使用 std::make_shared 因为构造函数是私有的,外部作用域无法访问 instance = std::shared_ptr<Singleton>(new Singleton()); }); return instance; }private: static std::shared_ptr<Singleton> instance; Singleton() = default; Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete;}; 3. CRTP 是什么?能不能实现一下?答:是一种将派生类作为模板参数传递给基类的技术,即一个类继承以自身为模板参数的基类。 基类代码: 123456789101112131415161718192021222324#include <memory>#include <mutex>template <typename T>class Singleton {protected: Singleton() = default; Singleton(const Singleton<T>& other) = delete; Singleton& operator=(const Singleton<T>& other) = delete; static std::shared_ptr<T> _instance;public: ~Singleton() = default; static std::shared_ptr<T> GetInstance() { static std::once_flag flag; std::call_once(flag, [&](){ _instance = std::shared_ptr<T>(new T); }); return _instance; }};// 注意模板类的 static 一定要在头文件中定义template <typename T>std::shared_ptr<T> Singleton<T>::_instance = nullptr; 派生类代码: 1234567class SingleNet : public Singleton<SingleNet> {private: SingleNet() = default; friend class Singleton<SingleNet>;public: ~SingleNet() = default;}; 观察者模式1. 什么是观察者模式?答:观察者模式 是一种行为设计模式。它定义了对象间的一种 一对多 的依赖关系,使得每当一个对象状态发生改变时,所有依赖于它的对象都会得到通知并被自动更新。 2. 你觉得里面的重点是什么? 解耦:被观察者(Subject)不需要知道观察者(Observer)的具体类,只需要知道它们实现了某个接口。 触发联动:状态改变自动触发行为,不需要轮询检查。 抽象依赖:Subject 依赖于 Observer 的抽象基类,符合开闭原则(对扩展开放,对修改关闭)。 3. 实现一下观察者模式吧123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566#include <iostream>#include <vector>#include <memory>#include <mutex>#include <algorithm>#include <string>// 1. 抽象观察者class Observer : public std::enable_shared_from_this<Observer> {public: virtual ~Observer() = default; virtual void update(const std::string& state) = 0;};// 2. 被观察者(Subject)class Subject {public: // 线程安全的订阅:使用 weak_ptr 记录观察者 void attach(std::weak_ptr<Observer> observer) { std::lock_guard<std::mutex> lock(_mtx); _observers.push_back(observer); } // 状态更新并通知 void setState(std::string state) { std::vector<std::shared_ptr<Observer>> active_observers; { std::lock_guard<std::mutex> lock(_mtx); _state = state; // 遍历并检查生命周期 auto it = _observers.begin(); while (it != _observers.end()) { // 尝试提升为 shared_ptr if (auto obj = it->lock()) { active_observers.push_back(obj); ++it; } else { // 观察者已销毁,自动清理从列表中剔除 it = _observers.erase(it); } } } // 在锁外进行通知,避免死锁风险及长时间占用锁 for (const auto& obs : active_observers) { obs->update(_state); } }private: std::string _state; std::vector<std::weak_ptr<Observer>> _observers; // 核心:弱引用 std::mutex _mtx; // 核心:互斥锁保证线程安全};// 3. 具体观察者示例class Worker : public Observer {public: Worker(std::string name) : _name(name) {} void update(const std::string& state) override { std::cout << "Worker " << _name << " received: " << state << std::endl; }private: std::string _name;}; Inline 内联函数1. 内联函数的实现机制?答:内联函数是向编译器发出的一个建议,请求将函数调用替换为函数体本身。 目的是消除函数调用的开销(如压栈、跳转、返回等),对于频繁调用的小函数性能提升明显。 2. 编译器一定会内联吗?答:不一定会内联。编译器会根据复杂的启发式算法自行决定。 如果函数过大或者过于复杂或者函数是虚函数(内联是编译时确定,虚函数是运行时确定的)的情况下,编译器会拒绝内联。同时现代的编译器非常聪明,即使没有写 inline 关键字,只要它认为有益且符合条件,也会自动进行优化内联","categories":[{"name":"C++ 面经","slug":"C-面经","permalink":"https://dxblacksmith.github.io/categories/C-%E9%9D%A2%E7%BB%8F/"}],"tags":[]},{"title":"烤面筋 Day 03","slug":"烤面筋_Day03","date":"2026-01-15T16:08:37.654Z","updated":"2026-01-18T09:20:10.912Z","comments":true,"path":"2026/01/16/烤面筋_Day03/","permalink":"https://dxblacksmith.github.io/2026/01/16/%E7%83%A4%E9%9D%A2%E7%AD%8B_Day03/","excerpt":"","text":"烤面筋 Day 03多态与虚函数表:1. 多态的定义是什么?分类有哪些?答:多态是允许基类的指针或引用在运行时根据实际指向的子类对象类型,调用相应子类重写方法的能力。主要分为两类。 静态多态(编译时多态):通过函数重载和模板实现。编译器在编译期间就确定了调用的函数版本。 动态多态(运行时多态):通过虚函数(Virtual Function)和继承来时实现。程序在运行期间根据对象的实际类型来决定调用哪个函数。 2. 虚函数是什么?虚函数表和虚函数指针有什么用?答:虚函数是允许在派生类中被重写、并通过基类指针或引用实现运行时多态的函数。是实现动态多态的基石 虚函数表: 每个拥有虚函数的类都有一个虚表。它本质上是一个存放虚函数地址的数组。 虚函数指针: 每个具体的对象实例中都有一个隐藏指针,指向该类对应的虚函数表。 当通过基类指针调用虚函数时,程序先通过对象的虚指针找到虚函数表,再根据偏移量找到对应的函数地址并执行,从而实现动态绑定。即通过虚指针在运行时查找正确的函数实现。 3. 继承关系中的虚函数表是 “拷贝” 还是 “共享”?答:派生类会继承基类的虚函数表结构,但会生成一份属于自己的新表。 继承与拷贝: 派生类创建时,会首先拷贝基类的虚表内容到自己的新虚表中。 重写(Override): 如果派生类重写了某个虚函数,编译器会用派生类自己的函数地址,**覆盖(Overwrite)**掉新虚表中原来基类的函数地址。 新增: 如果派生类定义了新的虚函数,这些地址会按顺序添加到新虚表的末尾。 结论: 基类和派生类拥有各自独立的虚函数表,互不干扰。 4. 下面的 B 对象占多大的内存空间?1234567891011121314151617181920class A { char a; virtual void add1();}class C { char aa; virtual void add2();}class B : public A, public C { short a2; int a; short b; int c; char d; int sum(); virtual void add(); void add1(); void add2();} 答:首先两个基础,创建一个 B 对象,这个对象的内存分布是怎么样的。构造一个派生类对象,它的内存布局里面肯定存在基类的子对象(以及其虚指针),如下图所示:  图中的 vptr分别指向自己类的虚函数表,通常一个类的虚指针放在内存布局的最前面,所以这里 B 的虚指针一般会把 A 的虚指针给覆盖掉。 12构造一个子类对象:首先会先去构建基类的对象内容,比如这里的 A 和 C,然后再构造自己的内容。析构一个子类对象:和构造相反,先执行自己的析构函数,然后去执行基类的析构函数。 Question: 那这里有个问题,如果 A 的析构函数是虚函数的话,之前覆盖了 A 的虚指针,这里就找不到 A 的虚函数表,那么如何析构 A 的内容呢? 答案就是编译器会在调用 B 的虚析构函数的时候,就把头部的虚指针重置为 A 的了,所以能够正常执行。 所以这里我们就明白了解决上面问题最重要的一个点,那就是 B 的内存空间一定会继承基类的虚指针以及基类的一些成员变量。 那么解决这个问题还需要明白一个知识点,那就是内存对齐。内存对齐的规则是什么呢? 类的每个基准内存块大小 = 类当中占用内存大小最大的成员变量的内存空间,比如这里的 A,虽然最大的成员变量是 1 字节,但是它存在虚函数,所以内部有虚指针。又由于虚指针是 8 字节,所以基准内存块大小就是 8 字节,所以这里 A 所占用是 8 + 8 = 16 字节 每个类型的起始地址都是该类型大小的整数倍,比如 int 的成员变量,那么它的起始地址只有可能是 0, 4, 8, …. 明白了上面的两个点以后,现在我们来看这个问题: 首先由于 A 和 C 中存在虚函数(虚指针)的缘故,他们的大小都是 16 字节。然后我们看到 B 中自己的成员变量,首先 short 一定是在 4 字节的,因为 short 本身是占 2 字节的,但是由于对齐规则 2 ,后续跟进的 int 的起始地址只能是 4 的倍数,所以 2 - 4 的空间也是只能给 short ,这样的话就是 16 + 16 + 16,最后还有一个 char 类型,不过因为对齐规则 1,这里只能是也占用 8 个字节,所有总共占用的字节数是 16 + 16 + 16 + 8 = 56 个字节。 Http 协议1. Http 请求的核心部分有哪些?一个标准的 HTTP 请求报文由四个部分组成: **请求行:**包含 Method、URL、协议版本 **请求头:**键值对,描述客户端环境、压缩格式,持久连接等(Host, User-Agent, Connection) 空行: \\r\\n,用于分隔 Header 和 Body,这是协议格式的强制要求 **请求体:**可选,通常用于 POST/PUT 提交的数据 2. Http1.1 常用 Method 及 GET/POST 区别?常用 Method:GET, POST,PUT,DELETE,HEAD GET 和 POST 的区别: **语义:**GET 倾向于获取资源,是幂等的;POST 倾向于处理资源(创建/修改),是非幂等的。 **参数位置:**GET 参数放在 URL 后面,POST 参数通常放在 Request Body 中。 **数据大小:**GET 受限于 URL 长度(浏览器/服务器限制);POST 理论上无限制。 3. 为什么 GET 请求一般没有请求体?有下面三点原因: **语义冲突:**GET 的定义是根据 URL 及其参数获取资源,引入 Body 会破坏这种简洁的映射。 **缓存兼容性:**CDN、代理服务器和浏览器通常只根据 URL 缓存 GET 请求。如果 Body 影响结果,这些缓存机制将失效。 **服务器实现:**许多 Web 服务器和解析器为了性能优化,会直接忽略 GET 请求中的 Body。 4. HTTP/1.1、HTTP/2、HTTP/3 的主要区别是什么? 特性 HTTP/1.1 HTTP/2 HTTP/3 传输格式 文本(明文) 二进制分帧 二进制分帧 多路复用 无(有线头阻塞) 支持(单一连接并发) 支持 底层协议 TCP TCP UDP (QUIC) 头部压缩 无 HPACK QPACK 连接建立 TCP 握手 + TLS 握手 同 1.1 QUIC 握手(1-RTT/0-RTT) 5. HTTP 有哪些常见的状态码?首先是分类:Http 状态共 5 大类,首位数字界定类别:1xx(信息)、2xx(成功)、3xx(重定向)、4xx(客户端错误)、5xx(服务端错误) 其次列举一些高频的状态码: 200(成功)、404(资源不存在)、500(服务器错误)、304(缓存)、401/403(权限)、429(限流)","categories":[{"name":"C++ 面经","slug":"C-面经","permalink":"https://dxblacksmith.github.io/categories/C-%E9%9D%A2%E7%BB%8F/"}],"tags":[]},{"title":"烤面筋 Day 02","slug":"烤面筋_Day02","date":"2026-01-14T07:13:34.486Z","updated":"2026-02-02T06:10:29.246Z","comments":true,"path":"2026/01/14/烤面筋_Day02/","permalink":"https://dxblacksmith.github.io/2026/01/14/%E7%83%A4%E9%9D%A2%E7%AD%8B_Day02/","excerpt":"","text":"烤面筋 Day 02 类型转换1. 向上转型 vs 向下转型?这是多态中两个方向完全相反的操作: 向上转型: 定义:将子类的指针或引用转换成父类 安全性:绝对安全。因为子类也是一种父类 用法:这种转换是隐式,不需要显式调用 static_cast ,是实现多态的基础 向下转型: 定义:将父类的指针或引用转换为子类。 安全性: 不安全。父类对象不一定是子类。 用法:必须使用显式转换,出于安全保证使用 dynamic_cast 2. static_cast 与 dynamic_cast 的区别?本质区别在于 “转换发生的时机” 和 “安全检查的机制” static_cast: 在编译阶段完成,不进行运行时类型检查 主要用于基本类型转换(如 int 转 float),非多态层级结构内的指针/引用转换。 dynamic_cast: 在运行阶段利用 RTTI(运行时类型信息) 检查转换是否合法 专门用于处理**多态(Polymorphism)**层级结构中的转换 3. static_cast 在什么场景有风险?static_cast 的风险主要发生在 向下转型(Downcasting),即把基类指针转换为派生类指针时: 风险点: 如果该基类指针实际上并没有指向那个派生类对象,static_cast 依然会强行转换成功,返回一个地址。 后果: 当你通过这个转换后的指针访问派生类特有的成员变量或虚函数时,会发生未定义行为(Undefined Behavior),通常表现为内存越界访问或程序崩溃,且这种错误在编译期无法察觉。 4. dynamic_cast 的优势是什么?它的核心优势是 “安全性”: 类型安全检查: 它会检查目标类型是否与对象的实际类型匹配。如果转换非法,对于指针会返回 nullptr,对于引用会抛出 std::bad_cast 异常。 支持虚继承转换: 在复杂的深层或菱形继承中,dynamic_cast 能够正确处理指针偏移。 Reactor + 线程池1. Reactor 模式是怎么实现的?Reactor 模式本质上是 “I/O 复用 + 派发”,其核心组件包括: Event Demultiplexer:底层通常是 epoll或者 poll ,监听注册了的一堆文件描述符有哪些动静 Reactor:核心循环。通过多路分离器等待事件发生,一旦有事件(如可读、可写),就将其分发(Dispatch)给对应的 Handler。 Handlers:绑定在事件上的回调函数,负责非阻塞的读写操作。 2. 为什么采用主从 Reactor + 线程池?主要是为了解决单线程 Reactor 的性能瓶颈: 分工明确:Main Reactor 只负责监听连接(Accept),Sub Reactor 负责处理已连接套接字的 I/O 事件。这避免了因为某个请求的 I/O 耗时过长导致新连接无法进入。 充分利用多核: 多个 Sub Reactor 可以运行在不同的 CPU 核心上,并行处理 I/O。 解耦计算:线程池 将业务计算逻辑从 I/O 线程中剥离。如果业务逻辑耗时较长,它不会阻塞 I/O 事件的分发,从而极大提高了系统的吞吐量。 accept() 仅需内核拷贝 socket 描述符(轻量高频操作),单线程 BossGroup 足以应对万级连接请求。如果是多线程竞争锁,反而会降低效率。IO 事件是重量级操作,涉及数据读写,业务逻辑,可能阻塞(数据库查询)。WorkGroup 多线程并行处理,充分利用多核 CPU。 Ps. 这里的 I/O 线程处理的是 I/O 读写,指的是从网卡驱动缓存拷贝到用户缓存区,或者将用户态数据拷贝到内核发送缓存区。业务计算逻辑则是加工数据(数据读上来以后,程序需要处理它),例如协议解析(二进制流解包成 Protobuf 或 JSON),数据库查询(根据请求差 SQL)等等 3. 从 Reactor 读到的数据如何传递给线程池?能不能实现一下?是通过任务队列来实现的。Sub Reactor 将 (数据,处理函数)封装成任务丢进队列,线程池里的 worker 线程通过竞争来获取任务。 线程安全的任务队列通常有使用条件变量的阻塞队列和无锁队列。 阻塞队列: 1234567891011121314151617181920212223242526272829303132333435363738#include <mutex>#include <condition_variable>#include <queue>template <typename T>class BlockQueue {public: bool push(const T& val) { std::lock_guard lock(mtx_); if (stop_) return false; // 队列已停止,返回false que_.push(val); cv_.notify_one(); return true; } bool pop(T& val) { std::unique_lock lock(mtx_); cv_.wait(lock, [this](){ return stop_ || !que_.empty(); }); if(stop_ && que_.empty()) return false; val = que_.front(); que_.pop(); return true; } void stop() { { std::lock_guard lock(mtx_); stop_ = true; } cv_.notify_all(); }private: std::condition_variable cv_; std::queue<T> que_; std::mutex mtx_; bool stop_ = false;}; 无锁队列: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970#include <atomic>#include <cstdio>/** push: 先检查尾部的一致性, tail->next = new_node, tail = new_node(用 CAS 版本实现)* pop:先检查头部的一致性,如果队列为空 tail = tail->next, 不为空 head = head->next*/template <typename T>class LockFreeQueue {public: LockFreeQueue(); ~LockFreeQueue(); void push(const T& value) { Node* newNode = new Node(value); while(true) { Node* current_tail = tail.load(); Node* next = current_tail->next.load(); // 一致性检查 if(tail.load() == current_tail) { // tail 是真正的尾部 if(next == nullptr) { // 一致性检查 if(current_tail->next.compare_exchange_weak(next, newNode)) { // 进行更新 tail.compare_exchange_weak(current_tail, newNode); return; } } else { tail.compare_exchange_weak(current_tail, next); } } } } bool pop(T& result) { while(true) { Node* current_head = head.load(); Node* current_tail = tail.load(); Node* next = current_head->next.load(); if(current_head == head.load()) { // 队列有可能为空 if(current_head == current_tail) { if(next == nullptr) return false; // 进行更新 tail = tail->next tail.compare_exchange_weak(current_tail, next); } else { result = next->data; if(head.compare_exchange_weak(current_head, next)) { delete current_head; return true; } } } } }private: struct Node { T data; std::atomic<Node*> next; Node(const T& val) : data(val), next(nullptr) {} }; std::atomic<Node*> head; std::atomic<Node*> tail;}; 4. CPU 密集型和 I/O 密集型怎么判断?主要是看瓶颈在哪里:$N_{cpu}$ 是 CPU 核心数的意思 CPU 密集型: 特点:大部分时间在做复杂的运算(图像处理、科学计算) 判断: 程序运行时,CPU 占用率极高,但几乎没有磁盘或网络 I/O。 线程池策略: 线程数通常设置为 $N_{CPU} + 1$。 I/O 密集型: 特点: 大部分时间在等待磁盘读写、数据库查询、网络响应。 判断: CPU 占用率较低,系统大量时间处于等待状态(Wait)。 线程池策略: 线程数可以设置得大一些,如 $2N_{CPU}$ 甚至更多。 5. 线程池线程数如何确定?首先针对于 I/O 密集型任务来说,一般设置为内核数 * 2。 当一个线程因为 I/O 阻塞或等待时,CPU 可以切换到另一个线程。避免过多的上下文切换(Context Switch)带来的损耗。 不过在实际生产环境下,我会优先通过 压力测试 和 性能监控(如查看 top 中的 iowait 指标)来动态调整线程数,而不会死守 公式。 6. 手撕一下线程池?123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475#include <vector>#include <queue>#include <thread>#include <mutex>#include <condition_variable>#include <future>#include <functional>#include <atomic>class ThreadPool {private: std::vector<std::thread> workers; std::queue<std::function<void()>> tasks; std::mutex queue_mutex; std::condition_variable condition; std::atomic<bool> stop; public: ThreadPool(size_t num_threads = std::thread::hardware_concurrency()) : stop(false) { for(size_t i = 0; i < num_threads; ++i) { workers.emplace_back([this] { while(true) { std::function<void()> task; { std::unique_lock<std::mutex> lock(this->queue_mutex); this->condition.wait(lock, [this] { return this->stop || !this->tasks.empty(); }); if(this->stop && this->tasks.empty()) return; task = std::move(this->tasks.front()); this->tasks.pop(); } task(); } }); } } ~ThreadPool() { stop = true; condition.notify_all(); for(std::thread &worker : workers) { if(worker.joinable()) worker.join(); } } template<class F, class... Args> auto enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type> { using return_type = typename std::result_of<F(Args...)>::type; auto task = std::make_shared<std::packaged_task<return_type()>>( std::bind(std::forward<F>(f), std::forward<Args>(args)...) ); std::future<return_type> res = task->get_future(); { std::unique_lock<std::mutex> lock(queue_mutex); if(stop) throw std::runtime_error("enqueue on stopped ThreadPool"); tasks.emplace([task](){ (*task)(); }); } condition.notify_one(); return res; }};","categories":[{"name":"C++ 面经","slug":"C-面经","permalink":"https://dxblacksmith.github.io/categories/C-%E9%9D%A2%E7%BB%8F/"}],"tags":[]},{"title":"烤面筋 Day 01","slug":"烤面筋_Day01","date":"2026-01-13T10:14:19.695Z","updated":"2026-01-14T07:11:17.577Z","comments":true,"path":"2026/01/13/烤面筋_Day01/","permalink":"https://dxblacksmith.github.io/2026/01/13/%E7%83%A4%E9%9D%A2%E7%AD%8B_Day01/","excerpt":"","text":"烤面筋 Day 01 原子操作与内存模型: 1. 原子操作是什么?Atomic 跟原子操作的关系是什么?答:原子操作就是要么不执行,要么全部执行成功,且中途不能被中断的操作。Atomic 就是 C++ 11 提供的标准库模板类,是原子操作在语言层面的封装和工具。 2. Atomic 底层是怎么实现的?答:底层主要依赖硬件层面处理器提供的原子指令,在 x86 架构下,通常会使用带有 LOCK 前缀的指令,其能锁定缓存行,确保执行执行期间不会被中断。 编译器层面也会根据不同的 CPU 架构,将 atomic 操作映射为相应的机器指令,根据指定的内存序,插入必要的 Memory Barrier ,防止指令重排。 3. 能简单讲讲 C++ 的内存序(Memory Order)吗?内存序的作用是约束编译器和 CPU 的指令重排优化,C++11 定义了 6 种内存序,但常用的是 memory_order_relaxed 和 获取/释放语义和序列一致性。 宽松顺序:只保证当前操作的原子性,不保证任何顺序(没有同步关系)。 释放(release)语义:持有 release 的线程是发布者,,逻辑是 “做完手头的事情了,现在发布表示数据准备好了” 获取(acquire)语义:持有 acquire 的线程是获取者,逻辑是 “看到发布的通知了,说明我可以放心去用数据了” 序列一致性:atomic 的默认参数,要求所有线程看到的执行顺序都是一模一样的,就像所有操作都排在一个全局队列里依次执行。 那为什么不能全用默认的 seq_cst 序列一致性的内存序? 高性能场景下(如无锁队列、底层驱动),默认的强顺序会导致大量的内存屏障指令,强制刷新 CPU 缓存,这会极大限制现代多核处理器的并行能力。 4. CAS 指令是什么?答:CAS 是实现原子操作的核心指令,包含三个操作数: 1231. 内存地址 V:要读取的变量位置。2. 旧的预期值 A:我们认为该变量当前应该是什么值。3. 准备写入的新值 B:如果检查通过,要更新成的值。 只有内存地址的值还是我之前读到的那个预期值,我才把它修改成新值。否则说明别人改过它,那我就放弃这次修改。整个过程是原子性的 5.CAS 的优缺点是什么?答:优点就是无锁,避免了传统互斥锁导致的线程上下文切换、挂起和恢复,性能开销更小。 缺点是有自旋开销,如果在高竞争环境下,CAS 一直失败,会导致 CPU 一直空转,浪费资源。同时还存在 ABA 问题。 6. ABA 问题是什么?如何解决这个问题?就是一个线程将数值 A 改成了 B,随后另一个线程又将 B 改回了 A,此时第三个线程进行检查,发现值依然是 A,认为没有改变,从而误操作。 目前解决的主流办法是版本号,实现方法就是记录值的同时维护一个版本号,每次更新的时候,不仅更新值,还让版本号 +1。这样遇到 ABA 问题的时候版本号不同,就知道值已经发生改变了。 7. C++ atomic 是否自带版本号机制?不自带。标准的 std::atomic 只是对类型 T 的原子封装。如果 T 是一个指针,仍然会存在 ABA 问题。 如果需要解决这个问题的话,开发者通常需要配合 std::atomic<std::pair<T, int>> 或者一些带有 Tag 的指针技术。 8. Volatile 关键字的作用是什么?能解决线程安全吗?主要的作用是禁止编译器优化:告诉编译器,这个变量的值可能随时被外部(如硬件中断、另一个进程)修改,因此每次使用它都必须从内存中读取,而不能使用寄存器里的缓存。 不能,因为他不保证原子性,同时不禁止指令重排,只针对编译器的,但不提供针对 CPU 的内存屏障,无法解决多核环境下的可见性和有序性问题。 9. 手撕一下:写一下 CAS 实现的无锁队列1234567891011121314151617181920212223242526272829303132333435#include <atomic>template <typename T>class LockFreeQueue {public: void Enqueue(const T& val) { Node* new_node = Node(val); Node* old_tail = tail.load(); while(true) { old_tail = tail.load(); Node* next = old_tail->next.load(); if(old_tail == tail.load()) { if(next == nullptr) { if(old_tail->next.compare_exchange_strong(next, new_node)) { tail.compare_exchange_weak(old_tail, new_node); return; } } else tail.compare_exchange_strong(old_tail, next); } } }private: struct Node { T data; std::atomic<Node*> next; Node(T val) : data(val), next(nullptr) {} }; std::atomic<Node*> head; std::atomic<Node*> tail;};int main(int argc, char* argv[]) {} 多线程与锁 1. 多线程最常见的问题是什么?主要是三个问题: 线程安全问题:多个线程同时读写同一个块内存,导致结果不可预期 活跃性问题:包括死锁,活锁和饥饿,其中死锁是最致命的,会导致程序完全卡死 性能开销:过渡的上下文切换和锁竞争导致性能下降 2. 死锁怎么避免?避免死锁的核心逻辑是:防止循环等待链的形成。常用的解决办法如下: 固定加锁顺序:规定所有的线程必须按照相同的顺序获取锁(先拿 A 锁,再拿 B 锁) 尝试锁(Try-lock):使用 std::unique_lock 的 try_lock,如果拿不到锁就立即释放已占用的所并且回退 使用高级抽象:使用其他无锁的机制(比如消息传递等等)来替代底层锁 3. 哪些设计方式/技巧能破坏死锁的 4 个条件?互斥性:资源一次只能被一个线程占用。这个是无法破坏(锁的本意)。 占用且等待:线程持有一个资源的同时,还在等待另一个资源。采用一次性申请的方式解决: 线程启动前一次性申请所有需要的锁;或者在无法获取下一个锁时,释放已占有的所有锁。 不可剥夺:资源不能被抢占,只能由持有者主动释放。采用抢占机制的方式解决:如果线程申请新锁失败,主动释放手中资源,过段时间再重试 循环等待:存在一个线程等待环路:T1 等 T2,T2 等 T1。采用资源线性排序的方式解决: 给所有锁分配唯一序号,强制规定必须按序号从小到大(或从大到小)获取锁。 4. 实际开发中,你会如何实现避免死锁的发生?我会使用 std::scoped_lock 一次性添加多个锁利用其内部的机制来避免 12345678910111213141516171819202122232425262728293031323334#include <iostream>#include <mutex>#include <thread>class Account {public: int balance = 1000; std::mutex mtx; // 每个账户自带一把锁};void transfer(Account& from, Account& to, int amount) { // 关键点:std::scoped_lock 同时接收多个 mutex // 它内部使用了一种免死锁算法(通常是逐个尝试且不持有等待) // 无论你传入参数的顺序如何,它都能保证不会发生死锁 std::scoped_lock lock(from.mtx, to.mtx); if (from.balance >= amount) { from.balance -= amount; to.balance += amount; std::cout << "转账成功: " << amount << std::endl; }}int main() { Account alice, bob; // 线程 1:从 Alice 转账给 Bob std::thread t1(transfer, std::ref(alice), std::ref(bob), 100); // 线程 2:从 Bob 转账给 Alice (如果用普通 lock 会因顺序相反导致死锁) std::thread t2(transfer, std::ref(bob), std::ref(alice), 200); t1.join(); t2.join(); return 0;} 其内部的机制 避免循环等待:内部并不简单地按顺序调用 lock(),而是采用了一种 “重试机制”: 1. 它会尝试锁住第一个互斥量。 2. 接着尝试 try_lock 后面的互斥量。 3. 如果中间任何一个锁失败了,它会立即释放已经持有的所有锁,然后重新开始。 原子性加锁:保证了所有传入的互斥量要么全部锁住,要么一个都不锁。","categories":[{"name":"C++ 面经","slug":"C-面经","permalink":"https://dxblacksmith.github.io/categories/C-%E9%9D%A2%E7%BB%8F/"}],"tags":[]},{"title":"连网","slug":"连网","date":"2025-12-07T05:09:21.000Z","updated":"2026-02-04T09:02:50.797Z","comments":true,"path":"2025/12/07/连网/","permalink":"https://dxblacksmith.github.io/2025/12/07/%E8%BF%9E%E7%BD%91/","excerpt":"","text":"“连网”平时我们说的连网,连网到底是什么呢?为什么实验室的主机只能接网线或者连接 wifi 才能访问互联网上的各种资源呢? 为了搞清楚这个问题,需要搞懂下面几个概念 连网:主机通过有线或无线方式接入网络,获得 IP 地址,并能够与其他设备或互联网进行数据通信的过程。 主机网络通信的方法公网通信两台主机都有公网 IP, 可以直接通过 ssh user@公网 IP 连接 这种通常只适用于具有固定公网 IP 的设备连接。 家庭的宽带通常是动态的公网 IP (重启光猫以后会变化),企业或者云服务器才是静态公网 IP 。 解释家里为什么只有连接 wifi (连接路由器)才能访问互联网上的各种资源。 只要是需要访问互联网上的资源,那么都需要设备具有公网 IP, 但是手机等设备出厂时只有 mac 地址,连接路由器的才会给你分配 IP 地址(IP 地址是逻辑地址,由网络环境决定的),但是就算给你分配了,也只是内网 IP,路由器才是唯一具有公网 IP 的设备。 不过连接的路由器会将你的内网 IP 结合自己的 WAN 口(公网 IP)转换成公网 IP,收到回复包的时候,又将这个公网 IP 转换成内网 IP 从而找到你的设备,这其中牵扯到的协议叫做 NAT 协议,这里先不仔细讲了,大概是实现了地址转换的意思。 Ps. 用 4G/5G 是不经过家用路由器,直接通过基站连接到运营商的核心网,然后运营商给你分配 CGNAT IP/ 公网 IP 。 局域网通信同一局域网内通信在同一局域网下面,通过局域网的 IP 能够直接进行通信。这个用的比较多,一般公司/学校/实验室内部服务器都是通过这种方式。 那么有些时候为了固定比如监控摄像头和打印机的 IP 或者实验室根本就没有路由器(纯内网通信),这个时候作为计算机专业的我们,可能会让我们配置静态 IP。 于是就会接触下面的概念: 子网掩码:用于划分网络地址和主机地址。同一局域网的所有 IP & 上这个掩码都会得到同一个地址(一般就填 255.255.255.0 ) 默认网关:当访问不是局域网的地址的时候,数据包就会发给网关。通常是路由器的内网 IP ,大多数默认网关的 IP 地址的最后一位是 1 或者 254(0 不可以,255 用作广播) DNS 服务器:用于将输入的域名解析成 IP 地址,比较方便。DNS 服务器通常填 114.114.114.114 这种公共 DNS 服务器即可 在纯内网通信(实验室中常用)当中,交换机由于没有 DHCP 服务器,就没有自动分配 IP 的功能(路由器有),所以这个时候只能通过手动配置 IP 的方式来实现通信。 这个时候只需要所有的机器都在同一个网段,比如 12345678910IP: 不冲突就行 机器 A : 192.168.10.10 机器 B : 192.168.10.11 机器 B : 192.168.10.12 子网掩码: 统一填 255.255.255.0 默认网关: 一般留空(0.0.0.0),因为一般不需要上网DNS: 一般也留空,除非你要解析域名,但是一般内网用 IP 地址 如何配置一个新设备的静态 IP 记录同网段下另一台配置好得设备的网段 (IP + 子网掩码 192.168.31.0/24)和默认网关( 192.168.31.1) 选择一个不冲突的静态 IP(ping 一下看看连不连通),登录设备的管理界面,这个一般根据设备的不同有些许不同,可以查看下表: 设备类型 登录方式 打印机 1. 在打印机面板上查看 IP 2. 用浏览器访问该 IP 3. 输入管理员账号密码(默认常印在机身) 交换机(网管型) 1. 用 Console 线(串口)连接电脑 2. 用终端软件(PuTTY、SecureCRT)登录 摄像头 / NAS 浏览器访问 IP 或专用客户端 Linux 服务器 SSH 登录后修改网络配置文件 配置 IP 参数(一般内网就配置 IP 地址和子网掩码就可以)同时保存验证,比如用新 IP 能够重新访问设备的管理界面和用其他设备 ping 一下这个设备来测试连通。 不同局域网之间通信 通过跳板机中转 跳板机:一个专门用于中转访问内网资源的 “堡垒” 服务器,一般用于集中管理访问权限。 比如公司的数据库服务器,或者内部的 API 服务部署在私有网络中,处于安全考虑不允许直接暴露到互联网中。所以他们没有公网 IP,同时又不可能和你在一个局域网内,这个时候就需要先登录一台有公网 IP 的跳板机,再从它跳转过去。 这样目标服务器就实现了完全隔离,其防火墙只允许跳板机 IP 访问 SSH 端口。 那么我们如何能够通过跳板机来连接目标服务器呢? 最推荐的就是直接编辑本地电脑的 SSH 配置文件(~/.ssh/config 文件)。假如我们有如下信息 角色 信息 跳板机 公网 IP: 203.0.113.10,用户名: abc,SSH 端口: 22 目标服务器 内网 IP: 10.0.1.20,用户名: cba 那么直接在配置文件中定义如下即可: 1234567891011# 跳板机定义Host jump HostName 203.0.113.10 User ops Port 22# 目标服务器(通过跳板机访问)Host db-server HostName 10.0.1.20 User app ProxyJump jump vscode 下载相应的插件以后读取这个配置,就能直接连接内部服务器,实现代码编辑和文件上传(vscode 内置的 SFTP )。也有人用 xftp 或者 Filezilla 来实现文件传输,我没用过,所以暂时不评论。 Ps. vscode 具体操作就是调出命令面板,remote-ssh: connet to host。 内网穿透 这个暂时没有接触到,先挖个坑。。。","categories":[{"name":"计算机网络","slug":"计算机网络","permalink":"https://dxblacksmith.github.io/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"}],"tags":[]},{"title":"引用第三方库","slug":"开发环境","date":"2025-11-26T09:56:41.160Z","updated":"2026-01-29T09:19:27.507Z","comments":true,"path":"2025/11/26/开发环境/","permalink":"https://dxblacksmith.github.io/2025/11/26/%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83/","excerpt":"","text":"引用第三方库Windows 环境一般引用第三方库的时候,主要需要三个东西: 头文件(include):包含库的函数声明,类定义等等 库文件(lib):包含编译好的二进制代码 运行时库(dll):动态链接库,静态链接库则不需要 为了得到这三个东西,一般的流程是: 获取项目源码这个一般直接从 github 直接拉下来就行,或者浏览器直接下载等等也行 12# 从 GitHub 克隆或下载源码git clone https://github.com/grpc/grpc CMake 配置和生成注意:cmake的配置和生成阶段不涉及编译。目的是生成构建系统所需要的文件(如 .sln和 Makefile文件) 121. configure: 解析 CMakeLists.txt 中的配置选项,设置项目变量和缓存,检查依赖关系2. generate: 生成构建系统所需要的文件(如 .sln 和 .vcxproj 项目文件) 所以 CMake不是编译器,它只是一个跨平台的构建工具 编译第三方库这一步在 windows通常用 msvc来完成,将源代码编译成二进制文件(如果是可执行项目,可以直接生成 .exe,第三方库就是 .lib 和 .dll 文件 。换句话说 .lib 文件 / .dll 文件 / .exe 文件都是二进制文件) 选择配置(Debug/Release) 编译源代码,生成 .obj 文件 链接 .obj 文件 -> 生成最终的 .lib 或者 .dll 文件(可执行项目就是 .exe 文件) Ps. 点击生成之前需要进行一个配置,很重要,很多项目不兼容就是因为这个原因。项目中的所有库必须使用相同的运行时库类型(runtime-link)! 这一步就是在配置运行时库的一个类型(属性 -> C/C++ -> 代码生成 -> 运行库) 12341. MT(Release): 静态链接多线程运行时库2. MD(Release): 动态链接多线程运行时库3. MTd(Debug): 静态链接多线程调试运行时库4. MDd(Debug): 动态链接多线程调试运行时库 上面这么多类型其实不用记,只需要记住如果有一个项目选择的是 shared方式,其余所有的项目都需要选择 MD。 配置第三方库在上一步库的编译以后,会得到第三方库的目录 include: 头文件目录 lib:库文件目录(grpc.dll, grpc.lib) 或者可执行文件目录 bin,然后接下来就是比较重要的配置第三方库: 包含目录(属性 -> VC++ 目录 -> 包含目录)输入第三方库 include 文件夹的路径 库目录(属性->VC++目录->库目录)输入第三方库 lib 文件夹的路径 运行时库(属性->链接器->输入->附加依赖项)输入 lib 文件夹下具体 lib/dll 文件的全称(有些库比如boost库不需要) Linux 环境1. vscode + clangd + conan配置开源项目 配置开源项目 a. 项目中一般有 conanfile.txt,所以第一步就直接安装 1conan install . --build=missing -s build_type=Release b. 为了让 clangd 正常工作,这里要首先 CMake 生成 compile_commands.json(作者一般写好了,直接构建就行) 123456cmake \\ -DCMAKE_TOOLCHAIN_FILE=build/Release/generators/conan_toolchain.cmake \\ -DCMAKE_BUILD_TYPE=Release \\ -S . \\ -B build \\ -G Ninja c. 创建软链接,让 clangd自动发现 1ln -sf build/compile_commands.json ./ 重新配置项目环境 a. 写 conanfile.txt,因为这里一般是复现,可以直接把作者的该文件抄过来 12345678910111213[requires]fmt/11.1.3spdlog/1.15.1cpp-httplib/0.20.1nlohmann_json/3.12.0gflags/2.2.2[generators]CMakeDepsCMakeToolchain[layout]cmake_layout b. 重新检测缓存环境,于项目根目录下生成第三方库的配置环境 1conan install . --build=missing -s build_type=Release c.写 CMakeLists来构建 compile_commands.json文件,注意一定要导出编译数据库,查找依赖和链接库 1234567891011121314151617181920212223242526272829# CMakeLists.txt - 最小可工作版本cmake_minimum_required(VERSION 3.20)project(Demo LANGUAGES CXX)# ✅ 关键:导出编译数据库(让 clangd 可用)set(CMAKE_EXPORT_COMPILE_COMMANDS ON)# 设置 C++ 标准set(CMAKE_CXX_STANDARD 17)set(CMAKE_CXX_STANDARD_REQUIRED ON)# 查找 Conan 安装的依赖find_package(fmt CONFIG REQUIRED)find_package(spdlog CONFIG REQUIRED)find_package(httplib CONFIG REQUIRED)find_package(nlohmann_json CONFIG REQUIRED)find_package(gflags CONFIG REQUIRED)# 添加一个空的可执行文件(只是为了触发编译和生成 compile_commands.json)add_executable(demo main.cpp)# 链接你关心的库(告诉 CMake:“我要用这些头文件和库”)target_link_libraries(demo PRIVATE fmt::fmt spdlog::spdlog httplib::httplib nlohmann_json::nlohmann_json gflags::gflags) d. 执行 CMake 并将生成的 compile_commands.json软链接到项目根目录下: 123456cmake \\ -DCMAKE_TOOLCHAIN_FILE=build/Release/generators/conan_toolchain.cmake \\ -DCMAKE_BUILD_TYPE=Release \\ -S . \\ -B build \\ -G Ninja 1ln -sf build/compile_commands.json ./ 2. vs2022 + ssh 配置开源项目(前提是项目是存在本地的)a. 通过 ssh 连接到远程系统 12341. 主机名一般填 127.0.0.1 (wsl),其它看情况2. 端口名一般就是 223. 用户名最好填 root,免得出现权限问题4. 密码就是 sudo passwd root 的密码 b. 指定运行的计算机 121. 项目属性 -> 常规 -> 远程生成计算机2. 同时下面可以指定远程生成代码的根目录 c. 指定调试的绝对路径 1231. 项目属性 -> 调试 -> 程序(写入绝对路径)ps. 这里的绝对路径是指的远端程序的绝对路径,就是刚刚指定生成远程代码根目录下面,比如:/root/projects/LinuxConsole/bin/x64/Debug/LinuxConsole.out","categories":[{"name":"项目经验","slug":"项目经验","permalink":"https://dxblacksmith.github.io/categories/%E9%A1%B9%E7%9B%AE%E7%BB%8F%E9%AA%8C/"}],"tags":[]},{"title":"Hot 100 刷题记录","slug":"Hot100 记录","date":"2025-01-13T10:05:08.000Z","updated":"2026-01-15T16:09:31.143Z","comments":true,"path":"2025/01/13/Hot100 记录/","permalink":"https://dxblacksmith.github.io/2025/01/13/Hot100%20%E8%AE%B0%E5%BD%95/","excerpt":"","text":"Hot 100 刷题记录(1)滑动窗口套路 定长滑动窗口 总结成三步:入-更新-出。 入:下标为 i 的元素进入窗口,更新相关统计量。如果窗口左端点 i−k+1<0,即 i<k−1,则尚未形成第一个窗口,重复第一步。 更新:更新答案。一般是更新最大值/最小值。 出:下标为 i−k+1 的元素离开窗口,更新相关统计量,为下一个循环做准备。 比如 123456for(int i = 0; i < s.size(); ++i) { if(uset.count(s[i])) cnt ++; // 入 res = std::max(res, cnt); // 更新答案 if(i < k - 1) continue; if(uset.count(s[i - k + 1])) cnt --; // 出} 不定长滑动窗口 越长越合法: 采取在 while 循环外更新答案 123456789for(...) { umap[c] ++; // 入 while(umap[c] > 1) { // 合法性判断 umap[s[left]] --; // 出 left ++; } // 更新 ans = std::max(ans, right - left + 1);} 越短越合法: 采取在 while 循环内更新答案 12345678for(...) { sum += nums[right]; // 入 while(sum >= target) { // 合法性判断 res = min(res, right - left + 1); // 更新 sum -= nums[left]; // 出 left ++; }} 求子数组个数: 1暂时还没刷到","categories":[{"name":"算法题","slug":"算法题","permalink":"https://dxblacksmith.github.io/categories/%E7%AE%97%E6%B3%95%E9%A2%98/"}],"tags":[]}],"categories":[{"name":"Java","slug":"Java","permalink":"https://dxblacksmith.github.io/categories/Java/"},{"name":"数据库","slug":"数据库","permalink":"https://dxblacksmith.github.io/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/"},{"name":"JavaScript","slug":"JavaScript","permalink":"https://dxblacksmith.github.io/categories/JavaScript/"},{"name":"项目经验","slug":"项目经验","permalink":"https://dxblacksmith.github.io/categories/%E9%A1%B9%E7%9B%AE%E7%BB%8F%E9%AA%8C/"},{"name":"第三方库","slug":"第三方库","permalink":"https://dxblacksmith.github.io/categories/%E7%AC%AC%E4%B8%89%E6%96%B9%E5%BA%93/"},{"name":"C++ 新特性","slug":"C-新特性","permalink":"https://dxblacksmith.github.io/categories/C-%E6%96%B0%E7%89%B9%E6%80%A7/"},{"name":"计算机网络","slug":"计算机网络","permalink":"https://dxblacksmith.github.io/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"},{"name":"计算机求职","slug":"计算机求职","permalink":"https://dxblacksmith.github.io/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E6%B1%82%E8%81%8C/"},{"name":"C++ 面经","slug":"C-面经","permalink":"https://dxblacksmith.github.io/categories/C-%E9%9D%A2%E7%BB%8F/"},{"name":"算法题","slug":"算法题","permalink":"https://dxblacksmith.github.io/categories/%E7%AE%97%E6%B3%95%E9%A2%98/"}],"tags":[]}