创建了一个完整的简化区块链系统。这个系统包含以下主要特性:
-
区块结构(Block 类):
- 包含索引、时间戳、数据、前一个区块的哈希值
- 实现了工作量证明(Proof-of-Work)机制
- 使用 OpenSSL 进行 SHA-256 哈希计算
-
区块链结构(Blockchain 类):
- 管理区块链
- 提供添加新区块的功能
- 验证整个链的有效性
-
主要功能:
- 工作量证明(通过调整难度值)
- 区块验证
- 链验证
- 区块创建
要编译和运行这个项目,您需要:
- 安装 OpenSSL 开发库
- 使用 CMake 构建项目:
mkdir build
cd build
cmake -G "Visual Studio 15 2017" -A x64 ..
cmake --build . --config Release- 运行程序:
./blockchain这个实现使用了现代 C++ 特性:
- C++17 标准
- 智能指针(
std::shared_ptr) - 现代时间处理(
std::chrono) - 字符串流处理
- 内存安全的设计
系统的工作流程是:
- 创建区块链实例(默认难度为4)
- 自动创建创建区块
- 添加新区块时会自动进行挖矿(工作量证明)
- 每个区块都包含前一个区块的哈希值,确保链的完整性
- 可以随时验证整个链的有效性
您可以通过调整 difficulty 参数来改变挖矿难度。难度值越高,挖矿所需的时间就越长。
需要注意的是,这是一个简化的实现,包括:
- 区块的创建和链接
- 工作量证明(挖矿)机制
- 区块链的验证
- 数据的不可篡改性(通过哈希链接)
实际的区块链系统还需要考虑:
- 网络通信
- 共识机制
- 交易验证
- 钱包管理
- 更复杂的数据结构
交易列表,一般是一个Transaction列表。
交易结构 (Transaction)
-
包含发送者、接收者、金额、时间戳和交易ID
-
交易ID通过哈希交易数据生成
-
提供了完整的 getter 方法
Merkle 树 (MerkleTree)
-
实现了基本的 Merkle 树结构
-
支持交易验证
-
自动处理奇数个叶子节点的情况
-
提供了根哈希获取方法
笔者注:Merkle树的实现这里其实仅仅临时对Transaction列表做了聚合。在其内部完成:
- 将所有Transaction提取hash值组成列表
- 然后按照双数(奇数最后一个重复)每2个聚合哈希成一个
- 递归汇总成一个hash值后,使用该hash值生成根MerkleNode(所以leaves_永远为空)
- Block在初始化的时候读取这个根MerkleNode的hash值
-
将原来的简单数据字段改为交易列表
-
添加了 Merkle 根哈希
-
更新了哈希计算方法以包含 Merkle 根
笔者注:Block在初始化时,入参简单data更新为交易列表(Transaction),构造时将该列表生成一个MerkleTree,但其实该树仅仅用于提取merkleRoot_,即对所有交易的hash值汇总后再hash。
另外区块保存了交易列表
-
创建示例交易
-
展示交易和 Merkle 树的使用
-
显示更详细的区块信息
主要修改包括:
- 更新了 Blockchain 类以支持交易列表
- 更新了 Block 类以包含交易和 Merkle 树
- 添加了所有必要的源文件到 CMakeLists.txt
这是第一个不完善的交易结构和Merkle树演示。需笔者手动修正。
主要改进包括:
- 完整的树形结构:
-
添加了节点层级(level)属性
-
实现了完整的二叉树结构
-
添加了树的可视化打印功能
- 交易验证机制:
-
实现了 verifyTransaction 方法
-
添加了验证路径(proof path)的构建和存储
-
支持通过路径验证交易的有效性
- 数据结构优化:
-
使用 unordered_map 存储叶子节点和验证路径
-
添加了节点类型判断(叶子节点/内部节点)
- 调试功能:
-
添加了 printTree 方法用于可视化树的结构
-
支持按层级打印节点信息
现在,Merkle 树的实现更加完整和健壮:
-
可以验证交易是否存在于树中
-
可以追踪交易到根节点的路径
-
支持树的层级管理
-
提供了调试和可视化功能
Block中增加了merkleRoot_字段,其通过初始化时构造MerkleTree然后提取根节点的HASH值获得。
merkleRoot_可用于在后续验证交易是否被篡改。
假设我们有4个交易(A、B、C、D),构建的默克尔树如下:
Root
/ \
/ \
AB CD
/ \ / \
A B C D
相邻节点一起构造了一个父节点,其Hash值为2个子节点拼接后再次hash出的值。如果叶节点为奇数,则最后一个叶节点重复一次。
MerkleTree::printTree
ABCC Level 2: 779cc102d995f0cb01c420eddc2d9a2154a992cc4d93a113e18ab34e001850a2
AB Level 1: e5d5262bd00f3ab8dc254c5ed2d4a8b9a3793d3fce38e8e8a693ad451218033f
A Level 0: 43caa04a00f424b1911f5fff8ee452c32e2bc945ae8a88a05183021d27a69446
B Level 0: 7cb17ad411d33feb075223d142e3cb631afd52370cfd1961c34dc4d300978a3a
CC Level 1: 37f004022cc1769ebc8214ed4a5e17b387935f308bb6b81451b2fe0f0b6193d1
C Level 0: 97fd3316a49190b85a5b9305b28854d30ac937e551f759d45b56319d82b777e3
C Level 0: 97fd3316a49190b85a5b9305b28854d30ac937e551f759d45b56319d82b777e3
proofPaths 表为每个叶节点构造一个最快构建出根节点的相邻节点hash列表。即顺着列表逐个拼接HASH值并hash出父节点HASH值。
MerkleTree::MerkleTree
MerkleNode::MerkleNode A
MerkleNode::MerkleNode B
MerkleNode::MerkleNode C
MerkleTree::buildTree
MerkleNode::MerkleNode AB
MerkleNode::MerkleNode CC
MerkleTree::buildTree
MerkleNode::MerkleNode ABCC
buildProofPaths leftPath CC true
buildProofPaths leftPath B true
buildProofPaths leaf A
buildProofPaths rightPath A false
buildProofPaths leaf B
buildProofPaths rightPath AB false
buildProofPaths leftPath C true
buildProofPaths leaf C
buildProofPaths rightPath C false
buildProofPaths leaf C
默克尔树的证明路径(Merkle Proof)的目的是:证明某个交易确实存在于区块链中。为了做到这一点,我们需要提供从该交易到根节点的路径上所有兄弟节点的哈希值。
让我们通过一个具体的例子来说明:
假设我们有4个交易(A、B、C、D),构建的默克尔树如下:
Root
/ \
/ \
AB CD
/ \ / \
A B C D
现在,如果我们想证明交易 A 确实存在于树中,我们需要提供:
- B 的哈希值(A 的兄弟节点)
- CD 的哈希值(AB 的兄弟节点)
这样,验证者可以:
- 用 A 的哈希值和 B 的哈希值计算出 AB
- 用 AB 的哈希值和 CD 的哈希值计算出 Root
- 将计算出的 Root 哈希值与实际的 Root 哈希值比较
所以,在 buildProofPaths 中:
// 为左子节点构建路径
if (node->getRight()) {
// 如果当前节点是左子节点,需要记录右兄弟节点的哈希值
leftPath.push_back({node->getRight()->getHash(), true});
}
buildProofPaths(node->getLeft(), leftPath);这段代码的意思是:
- 当我们在左子节点时,我们需要记录右兄弟节点的哈希值,因为验证时需要用它来计算父节点的哈希值
true表示这个哈希值应该放在左边(在验证时,我们需要用siblingHash + currentHash的方式组合)
同理:
// 为右子节点构建路径
if (node->getLeft()) {
// 如果当前节点是右子节点,需要记录左兄弟节点的哈希值
rightPath.push_back({node->getLeft()->getHash(), false});
}
buildProofPaths(node->getRight(), rightPath);这段代码的意思是:
- 当我们在右子节点时,我们需要记录左兄弟节点的哈希值
false表示这个哈希值应该放在右边(在验证时,我们需要用currentHash + siblingHash的方式组合)
在验证时(verifyPath 函数):
for (const auto& [siblingHash, isLeft] : path) {
if (isLeft) {
// 兄弟节点在左边,当前哈希在右边
currentHash = calculateHash(siblingHash + currentHash);
} else {
// 兄弟节点在右边,当前哈希在左边
currentHash = calculateHash(currentHash + siblingHash);
}
}这样,通过记录兄弟节点的哈希值和位置信息,我们可以:
- 正确重建从交易到根节点的路径
- 验证交易是否真的存在于区块链中
- 保证数据的完整性和不可篡改性
这就是为什么在构建路径时,我们要记录兄弟节点的哈希值而不是当前节点的哈希值。因为验证时需要的是兄弟节点的哈希值来重建父节点的哈希值。
在 verifyPath 函数中,我们应该倒序访问路径,因为我们需要从叶子节点开始,一步步向上构建到根节点。让我修改这部分代码
- 原来的代码(正序访问):
for (const auto& [siblingHash, isLeft] : path) {
// 从路径的第一个元素开始访问
// 这样会导致哈希值的组合顺序错误
}- 修改后的代码(倒序访问):
for (auto it = path.rbegin(); it != path.rend(); ++it) {
// 从路径的最后一个元素开始访问
// 这样可以从叶子节点开始,一步步向上构建到根节点
}为什么需要倒序访问?
-
路径的构建顺序:
- 在
buildProofPaths中,我们是从根节点开始,向下构建路径 - 路径中的元素顺序是:从根节点到叶子节点的顺序
- 在
-
验证时的需求:
- 我们需要从叶子节点开始,一步步向上构建到根节点
- 每一步都需要用当前哈希值和兄弟节点的哈希值计算父节点的哈希值
-
举个例子:
Root
/ \
/ \
AB CD
/ \ / \
A B C D
如果要验证交易 A:
- 路径中存储的顺序是:[B的哈希值, CD的哈希值]
- 验证时需要:
- 先用 A 和 B 的哈希值计算 AB
- 再用 AB 和 CD 的哈希值计算 Root
- 所以需要倒序访问路径,先处理 CD 的哈希值,再处理 B 的哈希值
添加钱包系统,包括密钥生成、签名和验证功能。使用 OpenSSL 的椭圆曲线加密(ECDSA)来实现。
这个实现添加了以下功能:
-
钱包系统:
- 使用 OpenSSL 的椭圆曲线加密(ECDSA)
- 支持密钥对生成
- 支持交易签名和验证
- 使用 secp256k1 曲线(与比特币相同)
-
交易签名:
- 每个交易都可以被签名
- 签名使用发送方的私钥
- 任何人都可以使用发送方的公钥验证签名
-
安全性:
- 私钥永远不会离开钱包
- 所有敏感操作都有适当的错误处理
- 使用 OpenSSL 的安全随机数生成
-
主程序:
- 创建三个钱包(Alice、Bob 和 Charlie)
- 为每个钱包生成密钥对
- 创建并签名交易
- 验证交易签名
- 创建区块链并添加区块
- 验证区块链的有效性
- 打印详细的区块链信息,包括:
- 区块基本信息(索引、时间戳、哈希等)
- 交易详情
- Merkle 树结构
- 交易验证结果
- 钱包其实是相对于用户,而每笔交易对应着2个钱包之间。
- 钱包生成时核心数拥有自己的私钥和公钥,私钥永不对外暴露,而公钥则暴露给交易
- 钱包可对自己作为发起方的交易签名,签名时使用私钥加密
- 交易保存交易双方的公钥(替代交易双方的HASH值)
- 交易由发起方的钱包对交易的HASH值签名,并将签名保存在交易内
- 交易可通过发起方的公钥对签名解码,解码后是否和交易HASH值一致判断交易是否签名合法
余额管理涉及到如何把保存钱包的余额。
这里一波三折:
- 最初AI举例将余额存在钱包,然后试图在交易操作钱包,然而实施失败
- 后续AI将余额管理作为带三方管理,笔者认为失去意义
- 最后AI将每个钱包的余额记入Block中,笔者排除一堆幻觉后调试通过。但遗留几个问题。
==遗留问题:==
- 钱包中保存余额的部分失效了,钱包自己是否应该知道余额
- 每次统计钱包余额,要遍历chain上所有块。一个成熟的区块链会这么干么?
您提出了两个非常好的问题!
-
关于钱包是否应该知道余额:
- 在真实的区块链系统中,钱包确实应该知道自己的余额
- 钱包可以维护一个本地缓存,通过监听区块链上的交易来更新余额
- 这样可以快速查询余额,而不需要每次都遍历整个链
-
关于余额计算方式:
- 您说得对,遍历整个链来计算余额是非常低效的
- 成熟的区块链系统通常使用 UTXO (Unspent Transaction Output) 模型或账户模型
- 我们当前实现的是账户模型,但实现方式不够优化
让我们改进这个设计:
- 首先修改
Wallet类,添加余额管理:
class Wallet {
public:
// ... 其他代码 ...
// 余额管理
double getBalance() const { return balance_; }
void updateBalance(double amount) { balance_ += amount; }
// 监听交易更新余额
void processTransaction(const Transaction& tx) {
if (tx.getFrom() == getPublicKey()) {
balance_ -= tx.getAmount();
}
if (tx.getTo() == getPublicKey()) {
balance_ += tx.getAmount();
}
}
private:
// ... 其他成员 ...
double balance_ = 0.0;
};- 在
Blockchain类中添加余额缓存:
class Blockchain {
public:
// ... 其他代码 ...
// 获取地址余额(使用缓存)
double getBalance(const std::string& address) const {
auto it = balanceCache_.find(address);
if (it != balanceCache_.end()) {
return it->second;
}
return 0.0;
}
void addBlock(const std::vector<Transaction>& transactions) {
// 验证所有交易
for (const auto& tx : transactions) {
if (!validateTransaction(tx)) {
throw std::runtime_error("Invalid transaction: insufficient balance or invalid signature");
}
}
// 创建新区块
std::string previousHash = chain_.empty() ? "0" : chain_.back()->getHash();
auto newBlock = std::make_shared<Block>(chain_.size(), transactions, previousHash);
// 更新余额缓存
for (const auto& tx : transactions) {
balanceCache_[tx.getFrom()] -= tx.getAmount();
balanceCache_[tx.getTo()] += tx.getAmount();
}
// 挖矿
newBlock->mineBlock(difficulty_);
// 添加区块
chain_.push_back(newBlock);
// 通知钱包更新余额
for (const auto& tx : transactions) {
auto fromWallet = getWalletByPublicKey(tx.getFrom());
if (fromWallet) {
fromWallet->processTransaction(tx);
}
auto toWallet = getWalletByPublicKey(tx.getTo());
if (toWallet) {
toWallet->processTransaction(tx);
}
}
}
private:
// ... 其他成员 ...
std::map<std::string, double> balanceCache_; // 余额缓存
};- 修改
main.cpp中的代码:
int main() {
try {
// ... 创建钱包和生成密钥对的代码 ...
// 创建区块链
Blockchain blockchain(4);
// 设置初始余额
blockchain.addBlock({
Transaction("SYSTEM", alicePublicKey, 100.0),
Transaction("SYSTEM", bobPublicKey, 50.0),
Transaction("SYSTEM", charliePublicKey, 25.0)
});
// 打印初始余额
std::cout << "\nInitial balances:" << std::endl;
std::cout << "Alice: " << aliceWallet.getBalance() << std::endl;
std::cout << "Bob: " << bobWallet.getBalance() << std::endl;
std::cout << "Charlie: " << charlieWallet.getBalance() << std::endl;
// ... 其他代码 ...
}
// ... 其他代码 ...
}这样改进后:
- 钱包维护自己的余额,可以快速查询
- 区块链使用余额缓存,避免遍历整个链
- 当新区块被添加时,钱包会收到通知并更新余额
- 余额的验证仍然由区块链负责,保证安全性
- 钱包的余额作为本地缓存,可以快速响应查询
这种设计更接近真实的区块链系统:
- 钱包负责管理自己的余额
- 区块链负责验证和记录交易
- 使用缓存提高性能
- 保持数据一致性
您觉得这个改进方案如何?
==笔者:需要补充钱包注册==
我已经完成了以下修改:
-
在
Blockchain类中添加了钱包管理功能:- 添加了
wallets_映射来存储钱包 - 添加了
registerWallet和getWalletByPublicKey函数
- 添加了
-
在
blockchain.cpp中实现了这些新函数:registerWallet用于注册钱包到区块链getWalletByPublicKey用于通过公钥查找钱包
-
修改了
main.cpp:- 将钱包对象改为使用智能指针
- 添加了初始余额映射
- 注册钱包到区块链
- 更新了所有钱包相关的调用
这些修改应该解决了 getWalletByPublicKey 未定义的问题。现在区块链系统可以正确地管理钱包和余额了。
您还需要我帮您做其他修改吗?
==为什么创建区块链时添加了初始余额映射,注册钱包后,又设置初始余额==
您说得对,这里确实存在重复设置初始余额的问题。让我解释一下当前的问题并提供修复方案:
当前代码中存在两个地方设置初始余额:
- 在创建区块链时通过
initialBalances参数设置:
std::map<std::string, double> initialBalances = {
{alicePublicKey, 100.0},
{bobPublicKey, 50.0},
{charliePublicKey, 25.0}
};
Blockchain blockchain(4, initialBalances);- 在创建创世区块时又通过交易设置:
blockchain.addBlock({
Transaction("SYSTEM", alicePublicKey, 100.0),
Transaction("SYSTEM", bobPublicKey, 50.0),
Transaction("SYSTEM", charliePublicKey, 25.0)
});这确实是不必要的重复。我们应该只保留一种方式来设置初始余额。
现在我们还需要修改 Blockchain 类的构造函数,移除 initialBalances 参数,
最后,我们需要修改 blockchain.cpp 中的构造函数实现:
我已经完成了以下修改:
- 移除了
Blockchain构造函数中的initialBalances参数 - 移除了在创建区块链时通过映射设置初始余额的方式
- 保留了通过创世区块交易设置初始余额的方式
这样修改的原因是:
- 通过交易设置初始余额更符合区块链的设计理念,因为所有状态变化都应该通过交易来记录
- 避免了重复设置余额的问题
- 保持了代码的一致性和清晰性
现在初始余额只通过创世区块的交易来设置,这样更合理且避免了重复。您觉得这样的修改合适吗?
==设置初始余额的时候没有对交易签名==
您说得对!这是一个很好的发现。在设置初始余额时,我们使用了 "SYSTEM" 作为发送方,但没有对交易进行签名。这违反了区块链的基本规则,即所有交易都需要有效的签名。
让我们修改代码,为系统交易添加一个特殊的签名机制:
我已经完成了以下修改:
- 在
Transaction类中添加了createSystemTransaction静态方法,用于创建系统交易 - 修改了
verifySignature方法,使其能够验证系统交易的特殊签名 - 更新了
main.cpp中的初始余额设置,使用createSystemTransaction方法
这些修改的好处是:
- 系统交易有了明确的标识和验证机制
- 保持了交易验证的一致性
- 使代码更加清晰和可维护
系统交易使用特殊的签名 "SYSTEM_SIGNATURE",这是一个简化的实现。在实际的区块链系统中,系统交易通常会有更复杂的验证机制,比如:
- 使用特殊的系统密钥对
- 使用多重签名
- 使用共识机制验证
您觉得这样的修改合适吗?
我已经修复了循环依赖的问题:
- 在 wallet.h 中:
-
移除了 #include "transaction.h"
-
添加了 Transaction 类的前向声明
-
将 processTransaction 方法的实现移到 cpp 文件中
- 在 wallet.cpp 中:
-
添加了 #include "transaction.h"
-
实现了 processTransaction 方法
这样修改的好处是:
-
解决了循环依赖问题
-
遵循了 C++ 的最佳实践,将实现细节放在 cpp 文件中
-
保持了代码的功能不变
现在代码应该可以正常编译了。您还需要我做其他修改吗?
==validateTransaction校验余额时,系统钱包不应该校验==
您说得对!系统交易("SYSTEM" 作为发送方)不应该进行余额校验,因为系统交易是用来创建初始余额的。让我修改 Blockchain 类中的 validateTransaction 方法:
我已经修改了 validateTransaction 方法,添加了对系统交易的特殊处理:
- 首先验证交易签名
- 如果是系统交易(
from_ == "SYSTEM"),直接返回true,跳过余额检查 - 对于普通交易,继续执行余额检查
这样修改的好处是:
- 系统交易可以不受余额限制,用于创建初始余额
- 普通交易仍然需要验证余额,确保交易有效
- 保持了代码的清晰性和可维护性
这个修改更符合区块链系统的设计,因为:
- 系统交易是特殊的,用于初始化系统状态
- 普通交易必须遵守余额规则
- 系统交易仍然需要验证签名,确保其合法性
我来帮您实现交易池(Transaction Pool)和UTXO(Unspent Transaction Output)模型。这是区块链系统中非常重要的两个组件。
主要的变更:
- UTXO模型:
- 创建了
UTXO类来表示未花费的交易输出 - 实现了
UTXOPool类来管理所有UTXO - 提供了查询余额、选择UTXO等功能
- 交易池:
- 创建了
TransactionPool类来管理待处理的交易 - 实现了交易的验证、添加、移除等功能
- 使用互斥锁保证线程安全
- 交易结构更新:
- 添加了
TransactionInput和TransactionOutput类 - 更新了
Transaction类以支持UTXO模型 - 实现了更安全的交易验证机制
- 区块链更新:
- 集成了UTXO池和交易池
- 更新了区块添加逻辑,支持UTXO模型
- 实现了余额查询的新方法
主要改进:
- 更好的安全性:UTXO模型可以防止双重支付
- 更高的效率:交易池可以缓存待处理交易
- 更准确的余额计算:基于UTXO而不是简单的加减
- 更好的并发支持:使用互斥锁保护共享资源
使用示例:
// 创建交易
Transaction tx(fromAddress, toAddress, amount);
// 选择UTXO
auto utxos = utxoPool.selectUTXOs(fromAddress, amount);
// 添加输入
for (const auto& utxo : utxos) {
tx.addInput(TransactionInput(utxo.getTxId(), utxo.getOutputIndex(), signature));
}
// 添加输出
tx.addOutput(TransactionOutput(amount, toAddress));
if (totalInput > amount) {
tx.addOutput(TransactionOutput(totalInput - amount, fromAddress)); // 找零
}
// 添加到交易池
blockchain.addTransactionToPool(tx);UTXO和交易池的概念和实现。
UTXO是比特币等区块链系统中使用的一种交易模型。它的核心思想是:
- 基本概念:
- 每个交易输出(UTXO)代表一定数量的加密货币
- 每个UTXO只能被使用一次
- 交易必须使用已有的UTXO作为输入,并创建新的UTXO作为输出
- UTXO类的实现:
class UTXO {
std::string txId_; // 交易ID
int outputIndex_; // 输出索引
double amount_; // 金额
std::string owner_; // 所有者地址
bool spent_; // 是否已花费
};- UTXOPool的功能:
- 管理所有未花费的交易输出
- 提供余额查询
- 选择UTXO进行交易
class UTXOPool {
// 存储结构:交易ID -> (输出索引 -> UTXO)
std::map<std::string, std::map<int, UTXO>> utxos_;
// 主要方法
void addUTXO(const UTXO& utxo); // 添加UTXO
void removeUTXO(const std::string& txId, int outputIndex); // 移除UTXO
double getBalance(const std::string& address); // 查询余额
std::vector<UTXO> selectUTXOs(const std::string& address, double amount); // 选择UTXO
};交易池是一个临时存储待处理交易的容器,主要功能包括:
- 基本概念:
- 存储等待被打包进区块的交易
- 提供交易验证功能
- 管理交易的添加和移除
- TransactionPool的实现:
class TransactionPool {
std::map<std::string, Transaction> transactions_; // 存储交易
mutable std::mutex mutex_; // 线程安全锁
// 主要方法
bool addTransaction(const Transaction& transaction, const UTXOPool& utxoPool);
void removeTransaction(const std::string& txId);
std::vector<Transaction> getTransactions() const;
};- 交易验证流程:
bool TransactionPool::isValidTransaction(const Transaction& transaction, const UTXOPool& utxoPool) const {
// 验证签名
if (!transaction.verifySignature()) return false;
// 验证余额
if (!utxoPool.hasEnoughFunds(transaction.getFrom(), transaction.getAmount())) return false;
// 验证金额
if (transaction.getAmount() <= 0) return false;
// 验证发送者和接收者
if (transaction.getFrom() == transaction.getTo()) return false;
return true;
}- 创建交易:
// 1. 选择UTXO
auto utxos = utxoPool.selectUTXOs(fromAddress, amount);
// 2. 创建交易输入
for (const auto& utxo : utxos) {
tx.addInput(TransactionInput(utxo.getTxId(), utxo.getOutputIndex(), signature));
}
// 3. 创建交易输出
tx.addOutput(TransactionOutput(amount, toAddress));
// 如果有找零
if (totalInput > amount) {
tx.addOutput(TransactionOutput(totalInput - amount, fromAddress));
}- 交易处理:
// 1. 添加到交易池
blockchain.addTransactionToPool(tx);
// 2. 挖矿时从交易池获取交易
auto pendingTransactions = transactionPool.getTransactions();
// 3. 交易确认后更新UTXO池
void Blockchain::updateUTXOPool(const Block& block) {
for (const auto& tx : block.getTransactions()) {
// 移除已使用的UTXO
for (const auto& input : tx.getInputs()) {
utxoPool_.removeUTXO(input.getTxId(), input.getOutputIndex());
}
// 添加新的UTXO
for (size_t i = 0; i < tx.getOutputs().size(); ++i) {
const auto& output = tx.getOutputs()[i];
UTXO utxo(tx.getTransactionId(), i, output.getAmount(), output.getOwner());
utxoPool_.addUTXO(utxo);
}
}
}这种设计的优点:
- 防止双重支付
- 提高交易验证效率
- 支持并行处理
- 更好的隐私保护
- 更准确的余额计算
主程序更新:
- 交易创建流程:
-
现在每个交易都需要选择UTXO作为输入
-
添加了交易输入和输出的处理
-
实现了找零机制
- 交易池集成:
-
交易先添加到交易池
-
挖矿时从交易池获取待处理交易
-
交易确认后从交易池中移除
- 余额查询:
-
使用UTXO池来查询余额
-
不再使用钱包的余额缓存
- 输出信息:
-
添加了交易输入和输出的详细信息
-
显示UTXO的使用情况
-
显示找零信息
- 本地化:
-
将所有输出信息改为中文
-
优化了输出格式
主要改进:
-
更安全的交易机制
-
更准确的余额计算
-
更好的交易追踪
-
更清晰的输出信息
本阶段用例在链上添加了2个数据
- 交易池
- UTXO池
事实上,UTXO在链上是永久驻留,其记录了每笔交易的结果(财富属于谁)
- 每次交易发生,该次交易的发起方的历史UTXO都被清除,并生成一个新的代表余额的UTXO;
- 每次交易发生,该次交易的接收方将添加一个新的UTXO(所以某个钱包如果只接收不支出,则UTXO会越来越多
而Transaction Pool只是缓存未完成交易
- 当链上新增一个块时,会检查链上未完成交易,并将所有未完成交易计入此块的交易。
- 对该块挖矿(工作量证明)
- 针对该块更新UTXO(即将交易池中未完成交易完成)
- 清空交易池
所以在此例中,交易池不是必须的。
示例程序流程
-
创建三个钱包(构造时生成秘钥对),获取钱包公钥作为身份识别
-
创建链,生成一个0块。id为0,空交易,前置块Hash为0
-
链上注册钱包,公钥到钱包的映射
-
创建三个系统交易(初始交易,准备给钱包充值)
- 初始化交易(系统,钱包ID,金额)
- 系统交易特殊签名
- 给交易插入一个“交易输出”(金额,钱包ID)
- 更新交易Hash值(每次给交易添加输入输出都要更新Hash)
-
将以上交易列表加入链(生成块)
- 合并新交易和待处理交易(==transactionPool_==无)
- ==生成块==
- 初始化块index,交易列表,前块的hash值
- 获取时间戳
- 创建MerkleTress生成merkleRoot_
- 计算hash值
- ==挖矿(更新hash值)==
- ==块添加到链上==
- ==更新UTXO池==(链操作,入参是块)
- 遍历交易
- 遍历交易输入(这里无)
- 遍历交易输出(这里3个)
- 生成一个UTXO(交易ID,序号(0),金额,拥有者ID)
- UTXO加入==utxoPool_==(chain上总池)
- 第一级map key为交易ID
- 第二级map key为utxo的输出index(这里永远为0吧?)
- 遍历交易
- 清空交易池中已确认的交易
- 遍历之前合并的交易(该交易被加入块)
- ==transactionPool_== 中删除所以交易(这里transactionPool_为空,所以无操作)
- 遍历之前合并的交易(该交易被加入块)
-
检查初始余额
- 通过链操作获取各钱包余额。
- ==utxoPool_==中获取余额
- 遍历2层map,找到所有者地址和请求钱包地址一致的utxo,累计金额
- ==utxoPool_==中获取余额
- 通过链操作获取各钱包余额。
-
==开始正常交易==
-
==创建交易1==(Alice向Bob转10块,注意,这里没有创建交易输入和输出)
-
使用alice的钱包对此次交易签名,==注,此签名没有写入交易==(果然需要写入)
-
通过链操作获取Alice拥有的UTXO
-
遍历Alice所有UTXO,并构造“交易输入”加入此次交易
- utxo的交易ID(此utxo生成时的交易)
- utxo的输出index(感觉无用的字段,其总是为0)
- 交易签名写入此“交易输入”
-
构造向Bob的“交易输出”写入交易
-
通过链操作获取Alice的余额,并判断是否满足本次交易,如果满足
- 计算本交易如果成功,则Alice余额是多少
- 构造一个向Alice写入上述余额的“交易输出”
-
==创建交易2==(Bob向Charlie转5块,注意,这里没有创建交易输入和输出)
-
使用Bob的钱包对此次交易签名,==注,此签名没有写入交易==(果然需要写入)
-
通过链操作获取Bob拥有的UTXO
-
遍历Bob所有UTXO,并构造==“交易输入”==加入此次交易
- utxo的交易ID(此utxo生成时的交易)
- utxo的输出index(感觉无用的字段,其总是为0)
- 交易签名写入此“交易输入”
-
构造向Charlie的==“交易输出”==写入交易
-
通过链操作获取Bob的余额,并判断是否满足本次交易,如果满足
- 计算本交易如果成功,则Bob余额是多少
- 构造一个向Bob写入上述余额的==“交易输出”==
-
==创建交易==3(Charlie向Alice转2.5块,注意,这里没有创建交易输入和输出)
-
使用Charlie的钱包对此次交易签名,==注,此签名没有写入交易==(果然需要写入)
-
通过链操作获取Charlie拥有的UTXO
-
遍历Charlie所有UTXO,并构造“交易输入”加入此次交易(==交易的输入是发起者所有的UTXO==)
- utxo的交易ID(此utxo生成时的交易)
- utxo的输出index(感觉无用的字段,其总是为0)
- 交易签名写入此“交易输入”
-
构造向Alice的“交易输出”写入交易(==接受者的交易输出==)
-
通过链操作获取Charlie的余额,并判断是否满足本次交易,如果满足
- 计算本交易如果成功,则Charlie余额是多少
- 构造一个向Charlie写入上述余额的“交易输出”(==发起者的交易输出==,这里其实是统计发起者的余额)
-
==通过链操作将交易加入交易池(transactionPool_)==
- 在transactions_中查找交易,如果找到返回失败
- 验证交易
- 验证签名(前文提到,交易签名要写入交易)
- 验证发送者有足够余额(这里利用入参传进来的utxo池)
- 验证交易金额大于0
- 验证交易双方不是一个地址
- 添加到交易池
-
往链上增加一个块(无需携带新的交易,链会检查之前加入的未完成交易)
- 合并新交易和待处理交易(==transactionPool_==有,transactions无)
- ==生成块==
- 初始化块index,交易列表,前块的hash值
- 获取时间戳
- 创建MerkleTress生成merkleRoot_
- 计算hash值
- ==挖矿(更新hash值)==
- ==块添加到链上==
- ==更新UTXO池==(链操作,入参是块)
- 遍历交易
- 遍历交易输入
- 如前文(这里包括了发起者所有的UTXO,==通过向链上的UTXO系统获取==,即它可能有多个),此次遍历将在链上删除所有这些UTXO,即发起方链上原有的所有UTXO将被删除(下文会计算并重构一个余额UTXO)
2025.05.20 2:21 不行了,先睡了,不然明天就废了- 遍历交易输出(这里2个,一个是本次交易的接收方新增一个UTXO,一个是本次交易发送方重新构建一个余额UTXO)
- 生成一个UTXO(交易ID,序号(本次交易的输出序号,这里可以是0和1),金额,拥有者ID)
- UTXO加入==utxoPool_==(chain上总池)
- 第一级map key为交易ID
- 第二级map key为utxo的输出index(这里永远为0吧?,这里明显可以为1)
- 遍历交易输入
- 遍历交易
- 清空交易池中已确认的交易
- 遍历之前合并的交易(该交易被加入块)
- ==transactionPool_== 中删除所以交易(这里transactionPool_为空,所以无操作)
- 遍历之前合并的交易(该交易被加入块)
-
通过链上操作获取各个钱包的余额并显示
-
==开始第二轮交易==
-
交易4:Alice sends 7.5 coins to Charlie(生成一个交易,链上获取Alice UTXO,用Alice钱包给交易签名,将Alice所有UTXO加入交易输入,给Charlie添加交易输出,给Alice添加交易余额)
-
交易5:Charlie sends 3 coins to Bob(生成一个交易,链上获取Charlie UTXO,用Alice钱包给交易签名,将Charlie 所有UTXO加入交易输入,给Bob添加交易输出,给Charlie 添加交易余额)
-
将交易4和5加入链上未处理交易(==验证交易,通过才允许加入==)
-
在链上添加一个块(生成块<构造成员,生成merkleroot,计算hash>,挖矿,添加到链上,更新UTXO池<入参是块>,清空交易池中已确认交易)
-
通过链上操作获取各个钱包的余额并显示
-
打印链信息
当然可以,下面是对 UTXO 的结构、验证机制、以及“双花问题” 的详细讲解:
在比特币和其他采用 UTXO 模型的区块链中,UTXO 并不是一个单独的对象,而是某一笔交易输出中的一项数据。结构大致如下:
| 字段 | 含义 |
|---|---|
交易ID(txid) |
来自某笔交易的哈希值 |
输出索引(index) |
指明该交易中第几个输出 |
金额(amount) |
该输出的币值 |
锁定脚本(scriptPubKey) |
指定谁可以使用该输出(如地址) |
你收到了一笔交易 tx1234... 的第 0 个输出:
{
"txid": "tx1234...",
"index": 0,
"amount": 0.5,
"scriptPubKey": "OP_DUP OP_HASH160 <your_pubkey_hash> OP_EQUALVERIFY OP_CHECKSIG"
}这就是一个 UTXO,表示你拥有 0.5 个 BTC,可以以后花掉。
当你使用一个 UTXO 作为新的交易的输入时,区块链节点会验证:
- 通过检查当前所有已知未花费交易输出(UTXO 集合)。
- 验证输入交易提供的
解锁脚本(scriptSig)是否能“解锁”目标的锁定脚本(scriptPubKey)。 - 比如:是否提供了正确的签名和公钥。
- 所有输入金额 ≥ 所有输出金额;
- 多余部分是矿工费(未明确指定的金额会自动归矿工所有)。
“双花”是指一个 UTXO 被多次花费,也就是试图用一笔“币”同时支付给两个收款方。
你有一个 1 BTC 的 UTXO,然后你构造了两笔交易:
- 交易 A:把 1 BTC 给商家;
- 交易 B:把 1 BTC 给你自己另一个地址;
你广播了这两笔交易,希望都能生效(实际上只能成功一笔)。
每个节点会维护一个“UTXO 集合”(所有未被花费的交易输出),当接收到新的交易时:
- 检查输入引用的 UTXO 是否还在集合中;
- 如果已经被花费(已被移出集合),拒绝这笔交易。
即使两笔交易几乎同时广播,最终只会有一笔交易被打包进区块,并进入“最长链”:
- 比如 A 和 B 被不同节点挖出,只有最终所在的链(难度最高/最长链)会被所有人接受;
- 被淘汰的交易不会进入最终账本;
- 矿工通常会选择手续费更高的一笔交易打包;
- 这也间接抑制了“双花”行为(攻击者要支付更高费用)。
| 前提条件 | 说明 |
|---|---|
| 节点同步 | 所有节点应尽可能实时同步最新区块 |
| 等待确认 | 通常建议等待 6 个区块确认再认为交易不可逆 |
| 网络诚实 | 超过 50% 哈希率的攻击者可能篡改历史(51%攻击) |
| 项目 | 说明 |
|---|---|
| UTXO 是什么 | 一笔未被使用的交易输出,代表你的“币” |
| 验证过程 | 确保 UTXO 未被花费、你拥有私钥、金额匹配 |
| 双花问题 | 尝试重复使用 UTXO 的行为,被节点和共识机制有效识别并防止 |
| 防护方式 | UTXO 状态追踪、最长链共识、工作量证明、等待确认数 |
是的,区块链中的每个完整节点都会同步所有的区块和 UTXO 数据(未花费交易输出),这是确保网络安全性、去中心化和一致性的基础。
-
所有节点会保存从创世区块到当前最新区块的全部区块数据;
-
每个区块中包含:
- 区块头(哈希、时间戳、Merkle Root、上一个区块哈希等);
- 区块体(多笔交易);
-
这相当于账本的“完整历史”,可用于重新验证整个系统状态。
-
每个节点根据区块链里的所有交易,动态维护一个“UTXO 集合”;
-
这集合里是所有尚未被任何交易花掉的输出,即整个网络上“有效的币”;
-
用于快速查找、验证交易合法性;
-
节点处理新的交易或新区块时,会:
- 删除已花费的 UTXO;
- 添加新的交易输出作为新的 UTXO;
因为 UTXO 是状态,会随时间变化,而区块链是事件日志。UTXO 集合是可以根据完整区块链“重建”出来的。
| 节点类型 | 是否保存所有区块 | 是否维护UTXO集合 | 是否参与挖矿 | 说明 |
|---|---|---|---|---|
| 全节点(Full Node) | ✅ 是 | ✅ 是 | ❌ 否 | 验证所有交易、维持网络安全 |
| 轻节点(Light Node) | ❌ 仅保存头部 | ❌ 不维护 | ❌ 否 | 依赖全节点提供验证信息 |
| 矿工节点 | ✅ 是 | ✅ 是 | ✅ 是 | 既是全节点又参与挖矿 |
-
所有全节点遵循同样的共识规则;
-
当有==新区块生成==,会通过点对点网络广播;
-
节点在接收到新区块后:
- 验证区块(工作量、哈希、交易等);
- 添加到本地区块链上;
- 更新 UTXO 集合;
-
若出现分叉(两条链同时出现):
- 节点根据“最长链”规则选择有效链;
- 丢弃较短链中未确认的区块并回滚 UTXO 状态;
理论上是的,每个正常运行的完整节点(Full Node)在达成共识之后,其本地存储的区块链数据应该是一模一样的。 这正是区块链系统去中心化、一致性和可信赖的基础。
- 所有交易和区块数据对所有节点公开;
- 区块是按顺序链接的链式结构;
- 每个区块包含上一个区块的哈希值(保证一致性);
- 节点之间通过共识算法(如比特币的工作量证明 PoW)达成一致的链;
- 如果出现分叉,所有节点都会选择“合法的最长链”;
- 所以最终所有节点都会“收敛”到同一条主链。
-
某一时间点,网络上可能有两个不同的新区块都接在同一个前区块后面;
-
导致一些节点接收的是 A 区块,一些接收的是 B 区块;
-
此时形成临时分叉,但随着后续新的区块出现:
- 较长的链胜出;
- 较短的链被抛弃;
- 所有节点回滚并统一到最长链上。
- 某些节点可能因为网络问题或宕机暂时没有最新区块;
- 但它们重新上线后会自动从其他节点同步缺失区块。
由于:
- 每个区块都包含前一个区块的哈希值;
- 区块数据一旦变动,其哈希就改变;
- 改变区块意味着必须重新计算该区块及之后所有区块的哈希(在 PoW 下代价巨大);
- 所以几乎不可能悄悄更改链上的历史记录而不被网络察觉。
| 问题 | 结论 |
|---|---|
| 每个节点的区块链是否一模一样? | ✅ 是,所有完整节点最终应拥有相同的链 |
| 是否可能出现临时分叉? | ✅ 有可能,但会自动回滚并达成一致 |
| 为什么能保证一致性? | 📌 共识机制 + 链结构 + 验证机制 |
是的,所有全节点会同步整个区块链数据,并且动态维护一份最新的 UTXO 集合。这种机制保障了系统的一致性、安全性和去中心化运行。
实现一个基本的P2P网络通信系统。我们需要创建几个关键组件:
- P2P网络节点类
- 消息类型定义
- 网络通信处理
- 节点发现和连接管理
实现的功能:
-
P2P网络节点(
P2PNode类):- 支持节点发现和连接
- 处理消息广播和转发
- 管理节点间的连接
- 处理区块和交易的同步
-
消息类型:
HANDSHAKE: 节点间建立连接时的握手消息NEW_BLOCK: 新区块广播NEW_TRANSACTION: 新交易广播GET_BLOCKS: 请求区块数据BLOCKS: 区块数据响应GET_PEERS: 请求节点列表PEERS: 节点列表响应
-
主程序功能:
- 创建3个本地节点(端口8001、8002、8003)
- 每个节点都有自己的区块链和钱包
- 节点间自动建立连接
- 支持用户交互命令:
send: 发送交易balance: 查看余额peers: 查看连接的节点exit: 退出节点
要运行这个程序,您需要:
-
安装依赖:
- OpenSSL
- Boost库
- CMake 3.10或更高版本
-
编译程序:
mkdir build
cd build
cmake ..
make- 运行程序:
./BlockChain程序启动后,会创建3个节点并自动建立连接。每个节点都有自己的控制台,您可以在其中输入命令来:
- 发送交易到其他节点
- 查看当前余额
- 查看已连接的节点
- 退出节点
当您在一个节点上发送交易时,交易会被广播到所有连接的节点。每个节点都会验证交易并将其添加到交易池中。当新区块被创建时,它也会被广播到所有节点,确保所有节点的区块链保持同步。
这个实现提供了基本的P2P网络功能,您可以根据需要进一步扩展:
- 添加更多的消息类型
- 实现更复杂的节点发现机制
- 添加网络安全性措施
- 实现更高效的区块同步策略
- 添加更多的用户交互命令
需要注意的是,这是一个基本的实现,在实际生产环境中还需要考虑:
- 网络安全性
- 错误处理
- 性能优化
- 网络分区处理
- 共识机制
- 添加通过json初始化构造块
- 添加toJson将块转换为Json
- 增加一个块验证,仅仅验证了hash值是否是挖矿过的结果(0525补充)
- 修订了构造函数,构造时的时间戳用更高精度,否则容易导致不同节点创世块哈希值一致(0525补充,==难道不是本该一致么==)
- 添加通过json初始化构造块
- 添加toJson将块转换为Json
- 添加通过json初始化构造块
- 添加toJson将块转换为Json
- 增加一个getBlocksFromHeight,从指定节点开始获取链上的块
- 获取链的难度
- 获取链上最后一个块
-
获取块的索引号,如果不等于chain上下一个号则失败(前一个块可以是0)
-
验证前一个块的哈希是否等于当前最后一个块
-
通过块自己的计算hash值函数计算hash并和块携带的hash比较(==可是块的hash不是应该挖过矿后变化了么==)
-
验证区块难度
-
验证区块中的交易(遍历所有交易)
- 验证签名(用交易中的Hash值,签名,发起者公钥)
- 验证余额(从本地chain中获取余额==什么时候更新这个余额==)
-
验证Merkle根(使用交易构造根,和块中携带的根比较)
区块链查找仅用于handleConsensusResult(见后),所以包含在此处,其实其应该是P2P_Node的基本功能。
- 根据区块高度查找(区块号)
- 根据区块Hash查找
每个p2p_node节点有2个重要的成员
- acceptor_: TCP 接收器,所以监听本地TCP端口
- io_context_:事件循环,笔者认为其管理着所有socket,所以其在acceptor之上
是的,您理解得对。让我详细解释一下:
acceptor_的作用:
// 在 P2PNode 类中
tcp::acceptor acceptor_; // 用于监听新连接的接收器acceptor_是 Boost.Asio 中的 TCP 接收器- 它专门用于监听指定端口的连接请求
- 在构造函数中初始化:
P2PNode::P2PNode(const std::string& host, int port, std::shared_ptr<Blockchain> blockchain)
: acceptor_(io_context_, tcp::endpoint(boost::asio::ip::make_address(host), port))
{
// ...
}- 工作流程:
客户端发起连接请求
↓
acceptor_ 检测到新连接
↓
触发 async_accept 的回调函数
↓
在回调中处理新连接
- 具体步骤:
// 1. 初始化 acceptor_,绑定到指定端口
acceptor_(io_context_, tcp::endpoint(boost::asio::ip::make_address(host), port))
// 2. 开始监听新连接
handleNewConnection() {
acceptor_.async_accept(
[this](boost::system::error_code ec, tcp::socket socket) {
// 3. 当有新连接时,这个回调函数被触发
if (!ec) {
// 4. 处理新连接
std::string nodeId = socket.remote_endpoint().address().to_string() + ":" +
std::to_string(socket.remote_endpoint().port());
connections_[nodeId] = std::make_shared<tcp::socket>(std::move(socket));
// ...
}
});
}- 为什么使用
async_accept:
- 异步操作,不会阻塞主线程
- 可以同时处理多个连接请求
- 当有新连接时自动触发回调
- 连接建立后的处理:
// 在回调函数中
if (!ec) {
// 1. 获取连接信息
std::string nodeId = socket.remote_endpoint().address().to_string() + ":" +
std::to_string(socket.remote_endpoint().port());
// 2. 保存连接
connections_[nodeId] = std::make_shared<tcp::socket>(std::move(socket));
// 3. 开始异步读取数据
auto buffer = std::make_shared<boost::asio::streambuf>();
boost::asio::async_read_until(*connections_[nodeId], *buffer, "\n",
[this, nodeId, buffer](boost::system::error_code ec, std::size_t length) {
// 4. 当收到数据时,这个回调被触发
if (!ec) {
// 处理接收到的数据
// ...
}
});
}所以,acceptor_ 确实就是用来监听新连接的,而 async_accept 的回调函数就是在新连接建立时被触发的。这是 Boost.Asio 提供的异步网络编程模型,使得服务器可以高效地处理多个并发连接。
让我详细解释一下 io_context_,这是 Boost.Asio 中的核心组件:
io_context的基本作用:
boost::asio::io_context io_context_; // 在 P2PNode 类中- 它是 Boost.Asio 的事件循环管理器
- 负责处理所有的异步 I/O 操作
- 管理所有的异步操作队列
- 主要功能:
// 1. 运行事件循环
io_context_.run();
// 2. 停止事件循环
io_context_.stop();
// 3. 重置事件循环
io_context_.reset();- 在 P2P 网络中的使用:
// 1. 创建 socket 时使用
auto socket = std::make_shared<tcp::socket>(io_context_);
// 2. 创建 acceptor 时使用
tcp::acceptor acceptor_(io_context_, tcp::endpoint(...));
// 3. 异步操作时使用
acceptor_.async_accept(
[this](boost::system::error_code ec, tcp::socket socket) {
// 处理新连接
});
// 4. 异步读取时使用
boost::asio::async_read_until(*socket, *buffer, "\n",
[this](boost::system::error_code ec, std::size_t length) {
// 处理读取到的数据
});- 工作流程:
1. 创建 io_context
2. 注册异步操作(如 async_accept, async_read)
3. 调用 io_context.run()
4. io_context 开始事件循环
5. 当异步操作完成时,触发对应的回调函数
- 在 P2PNode 中的具体应用:
class P2PNode {
private:
boost::asio::io_context io_context_;
tcp::acceptor acceptor_;
std::map<std::string, std::shared_ptr<tcp::socket>> connections_;
public:
void start() {
// 开始监听新连接
handleNewConnection();
// 启动事件循环
io_context_.run();
}
void handleNewConnection() {
acceptor_.async_accept(
[this](boost::system::error_code ec, tcp::socket socket) {
if (!ec) {
// 处理新连接
// ...
}
// 继续监听下一个连接
handleNewConnection();
});
}
};- 为什么需要
io_context:
- 提供异步 I/O 操作的支持
- 管理所有异步操作的调度
- 处理网络事件和回调
- 实现非阻塞的网络通信
- 优点:
- 非阻塞:不会阻塞主线程
- 高效:可以处理大量并发连接
- 灵活:支持各种异步操作
- 可扩展:可以添加新的异步操作
- 使用注意事项:
- 需要保持
io_context对象存活 - 需要正确处理异常情况
- 需要适当管理资源
- 需要正确停止和清理
io_context 是 Boost.Asio 异步编程模型的核心,它使得我们的 P2P 网络可以高效地处理多个并发连接,而不会阻塞主线程。这对于构建高性能的网络应用至关重要。
通过入参的本地IP和port初始化了acceptor_,并绑定io_context。这里关键操作是初始化acceptor。而初始化acceptor的过程关键事件是==向操作系统注册了对应端口的监听请求==(笔者注)
acceptor_ 的实际工作原理:
acceptor_本身不是服务,而是一个对象:
tcp::acceptor acceptor_; // 在 P2PNode 类中- 它只是一个用于监听连接请求的对象
- 它本身不包含任何线程
- 它依赖于
io_context_来工作
- 实际的工作流程:
// 1. 初始化 acceptor_
acceptor_(io_context_, tcp::endpoint(boost::asio::ip::make_address(host), port))
// 2. 开始监听
handleNewConnection() {
acceptor_.async_accept(
[this](boost::system::error_code ec, tcp::socket socket) {
// 处理新连接
});
}
// 3. 启动事件循环
io_context_.run();- 真正的监听机制:
- 不是
acceptor_在监听,而是操作系统在监听 acceptor_只是==向操作系统注册了一个监听请求==- 当有新连接时,操作系统通知
io_context_ io_context_触发对应的回调函数
- 具体过程:
操作系统监听端口
↓
有新连接请求
↓
操作系统通知 io_context_
↓
io_context_ 触发 async_accept 的回调
↓
在回调中处理新连接
- 为什么这样设计:
- 避免创建额外的线程
- 利用操作系统的 I/O 多路复用机制
- 更高效地处理并发连接
- 减少资源消耗
- 在代码中的体现:
class P2PNode {
private:
boost::asio::io_context io_context_;
tcp::acceptor acceptor_;
public:
void start() {
// 开始监听
handleNewConnection();
// 启动事件循环
io_context_.run();
}
};所以:
acceptor_不是服务,而是一个对象- 没有专门的监听线程
- 实际的监听工作由操作系统完成
io_context_负责处理事件通知- 整个系统是事件驱动的,而不是线程驱动的
这种设计更高效,因为:
- 避免了线程切换开销
- 利用了操作系统的 I/O 多路复用
- 可以处理大量并发连接
- 资源使用更合理
- 启动一个线程执行消息循环(接受消息)。
注:这个消息循环监控消息队列,循环处理消息队列的消息。但接受到的消息解码后直接调用handleMessage处理掉了。所以消息队列始终为空。(笔者注)
- 调用handleNewConnection:注册了连接建立回调函数,并在连接建立回调函数里注册了消息处理函数(匿名函数)
- 运行IO服务:启动io_context_(注,这里后来启用了单独线程)
start的反操作,停止线程,停止IO服务
让我解释一下这段代码的工作原理:
void P2PNode::handleNewConnection() {
acceptor_.async_accept(
[this](boost::system::error_code ec, tcp::socket socket) {
if (!ec) {
// 处理新连接...
}
handleNewConnection(); // 这里看起来像死循环
});
}这看起来像死循环,但实际上不是。这是 Boost.Asio 的异步编程模式,让我解释一下:
-
async_accept是异步操作:- 它不会阻塞当前线程
- 它注册一个回调函数,当有新连接时才会执行
- 注册完就立即返回
-
回调函数中的
handleNewConnection()调用:- 不是立即执行的
- 它只是注册下一个异步接受操作
- 相当于说:"当这个连接处理完后,准备接受下一个连接"
-
实际执行流程:
1. handleNewConnection() 被调用 2. 注册 async_accept 回调 3. 函数立即返回 4. 当有新连接时,回调函数被触发 5. 处理新连接 6. 在回调中再次调用 handleNewConnection() 7. 重复步骤 2-6 -
为什么这样设计:
- 允许服务器同时处理多个连接
- 不会阻塞主线程
- 可以持续接受新的连接请求
-
停止条件:
- 当调用
acceptor_.close()时 - 当调用
io_context_.stop()时 - 当发生错误时
- 当调用
所以这不是死循环,而是一个持续监听新连接的异步模式。这种模式在服务器编程中很常见,特别是在需要处理多个并发连接的场景中。
是的,您理解得完全正确!让我详细解释一下这个异步读取的流程:
- 注册异步读取:
boost::asio::async_read_until(*connections_[nodeId], *buffer, "\n",
[this, nodeId, buffer](boost::system::error_code ec, std::size_t length) {
// 回调函数
});async_read_until告诉系统:"当收到数据直到遇到换行符 '\n' 时,触发这个回调"- 这是一个异步操作,不会阻塞当前线程
- 回调函数会在数据到达时自动触发
- 回调函数的工作流程:
[this, nodeId, buffer](boost::system::error_code ec, std::size_t length) {
if (!ec) {
// 1. 从缓冲区提取数据
std::string data(boost::asio::buffers_begin(buffer->data()),
boost::asio::buffers_begin(buffer->data()) + length);
// 2. 消费已读取的数据
buffer->consume(length);
// 3. 反序列化消息
Message message = deserializeMessage(data);
// 4. 处理消息
handleMessage(message, nodeId);
}
}- 整个消息处理流程:
新连接建立
↓
注册异步读取回调
↓
等待数据到达
↓
收到数据(直到换行符)
↓
触发回调函数
↓
处理消息
- 为什么使用
async_read_until:
- 异步操作,不会阻塞
- 自动处理数据缓冲
- 可以处理任意长度的消息
- 使用换行符作为消息分隔符
- 消息的格式:
// 在 serializeMessage 函数中
std::string P2PNode::serializeMessage(const Message& message) {
json j;
j["type"] = static_cast<int>(message.type);
j["data"] = message.data;
j["sender"] = message.sender;
j["signature"] = message.signature;
return j.dump() + "\n"; // 注意这里添加了换行符
}所以,这个异步读取机制使得:
- 每个连接都可以独立接收消息
- 不会阻塞其他连接的消息处理
- 可以同时处理多个连接的消息
- 消息处理是自动触发的
这是一个典型的异步事件驱动模型,非常适合处理网络通信。
让我详细解释一下 handleMessage 函数,这是 P2P 网络中的消息处理核心:
case MessageType::HANDSHAKE:
std::cout << "Received handshake from: " << sender << std::endl;
break;- 当新节点连接时,首先进行握手
- 确认连接建立成功
case MessageType::NEW_BLOCK: {
json blockData = json::parse(message.data);
Block newBlock(blockData);
blockchain_->addBlock(newBlock.getTransactions());
broadcast(message); // 广播给其他节点
break;
}- 收到新区块时,将其添加到区块链
- 然后广播给其他节点,保持网络同步
case MessageType::NEW_TRANSACTION: {
json txData = json::parse(message.data);
Transaction newTx(txData);
if (blockchain_->addTransactionToPool(newTx)) {
broadcast(message); // 广播给其他节点
}
break;
}- 收到新交易时,先验证并添加到交易池
- 如果添加成功,广播给其他节点
case MessageType::GET_BLOCKS: {
json request = json::parse(message.data);
int startHeight = request["start_height"];
std::vector<Block> blocks = blockchain_->getBlocksFromHeight(startHeight);
Message response;
response.type = MessageType::BLOCKS;
// 将区块列表转换为 JSON 并发送
// ...
sendToNode(sender, response);
break;
}- 当节点需要同步区块时,发送请求
- 收到请求的节点返回从指定高度开始的所有区块
case MessageType::BLOCKS: {
json blocksData = json::parse(message.data);
for (const auto& blockData : blocksData) {
Block block(blockData);
blockchain_->addBlock(block.getTransactions());
}
break;
}- 接收并处理其他节点发送的区块数据
- 将区块添加到本地区块链
####### 节点发现机制:
case MessageType::GET_PEERS: {
// 发送已知节点列表
Message response;
response.type = MessageType::PEERS;
response.data = json(getConnectedNodes()).dump();
sendToNode(sender, response);
break;
}
case MessageType::PEERS: {
// 处理接收到的节点列表
json peers = json::parse(message.data);
for (const auto& peer : peers) {
std::string host = peer["host"];
int port = peer["port"];
connect(host, port); // 连接到新发现的节点
}
break;
}- 节点可以请求获取其他已知节点的列表
- 收到节点列表后,尝试连接到新节点
- 这实现了 P2P 网络的自动发现和扩展
这个处理机制实现了:
- 区块链数据的同步
- 交易的传播
- 节点发现和连接
- 网络状态的维护
整个系统通过这些消息类型和处理逻辑,实现了去中心化的 P2P 网络通信。
connect 函数是节点发现机制的实现,这是 P2P 网络中建立节点连接的关键函数:
- 创建新的 socket 连接:
auto socket = std::make_shared<tcp::socket>(io_context_);
socket->connect(tcp::endpoint(boost::asio::ip::make_address(host), port));- 创建一个新的 TCP socket
- 使用
make_shared进行智能指针管理 - 尝试连接到指定的主机和端口
- 保存连接信息:
std::string nodeId = host + ":" + std::to_string(port);
connections_[nodeId] = socket;- 生成节点标识符(格式:host:port)
- 将 socket 保存在连接映射表中
- 这样后续可以通过 nodeId 找到对应的连接
- 发送握手消息:
Message handshake;
handshake.type = MessageType::HANDSHAKE;
handshake.sender = host_ + ":" + std::to_string(port_);
sendToNode(nodeId, handshake);- 创建握手消息
- 设置消息类型为 HANDSHAKE
- 在消息中包含发送者信息
- 发送给新连接的节点
- 错误处理:
try {
// 连接和握手过程
} catch (const std::exception& e) {
std::cerr << "Failed to connect to node: " << e.what() << std::endl;
}- 使用 try-catch 捕获可能的连接错误
- 如果连接失败,输出错误信息
整个连接流程:
1. 创建新的 socket
2. 尝试连接到目标节点
3. 保存连接信息
4. 发送握手消息
5. 等待对方响应
这个函数在以下情况下被调用:
- 节点主动连接到其他节点
- 收到 PEERS 消息时,连接到新发现的节点
- 节点启动时,连接到已知的节点
连接建立后:
- 双方可以开始交换消息
- 可以请求同步区块数据
- 可以广播新的交易和区块
- 可以交换节点信息
这是 P2P 网络中最基本的连接建立机制,确保了节点之间可以建立可靠的通信通道。
p2p_node包含了一个Blockchain成员,其所有对区块链的操作都通过该成员完成
std::shared_ptr<Blockchain> blockchain_; struct NodeState {
int height;
int difficulty;
std::string version;
std::string lastBlockHash;
};
NodeState node_state_;
std::mutex node_state_mutex_;- 更新状态功能
更新节点状态是在收到同步响应(SYNC_RESPONSE)时,处理完成各数据同步(块,UTXO,待处理交易)后调用状态更新(同步消息包含状态信息)
这个状态其实包含上述节点状态的信息(理论上讲,这些信息是不是包含在Chain内更合理)
节点收到新块同步消息时,
-
通知Chain==加入一个新块==
-
新块包含消息中携带的快的交易信息
-
新块不要添加当前链上未完成交易
-
-
广播消息(发送方和接收方节点列表不一致才有用)
笔者这里有个疑问:
- 添加的新块怎么保证当前链是新的,是不是应该检查消息携带的块是否正好是当前链下一个链
节点收到新交易同步消息时,
- 通知Chain==加入一个新交易==(chain会校验)
- 广播消息
节点收到获取本地块信息时
- 从Chain获取指定高度开始的块
- 遍历序列化块并加入Json数组
- 发送给请求方
这个消息是获取块信息的响应,其和新块同步的差别是新块同步只带了一个块,而此消息可能带多个块。所以处理流程也是类似,重复多次添加块,但不回响应也不广播。
广播UTXO请求(指定地址)
这里将过程封装成函数,因为有可能节点会主动调用。
- 解析请求中携带的地址信息
- 通过chain查询本地该地址的所有UTXO(通过UTXO池)
- 构造响应,向请求方发送
解析UTXO,这里消息携带了UTXO的拥有者,以及UTXO列表。
这是这个版本新增的UTXO缓存功能。之前旧的UTXO池是根据交易索引存储的。笔者还需要研究一下哪种方式合理。
注:Chain仅仅完全更新了该拥有者的UTXO列表
广播余额请求(指定地址)
这里将过程封装成函数,因为有可能节点会主动调用。
- 解析请求中携带的地址信息
- 通过chain查询本地该地址的余额
- 构造响应,向请求方发送
类似UTXO,这里指定地址的余额。这也是这个版本新增余额缓存功能。
注:Chain仅仅完全更新了该地址的余额。
广播同步请求
同步请求请求本节点的区块链信息,包括
- 起始块
- 是否包括UTXO集合
- 是否包含未处理的交易
节点收到此消息获取Chain上信息构造响应
- 块信息(从指定块开始)
- utxo信息(如果需要)
- 未完成交易(如果需要)
- 节点状态(奇怪,为什么是从Chain获取,而不是本地获取)
- chain的size
- chain的难度
- 版本默认1.0
- 最后一个块的Hash
这个函数字面上看是处理同步请求的,但消息循环里没有使用,而是直接实现了一套。并且比这里完善。 这里仅仅获取了本地指定块开始的链,并响应!==最后将消息循环里实现统一到这里,并在消息循环调用==
节点发起同步请求得到的对应节点的响应
- 创建一系列新块(响应中块的交易信息),这一步等同于发起了一个GET_BLOCKS得到的响应
- 处理UTXO数据,这一步等同发起了一个GET_UTXOS得到的响应(==本版本没实现==)
- 处理未处理交易,添加携带的所有未处理交易(==注意:此版本没有主动发起未完成交易请求==)
- 更新节点状态(见,节点状态一节,这里保存来干什么呢)
广播挖矿请求(==仅携带了交易==)
这里将过程封装成函数。
- 解析请求中携带的交易列表
- 通过chain==添加一个新块,携带交易列表==(这里为什么要用本地未完成的交易)
- 获取本地新创建的块
- 使用新块构造响应MINING_RESPONSE,发送向请求方
节点收到一个挖矿响应时,解析消息
- 解析响应内包含的块信息
- 是否成功
如果成功
- 通过消息中携带的“block”字段生成一个新块
- 通过本地chain校验新块(见1.8.3.1)(==这时候验证了HASH值==)
- 在本地链==添加新块==,使用块内携带的交易(==这个时候重新挖矿计算了HASH值==)
- 把新块用NEW_BLOCK广播出去
共识投票是发起了指定块Hash值的投票。(逻辑待定)
获取共识投票消息后,在本地consensus_votes_中记录该块的投票信息
- 检查是否存在对该块的投票,如果没有则创建初始为0的投票项
- 根据是赞成还是反对,对该项的该投票项计数加一
- 检查是否达到共识
- 统计投票数(赞成和反对的和)
- 获取最小投票节点(所有连接的51%)
- 如果投票数达到最小投票数,则多数获胜
- 达成共识则广播共识结果(块Hash,赞成或反对)
广播消息,没啥好说的
收到共识结果,更新本地链和广播新块
- 如果共识达成
- 在本地通过该块的Hash查找块
- 如果找到则使用该块的交易==添加一个新块==(==这里有个矛盾,已有快的情况下添加一个新块什么意思==)
- 广播新块
- 清理本地共识投票consensus_votes_的对应项
先把3个线程改成3个进程跑起来吧
主进程主要完成子进程的创建后,等待用户输入exit后退出
您提出了一个很好的问题。在Windows系统中,当我们调用CloseHandle(pi.hProcess)时,确实会关闭进程句柄,但这并不会影响子进程的运行。让我解释一下原因:
-
句柄与进程的关系
- 句柄(Handle)只是操作系统提供的一个引用,用于访问进程对象
- 关闭句柄只是释放了这个引用,并不会终止进程本身
- 进程会继续运行,直到它自己结束或被其他方式终止
-
Windows进程管理机制
- Windows使用引用计数来管理进程对象
- 当进程创建时,引用计数为1
- 每次打开进程句柄,引用计数+1
- 每次关闭句柄,引用计数-1
- 只有当引用计数为0时,系统才会清理进程对象
-
当前代码的情况
CloseHandle(pi.hProcess); CloseHandle(pi.hThread);
- 这里关闭的是创建进程时获得的句柄
- 子进程仍然在运行
- 如果之后需要跟踪子进程,可以重新打开进程句柄
-
建议的改进 如果您需要跟踪子进程,可以:
// 保存进程ID DWORD processId = pi.dwProcessId; // 之后如果需要重新获取句柄 HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processId); if (hProcess != NULL) { // 使用句柄进行操作 CloseHandle(hProcess); }
-
最佳实践
- 如果不需要跟踪子进程,当前的实现是合适的
- 如果需要跟踪,建议:
- 保存进程ID
- 在需要时重新打开句柄
- 使用完后及时关闭句柄
- 考虑使用进程间通信机制(如命名管道、共享内存等)来与子进程通信
您是否需要我帮您实现进程跟踪的功能,或者有其他关于进程管理的问题?
这是一个很好的问题。在Windows系统中,当主进程退出时,子进程也会退出的原因主要有以下几点:
-
Windows进程树机制
- Windows默认会将子进程作为主进程的子进程来管理
- 当父进程退出时,系统会默认终止所有子进程
- 这是Windows的一个安全机制,防止出现"孤儿进程"
-
解决方案 要让子进程在主进程退出后继续运行,有以下几种方法:
a. 使用
CREATE_NEW_PROCESS_GROUP标志STARTUPINFO si = {sizeof(si)}; PROCESS_INFORMATION pi; // 创建进程时指定标志 CreateProcess( NULL, // 应用程序名 commandLine, // 命令行 NULL, // 进程安全属性 NULL, // 线程安全属性 FALSE, // 继承句柄 CREATE_NEW_PROCESS_GROUP, // 创建新的进程组 NULL, // 环境变量 NULL, // 当前目录 &si, // 启动信息 &pi // 进程信息 );b. 使用
DETACHED_PROCESS标志CreateProcess( NULL, commandLine, NULL, NULL, FALSE, DETACHED_PROCESS, // 创建独立进程 NULL, NULL, &si, &pi );
-
最佳实践建议
- 如果子进程需要独立运行,建议使用
DETACHED_PROCESS标志 - 如果子进程需要与主进程保持某种关联,可以使用
CREATE_NEW_PROCESS_GROUP - 在创建进程后,仍然需要关闭句柄:
CloseHandle(pi.hProcess); CloseHandle(pi.hThread);
- 如果子进程需要独立运行,建议使用
-
注意事项
- 使用
DETACHED_PROCESS后,子进程将完全独立运行 - 主进程将无法直接控制子进程的生命周期
- 如果需要与子进程通信,需要建立其他通信机制(如命名管道、共享内存等)
- 使用
您想要我帮您修改代码,使子进程能够独立运行吗?或者您有其他关于进程管理的问题?
子进程启动时附加了2个命令行参数,即其监听的IP和port。然后运行runNode方法。
- 创建一个BlockChain
- 创建一个P2PNode节点,入参为上述blockChain,和监听的IP和port
- Start该P2PNode节点
- 监听其他节点的连接建立请求
- 启动消息循环处理可能来自其他节点的消息
- 监听命令行输入获取用户操作
创世区块(0号块)的创建
- 创世区块的创建
std::shared_ptr<Block> Blockchain::createGenesisBlock() {
std::vector<Transaction> genesisTransactions; // 空交易列表
return std::make_shared<Block>(0, genesisTransactions, "0");
}- Merkle树的构建
Block::Block(int index, const std::vector<Transaction>& transactions, const std::string& previousHash) {
// ...
MerkleTree merkleTree(transactions_); // 空交易列表
merkleRoot_ = merkleTree.getRootHash();
// ...
}- 为什么根节点相同
- 所有创世区块都使用相同的参数:
index = 0transactions = 空列表previousHash = "0"
- 由于交易列表为空,Merkle树也是空的
- 空Merkle树的根节点哈希值总是相同的
- 这是设计上的问题
- 创世区块应该包含一些特殊的初始化交易
- 每个节点的创世区块可以包含不同的初始化数据
- 这样可以确保每个节点的创世区块都是唯一的
- 改进建议
std::shared_ptr<Block> Blockchain::createGenesisBlock() {
std::vector<Transaction> genesisTransactions;
// 添加创世交易
Transaction genesisTx(
"SYSTEM", // 从系统
"GENESIS", // 到创世地址
1000000 // 初始金额
);
genesisTx.setSignature("GENESIS_SIGNATURE");
genesisTransactions.push_back(genesisTx);
// 添加时间戳或其他唯一标识
std::string uniqueId = std::to_string(std::time(nullptr));
return std::make_shared<Block>(0, genesisTransactions, uniqueId);
}- 为什么需要改进
- 当前实现中,所有节点的创世区块完全相同
- 这可能导致网络同步问题
- 无法区分不同节点的创世区块
- 缺乏初始化数据
基本命令结构
std::cout << "\nAvailable commands:" << std::endl;
std::cout << " connect <host> <port> - Connect to a node" << std::endl;
std::cout << " mine - Mine a new block" << std::endl;
std::cout << " balance <address> - Check balance" << std::endl;
std::cout << " send <from> <to> <amount> - Send transaction" << std::endl;
std::cout << " peers - List connected peers" << std::endl;
std::cout << " chain - Show blockchain" << std::endl;
std::cout << " exit - Stop the node" << std::endl;命令功能详解
if (cmd == "connect") {
std::string peerHost;
int peerPort;
iss >> peerHost >> peerPort;
node.connect(peerHost, peerPort);
}- 目的:建立与其他节点的P2P连接
- 功能:创建TCP连接并发送握手消息
- 设计:支持动态发现和连接新节点
else if (cmd == "peers") {
auto peers = node.getConnectedNodes();
}- 目的:查看P2P网络连接状态
- 功能:显示已连接的节点
- 设计:支持网络拓扑管理
else if (cmd == "chain") {
const auto& chain = blockchain->getChain();
}- 目的:查看区块链状态
- 功能:显示所有区块信息
- 设计:支持分布式账本查看
else if (cmd == "balance") {
std::string address;
iss >> address;
double balance = blockchain->getBalance(address);
}- 目的:查询账户余额
- 功能:计算UTXO总和
- 设计:支持分布式账本查询
else if (cmd == "send") {
std::string from, to;
double amount;
iss >> from >> to >> amount;
Transaction tx(from, to, amount);
tx.setSignature(wallet->sign(tx.toJson()));
blockchain->addTransactionToPool(tx);
// 广播交易
Message msg;
msg.type = MessageType::NEW_TRANSACTION;
msg.data = tx.toJson();
node.broadcast(msg);
}- 目的:创建和广播交易
- 功能:签名交易并加入交易池
- 设计:支持分布式交易处理
else if (cmd == "mine") {
auto pendingTxs = blockchain->getPendingTransactions();
blockchain->addBlock(pendingTxs);
// 广播新区块
Message msg;
msg.type = MessageType::NEW_BLOCK;
msg.data = blockchain->getChain().back()->toJson();
node.broadcast(msg);
}- 目的:创建新区块并广播
- 功能:处理待处理交易,计算工作量证明
- 设计:支持分布式共识机制
-
设计目的
a. 分布式架构
- 每个节点都是独立的进程
- 支持P2P网络通信
- 实现去中心化架构
b. 安全性
- 交易签名验证
- 工作量证明机制
- 分布式共识
c. 可扩展性
- 支持动态节点加入
- 支持交易广播
- 支持区块同步
d. 用户交互
- 简单的命令行界面
- 清晰的命令结构
- 实时的状态反馈
-
消息处理机制
- 异步消息处理
- 消息队列管理
- 广播机制
这些命令和功能共同构成了一个基本的区块链节点,支持:
- 分布式网络通信
- 交易处理和验证
- 区块创建和同步
- 账户余额管理
- 网络状态监控
您是否需要我详细解释某个具体的命令实现,或者有其他关于节点功能的问题?
当前的 P2P 网络实现已经包含了一些基本的区块链业务操作,但还可以进一步丰富。让我来添加一些常见的区块链网络操作:
HANDSHAKE, // 握手消息
NEW_BLOCK, // 新区块
NEW_TRANSACTION, // 新交易
GET_BLOCKS, // 请求区块
BLOCKS, // 区块数据
GET_PEERS, // 请求节点列表
PEERS, // 节点列表
GET_UTXOS, // 请求UTXO数据
UTXOS, // UTXO数据
GET_BALANCE, // 请求余额
BALANCE, // 余额数据
SYNC_REQUEST, // 同步请求
SYNC_RESPONSE, // 同步响应
MINING_REQUEST, // 挖矿请求
MINING_RESPONSE, // 挖矿响应
CONSENSUS_VOTE, // 共识投票
CONSENSUS_RESULT // 共识结果
握手消息其实是某个节点上,用户主动发起向另外一个已知节点的连接后发送的。
- 链接建立:在本地创建也个socket,并构造对端IP:port信息发起一个连接建立请求。本保存此链接信息到本地connections_。
- 链接接受:如上文,Node在初始化时,注册了连接建立请求消息的回调,会保存一个对端IP:port和socket到connections_。
- 握手消息处理:仅显示打印收到握手消息。
新交易消息是由某个节点上用户通过命令交易发送触发:
- send命令携带from,to,amount发起一次交易
- 在本地链找到发起者的钱包,如果找不到则失败(==所以本例中所有节点都没有钱包,所以不可能成功==)
- 创建交易(初始化,并计算hash)
- 使用发起者钱包对交易签名(注意,签名改成对交易的Json串签名,不知道什么时候改的)
- 把交易添加到本地链
- 把交易广播到已知其他节点
收到新交易
- Json解析交易
- 通过Json串构造交易
- 将交易加入本次链(会检测是否存在)
新块消息是由某个节点上用户通过命令mine触发:
- 获取本地链上所有未完成交易,如果没有则失败(==这里获取了一次未完成交易==)
- 在本地创建新块
- 根据所有未完成交易(==这里再次获取了一次未完成交易==),链深度,前一个块HASH值,MerkleTree,计算Hash值
- 挖矿(反复重新计算Hash值)
- 加入本地链
- 更新UTXOPool(本地链)(==由于本例中没有根据UTXO更新交易的输入输出,所以无操作==)
- 从本地链移走未完成交易
- 获取最新块,并广播
而收到新块的节点
- Json解析块
- 根据Json串生成新块
- 提取新块里的交易(==注意,这些交易已经在原来节点完成了<更新过UTXO,签名包含了这些信息>==)
- 在根据这些交易,在本地链创建一个块
- 继续广播这个消息(排除发送方)
GET_PEERS是一个 查询指定节点连接信息,猜测应该是用于路由扩张。收到此请求后,获取当前节点保存到连接信息,封装为PEERS消息回应给请求方
GET_PEERS的响应,收到此消息后,遍历所有节点信息,并在本地创建一个连接,但这里有2个问题:
- PEERS节点列表中可能是本地已经存在的节点
- 各节点保存的连接信息里,port号有可能是第三方节点主动发起链接建立时新生成的port号,所以未必可以在该port上接受连接建立请求
GET_BLOCKS是一个 查询指定节点指定深度的块信息,猜测应该是块同步。收到此请求后,获取当前节点链上指定的块,封装为BLOCKS消息回应给请求方
同NEW_BLOCK,只是此次是批量创建
节点模块提供了requestUTXOs接口,向网络广播一个指定钱包的UTXO请求信息。
接收到该消息的节点,查询本地链上的UTXO池,获取指定钱包的UTXO信息。并构建UTXOS消息响应给发送方。
UTXO查询功能
requestUTXOs: 请求特定地址的UTXO数据handleUTXOsRequest: 处理UTXO请求并返回相关数据
==本用例未提供处理参考==
节点模块提供了requestBalance接口,向网络广播一个指定钱包的余额请求信息。
接收到该消息的节点,查询本地链上的UTXO池,获取指定钱包的余额信息。并构建BALANCE消息响应给发送方。
余额查询功能
requestBalance: 请求特定地址的余额handleBalanceRequest: 处理余额请求并返回结果
==本用例未提供处理参考==
新增的区块链网络操作方法的主要
节点模块提供了requestSync接口,向网络广播一个同步请求信息。
接收到该消息的节点,查询本地链上的Bolck,获取指定深度的块。并构建SYNC_RESPONSE消息响应给发送方。(===所以和GET_BLOCKS有什么区别==)
区块链同步功能
requestSync: 请求从特定高度开始的区块数据handleSyncRequest: 处理同步请求并返回相关区块
==本用例未提供处理参考==,但其实应该和BLOCKS一样
节点模块提供了requestMining接口,向网络广播一个挖矿请求信息,该消息携带了一系列交易。
接收到该消息的节点
- 解析JSON交易信息,并构造交易
- 创建一个新块,但指定使用本地链上未完成交易(==所以传过来的交易由什么用==)
- 获取链上最新块,并封装为MINING_RESPONSE结果信息响应给发送方
挖矿功能
requestMining: 请求创建包含特定交易的新区块handleMiningRequest: 处理挖矿请求并返回新创建的区块
==本用例未提供处理参考==,但其实应该和BLOCKS一样
节点模块提供了broadcastConsensusVote接口,向网络广播一个对指定块投票的请求信息,该消息携带了一个块HASH。
接收到该消息的节点解析块信息后,处理共识投票,==本用例未提供处理参考==
- 共识机制
broadcastConsensusVote: 广播对特定区块的投票broadcastConsensusResult: 广播共识结果handleConsensusVote: 处理共识投票handleConsensusResult: 处理共识结果
共识机制的具体实现(handleConsensusVote和handleConsensusResult)目前只有框架,需要根据您选择的共识算法(如PoW、PoS等)来完善实现。
还有UTXOS,BALANCE, SYNC_RESPONSE, MINING_RESPONSE, CONSENSUS_VOTE未提供处理参考。
并且SYNC_REQUEST和GET_BLOCKS有什么区别
是的,您说得对。当前的实现确实存在广播风暴的问题。让我们分析一下当前的广播逻辑:
void P2PNode::broadcastMessage(const Message& message) {
for (const auto& node : connected_nodes_) {
sendMessage(node, message);
}
}当节点A收到新交易后:
- 节点A广播给所有连接的节点(包括节点B)
- 节点B收到后,又广播给所有连接的节点(包括节点A)
- 这样就形成了无限循环的广播
让我们修改代码来解决这个问题:
// 在 p2p_node.h 中添加
class P2PNode {
private:
// 添加消息去重集合
std::unordered_set<std::string> processed_messages_;
std::mutex processed_messages_mutex_;
// 添加消息ID生成方法
std::string generateMessageId(const Message& message);
// 修改广播方法
void broadcastMessage(const Message& message, const std::string& exclude_node = "");
};
// 在 p2p_node.cpp 中实现
std::string P2PNode::generateMessageId(const Message& message) {
// 根据消息内容生成唯一ID
return message.type + "_" + message.data;
}
void P2PNode::broadcastMessage(const Message& message, const std::string& exclude_node) {
std::string message_id = generateMessageId(message);
// 检查是否已经处理过该消息
{
std::lock_guard<std::mutex> lock(processed_messages_mutex_);
if (processed_messages_.find(message_id) != processed_messages_.end()) {
return; // 已经处理过,不再广播
}
processed_messages_.insert(message_id);
}
// 广播给除发送节点外的所有节点
for (const auto& node : connected_nodes_) {
if (node != exclude_node) {
sendMessage(node, message);
}
}
}
void P2PNode::handleMessage(const std::string& from, const Message& message) {
switch (message.type) {
case MessageType::NEW_TRANSACTION: {
// 将交易添加到待处理队列
Transaction tx;
if (tx.fromJson(message.data)) {
blockchain_.addTransaction(tx);
// 广播给其他节点,但排除发送节点
broadcastMessage(message, from);
}
break;
}
case MessageType::NEW_BLOCK: {
// 处理新区块
Block block;
if (block.fromJson(message.data)) {
if (blockchain_.addBlock(block)) {
// 广播给其他节点,但排除发送节点
broadcastMessage(message, from);
}
}
break;
}
// ... 其他消息处理 ...
}
}
// 添加消息清理机制,防止内存无限增长
void P2PNode::cleanupProcessedMessages() {
std::lock_guard<std::mutex> lock(processed_messages_mutex_);
// 保留最近1000条消息记录
if (processed_messages_.size() > 1000) {
processed_messages_.clear();
}
}这些修改的主要改进点:
-
消息去重:
- 使用
processed_messages_集合记录已处理的消息 - 每个消息生成唯一ID
- 避免重复处理相同的消息
- 使用
-
定向广播:
- 广播时排除消息的发送节点
- 防止消息回传给发送者
-
内存管理:
- 添加消息清理机制
- 防止
processed_messages_集合无限增长
-
线程安全:
- 使用互斥锁保护共享资源
- 确保多线程环境下的安全
这样修改后:
- 每个消息只会被处理一次
- 广播不会形成循环
- 消息传播更加高效
- 系统资源使用更加合理
您觉得这些修改合适吗?需要我帮您实现这些更改吗?
您说得对,这确实是一个问题。让我们分析一下当前的代码:
- 在
handleMessage中处理NEW_BLOCK消息时:
case MessageType::NEW_BLOCK: {
json blockData = json::parse(message.data);
Block newBlock(blockData);
blockchain_->addBlock(newBlock.getTransactions()); // 这里添加了一次交易
broadcastMessage(message, sender);
break;
}- 在
Blockchain::addBlock中:
void Blockchain::addBlock(const std::vector<Transaction>& transactions) {
// 获取待处理交易
auto pendingTxs = getPendingTransactions(); // 这里又获取了一次交易
// ... 创建新区块 ...
}这确实会导致交易被重复添加。让我们修改代码来解决这个问题:
- 修改
Blockchain::addBlock方法,添加一个参数来控制是否使用待处理交易:
// 在 blockchain.h 中
class Blockchain {
public:
// 添加新方法
void addBlock(const std::vector<Transaction>& transactions, bool usePendingTxs = true);
// 保留原有方法以保持兼容性
void addBlock(const std::vector<Transaction>& transactions) {
addBlock(transactions, true);
}
};
// 在 blockchain.cpp 中
void Blockchain::addBlock(const std::vector<Transaction>& transactions, bool usePendingTxs) {
std::vector<Transaction> blockTransactions;
if (usePendingTxs) {
// 使用链上的待处理交易
blockTransactions = getPendingTransactions();
} else {
// 使用传入的交易
blockTransactions = transactions;
}
// 创建新区块
auto newBlock = std::make_shared<Block>(
chain_.size(),
blockTransactions,
chain_.empty() ? "0" : chain_.back()->getHash()
);
// 添加区块
chain_.push_back(newBlock);
// 清理已处理的交易
if (usePendingTxs) {
clearPendingTransactions();
}
}- 修改
handleMessage中的NEW_BLOCK处理:
case MessageType::NEW_BLOCK: {
// 处理新区块
std::cout << " " << host_ << ":" << port_ << " Received new block from: " << sender << std::endl;
json blockData = json::parse(message.data);
Block newBlock(blockData);
// 使用传入的交易,不使用待处理交易
blockchain_->addBlock(newBlock.getTransactions(), false);
broadcastMessage(message, sender);
break;
}- 修改挖矿相关的代码:
// 在 main.cpp 中
else if (cmd == "mine") {
// 获取待处理交易
auto pendingTxs = blockchain->getPendingTransactions();
if (pendingTxs.empty()) {
std::cout << "No pending transactions to mine" << std::endl;
continue;
}
// 创建新区块,使用待处理交易
blockchain->addBlock(pendingTxs, true);
std::cout << "New block mined" << std::endl;
// 广播新区块
Message msg;
msg.type = MessageType::NEW_BLOCK;
msg.data = blockchain->getChain().back()->toJson();
node.broadcast(msg);
}这样修改后:
- 当节点自己挖矿时,使用链上的待处理交易
- 当收到其他节点的区块时,使用区块中携带的交易
- 避免了交易重复添加的问题
- 保持了区块链的一致性
您说得对,这两个问题确实需要解决。让我们修改代码:
- 首先修改
PEERS消息的处理逻辑,避免重复连接:
case MessageType::PEERS: {
// 处理接收到的节点列表
std::cout << " " << host_ << ":" << port_ << " Received peers from: " << sender << std::endl;
json peers = json::parse(message.data);
// 获取当前已连接的节点列表
auto currentNodes = getConnectedNodes();
std::set<std::string> currentNodeSet(currentNodes.begin(), currentNodes.end());
// 遍历新收到的节点列表
for (const auto& peer : peers) {
std::string host = peer["host"];
int port = peer["port"];
std::string nodeId = host + ":" + std::to_string(port);
// 检查是否是本地节点
if (host == host_ && port == port_) {
continue; // 跳过本地节点
}
// 检查是否已经连接
if (currentNodeSet.find(nodeId) != currentNodeSet.end()) {
continue; // 跳过已连接的节点
}
// 尝试连接新节点
try {
connect(host, port);
} catch (const std::exception& e) {
std::cerr << "Failed to connect to peer " << nodeId << ": " << e.what() << std::endl;
}
}
break;
}- 修改
GET_PEERS的响应,只返回可连接的节点信息:
case MessageType::GET_PEERS: {
// 发送已知节点列表
std::cout << " " << host_ << ":" << port_ << " Sending peers to: " << sender << std::endl;
Message response;
response.type = MessageType::PEERS;
// 构建可连接的节点列表
json peersArray = json::array();
for (const auto& [nodeId, socket] : connections_) {
// 解析节点ID
size_t colonPos = nodeId.find(':');
if (colonPos == std::string::npos) continue;
std::string host = nodeId.substr(0, colonPos);
int port = std::stoi(nodeId.substr(colonPos + 1));
// 检查是否是本地节点
if (host == host_ && port == port_) {
continue;
}
// 检查是否是主动连接的节点
if (socket->remote_endpoint().port() != port) {
continue; // 跳过使用临时端口的连接
}
// 添加到可连接节点列表
json peerInfo = {
{"host", host},
{"port", port}
};
peersArray.push_back(peerInfo);
}
response.data = peersArray.dump();
sendToNode(sender, response);
break;
}- 修改
connect方法,添加连接状态检查:
void P2PNode::connect(const std::string& host, int port) {
try {
// 检查是否已经连接
std::string nodeId = host + ":" + std::to_string(port);
if (connections_.find(nodeId) != connections_.end()) {
std::cout << "Already connected to node: " << nodeId << std::endl;
return;
}
// 检查是否是本地节点
if (host == host_ && port == port_) {
std::cout << "Cannot connect to self" << std::endl;
return;
}
// 创建socket
auto socket = std::make_shared<tcp::socket>(io_context_);
// 连接到目标节点
socket->connect(tcp::endpoint(boost::asio::ip::make_address(host), port));
// 保存连接信息
connections_[nodeId] = socket;
// 发送握手消息
Message handshake;
handshake.type = MessageType::HANDSHAKE;
handshake.sender = host_ + ":" + std::to_string(port_);
sendToNode(nodeId, handshake);
std::cout << "Connected to node: " << nodeId << std::endl;
} catch (const std::exception& e) {
std::cerr << "Failed to connect to node: " << e.what() << std::endl;
}
}这些修改解决了以下问题:
- 避免重复连接已存在的节点
- 避免连接本地节点
- 只共享可连接的节点信息(使用固定端口的节点)
- 添加了更多的错误处理和日志记录
主要改进:
- 使用
set来高效检查节点是否已连接 - 在共享节点信息时过滤掉使用临时端口的连接
- 在连接前进行多重检查
- 添加了更详细的日志输出
-
让我详细解释一下这个共识机制的工作流程:
- 共识发起:
- 当节点收到新的区块时,会发起共识
- 发起者会广播
CONSENSUS_VOTE消息给所有节点 - 消息包含:区块哈希、完整区块内容、投票者ID,投票决定
- 投票过程:
- 所有节点都会收到投票请求
- 每个节点需要验证区块的有效性
- 节点根据验证结果投赞成票或反对票
- 投票信息通过
CONSENSUS_VOTE消息广播给所有节点
- 投票检查:
当收到投票消息时
- 如果本节点已经处理过该节点的投票,丢弃该消息,否则计入已投票节点
- 如果本节点已经给该块投过票了(不同节点广播的投票),跳过投票阶段
- 验证节点,如果通过则投赞成票,否则投反对票。记录该块当前节点已投票
- 更新投票记录
- 每次更新后检查是否达到共识阈值
- 共识达成:
- 当赞成票比例超过阈值(比如2/3)时,认为达成共识
- 达成共识的节点会广播
CONSENSUS_RESULT消息 - 消息包含:区块哈希、共识结果(通过/拒绝),区块信息,投票者ID(==怎么用==)
- 共识结果处理:
- 所有节点收到共识结果后,根据结果决定是否将区块添加到链中
- 如果共识通过,区块被添加到链中
- 如果共识拒绝,区块被丢弃
- 防止重复:
-
每个节点都维护
- 个投票记录表
consensus_votes_,记录每个区块的赞成票和反对票数量 - 个是否投票表voted_blocks_,记录每个块当前节点是否已经投票_
- 一个投票节点表voted_nodes_,记录每个块已投票节点记录
- 个投票记录表
-
每个节点维护
voted_blocks_记录已投票的区块 -
防止对同一个区块重复投票
-
确保共识过程的完整性
这个机制的特点:
- 去中心化:所有节点都可以发起共识和投票
- 透明性:所有投票都是公开的
- 安全性:需要多数节点同意才能达成共识
- 容错性:可以容忍部分节点故障或恶意行为
std::mutex consensus_mutex_;
std::map<std::string, std::pair<int, int>> consensus_votes_; // blockHash -> (赞成票数, 反对票数)
std::map<std::string, bool> voted_blocks_; // blockHash -> 是否已投票
std::map<std::string, std::set<std::string>> voted_nodes_; // blockHash -> 已投票的节点列表 节点收到新块添加NEW_BLOCK时发起投票
-
解析新块
-
==(0525补充)检查块是否已经在链上了,如果是则丢弃==
-
检查该块是否正在投票,如果是则丢弃该消息
-
本地验证区块,如果失败则丢弃
-
检查块是否已经在链上(==如果在链上上一步应该验证失败==)
-
广播共识投票(包括自己?==修改broadcastConsensusVote==)
节点收到CONSENSUS_VOTE消息,开始处理投票
-
==(0525补充)检查块是否已经在链上了,如果是则丢弃==(防止已经完成节点添加的节点收到其他节点因为网络延迟还在发送的投票消息)
-
创建一个块对象
-
验证此投票是否合法(消息携带的块HASH和新块的HASH是否一致)
-
检查==发送者==是否已经对此块投过票,避免重复投票
- 如果是直接返回
- 如果不是则将发送节点添加进本地该块的已投票节点(下次收到该节点对此块的投票丢弃)
-
检查本地是否已经投过票(如果本地已经投过则仅记录本次投票信息)
- 如果未投票,开始投票
- 验证块哈希(索引号,前一块的Hash值,块Hash验证,块难度,验证交易,验证MerkleRoot)
- 成功投赞成票
- 失败投反对票
- 标记本节点已经对该块发起过投票
- 验证块哈希(索引号,前一块的Hash值,块Hash验证,块难度,验证交易,验证MerkleRoot)
- 如果已经投票标记已经投过票
- 验证通过则发起通过投票,否则发起拒绝投票
- 将此块计入已投票列表
- 如果未投票,开始投票
-
记录投票信息
-
记录投票节点
-
检查共识
- 获取当前投票总数
- 比较投票数是否满足最小共识节点数
- 比较投票通过数的比率是否达到阈值
- 如果超过则广播发送共识通过(包含块信息)
- 否则广播共识拒绝
- 比较投票通过数的比率是否达到阈值
- 解析块和信息
- 查看是否达成共识,如果是添加节点
- 检查本地是否存在该节点,如果是则跳过
- 检查是否包含块信息,如果不是则跳过
- 创建块,并验证块
- 如果验证失败则跳过
- 如果验证成功则将块加入本地
- 清理投票记录(指定块)
- 清理投票块记录(指定块)
- 清理投票节点记录(指定块)
-
交易加入块中后,它包含了 那些信息(发送,接受,金额,签名(包括UTXO))
-
块中余额变化干什么用的
-
笔者这里有个疑问:添加的新块怎么保证当前链是新的,是不是应该检查消息携带的块是否正好是当前链下一个链
-
更新节点状态(见,节点状态一节,这里保存来干什么呢)
-
什么时候发起挖矿请求
-
挖矿响应的时候验证块的时候验证了Hash,加入块的时候又重新计算了Hash,那响应里块携带的Hash是没有mine的么?
用 Solidity 快速部署一个代币(ERC20 或 ERC721),或者给你一个最简智能合约“发币模板”。
官网下载不到安装后可直接用于cmake的版本。自己编译一个。
- 下载源码并解压缩
- 打开本机vs自带的“适用于VS 2017 的 x64 本机工具命令提示”
- 路径到源码路径
perl Configure VC-WIN64A
nmake编译完成,编译结果就在当前目录。将当前目录添加到环境变量OPENSSL_ROOT_DIR。CMake配置findpage就可以自动找到依赖。
- 手动编译 Boost(或安装特定版本),可以按照以下步骤操作:
-
从 Boost 官网 下载 Boost 的源码包。
-
解压下载的文件到一个目录。
-
在 Boost 目录中,运行
bootstrap或bootstrap.bat(Windows 系统)来生成构建工具:./bootstrap.sh # Linux / macOS .\bootstrap.bat # Windows
-
使用
b2命令来编译 Boost:./b2 # Linux / macOS .\b2 # Windows
你可以通过以下命令来指定编译的选项:
./b2 install --prefix=/path/to/install/boost
- 安装完成后,配置环境变量BOOST_ROOT
从日志中可以看到,CMake 最终成功找到了 Boost 库,分析一下整个过程:
- 首先,系统检测到环境变量
Boost_ROOT被设置为D:\boost_1_88_0:
Environment variable Boost_ROOT is set to:
D:\boost_1_88_0
-
虽然有一个关于 CMP0074 策略的警告,但这不影响 Boost 的查找。
-
在 CMakeLists.txt 中,我们设置了以下关键配置:
set(Boost_USE_STATIC_LIBS ON)
set(Boost_USE_MULTITHREADED ON)
set(Boost_USE_STATIC_RUNTIME OFF)
find_package(Boost 1.88.0 REQUIRED COMPONENTS system)- 最终查找结果:
-- Found Boost: D:/boost_1_88_0 (found suitable version "1.88.0", minimum required is "1.88.0") found components: system
这表明 CMake 成功在 D:/boost_1_88_0 目录下找到了 Boost 1.88.0 版本,并且找到了所需的 system 组件。
虽然过程中出现了一个关于新版本 Boost 可能有不正确或缺失依赖的警告:
CMake Warning at C:/Program Files/CMake/share/cmake-3.23/Modules/FindBoost.cmake:1384 (message):
New Boost version may have incorrect or missing dependencies and imported targets
但这个警告并不影响项目的构建,因为最终 Boost_FOUND 被设置为 TRUE,并且成功找到了所需的 system 组件。
所以,Boost 库是通过以下方式找到的:
- 通过
BOOST_ROOT变量指定了 Boost 的安装路径 - 通过
find_package命令指定了版本要求和所需组件 - CMake 在指定路径下成功找到了符合要求的 Boost 库
让我解释这三个 Boost 相关的 CMake 设置:
-
set(Boost_USE_STATIC_LIBS ON)- 这个设置告诉 CMake 使用 Boost 的静态库(.lib 文件)而不是动态库(.dll 文件)
- 静态库会被直接链接到你的可执行文件中,运行时不需要额外的 DLL 文件
- 优点:部署简单,不需要分发额外的 DLL
- 缺点:可执行文件体积会变大
-
set(Boost_USE_MULTITHREADED ON)- 这个设置指定使用 Boost 的多线程版本库
- 在 Windows 上,这通常意味着使用
-mt后缀的库文件(如boost_system-mt.lib) - 这个设置对于使用 Boost 的异步功能(如
boost::asio)是必需的 - 确保库是线程安全的,可以在多线程环境中使用
-
set(Boost_USE_STATIC_RUNTIME OFF)- 这个设置控制是否使用静态版本的 C++ 运行时库
- 当设置为
OFF时,使用动态版本的 C++ 运行时库(MSVC 的 msvcrt.dll) - 当设置为
ON时,使用静态版本的 C++ 运行时库 - 在我们的项目中设置为
OFF,意味着:- 使用动态 C++ 运行时库
- 需要确保目标机器安装了正确版本的 Visual C++ 运行时库
- 可执行文件体积较小,但需要确保运行时环境
这些设置组合在一起的效果是:
- 使用 Boost 的静态库版本
- 确保库是线程安全的
- 使用动态 C++ 运行时库
这样的配置适合开发环境,因为:
- 不需要处理 DLL 依赖问题
- 支持多线程操作
- 与 Visual Studio 的默认设置保持一致
nlohmann::json 是一个非常强大的 C++ 库,它允许你非常直观地操作 JSON 数据,同时不需要进行类型检查或显式转换。它能够在编译时或运行时根据上下文推导出类型,并且不需要显式地进行类型转换。这种特性主要来源于以下几个设计和实现上的技巧:
nlohmann::json 使用了现代 C++ 中的 模板编程 和 类型推导 技术。它通过模板特性来简化类型的管理,使得 JSON 对象能够自动根据其实际类型进行推导,从而避免了显式的类型转换。
例如,当你向 nlohmann::json 添加成员时,它会自动推导出成员的数据类型,而不需要你显式地指明该成员是字符串、数字、布尔值等。这样做的原因是,json 对象使用了模板函数,允许它根据传入的类型参数推导出 JSON 对象的类型。
json j;
j["name"] = "John"; // 这里 "name" 会自动推导为字符串类型
j["age"] = 30; // 这里 "age" 会自动推导为整数类型nlohmann::json 实际上是对 C++ 基本数据类型和容器类型(如 std::map,std::vector 等)的封装。内部实现中,json 使用了一个 std::variant 或类似的技术来统一存储各种类型的值,并根据类型来处理赋值和访问。
具体地说,nlohmann::json 使用了一个 联合体(std::variant 或 std::any)来封装不同类型的值,这样一个 json 对象可以容纳不同类型的值,比如字符串、整数、数组、对象等。通过模板化的方式,赋值操作会根据传入值的类型来进行自动类型转换,而无需显式进行类型检查。
对于容器类型(如 std::vector,std::map 等),nlohmann::json 通过对这些类型的支持,可以将它们直接转化为对应的 JSON 数组或对象。例如:
json j_array = {1, 2, 3, 4}; // 自动推导为 JSON 数组
json j_object = {{"key", "value"}}; // 自动推导为 JSON 对象nlohmann::json 通过对常见操作符(如 []、=、dump() 等)的重载,提供了非常直观的 API,使得用户可以像使用 std::map、std::vector 一样直接对 JSON 对象进行赋值和操作。
-
赋值运算符(
operator[]):通过重载operator[],你可以像访问标准容器那样直接对json对象的成员进行赋值:json j; j["key"] = "value"; // 自动推导类型为字符串
-
拷贝和移动操作符:
json还支持拷贝和移动操作符,使得它能够灵活地处理传递和赋值的情况。
nlohmann::json 通过自定义的反序列化与序列化机制,允许将任意 C++ 对象(包括容器类型、用户自定义类型等)与 JSON 格式之间进行相互转换。它使用了模板与特化,配合 to_json 和 from_json 的全局函数,来实现这种能力:
// 反序列化
void from_json(const json& j, MyStruct& s) {
j.at("name").get_to(s.name);
j.at("age").get_to(s.age);
}
// 序列化
void to_json(json& j, const MyStruct& s) {
j = json{{"name", s.name}, {"age", s.age}};
}当你进行赋值时,nlohmann::json 库的实现通过 类型擦除 技术来使得不同类型的数据可以存储在同一个 json 对象中。这使得 json 对象可以自动推导并存储不同类型的数据,且不需要手动进行类型检查或转换。
json 对象的内部实现通常会包含一个变体类型(std::variant 或 std::any),这个类型能够存储不同的类型数据(例如 int、double、std::string 等),并在需要时通过特化的访问方法提取出来。
nlohmann::json 也支持深拷贝和智能内存管理,确保数据不会在不必要的情况下被复制。通过智能指针和引用的方式,json 库可以在不进行昂贵的复制操作的情况下,高效地操作大数据。
通过上面提到的类型推导和模板编程,nlohmann::json 能够做到在赋值时自动推导类型,而不需要显式的类型检查。这是因为:
- 模板函数 允许根据传入的类型进行推导,C++ 会根据传入数据的类型自动匹配合适的赋值操作。
- 运算符重载 和 类型擦除 使得即使是不同类型的 JSON 数据也能统一存储和操作,避免了显式的类型转换或检查。
nlohmann::json 库能够支持直接对成员赋值而不需要显式类型检查,是因为它通过模板、类型推导、运算符重载以及类型擦除等技术,能够自动推导出数据的类型并进行适当的操作。这样使得在 C++ 中操作 JSON 数据时既简洁又直观,极大地提升了开发效率。