简介
本文向您展示了典型的基于bitcoinj的应用程序中,不同对象和接口如何交互。 我们将看到数据如何从网络到达,转换成Java对象,然后这些对象如何四处走动,直到最终用它们执行各种操作或保存到磁盘。
为了本文的目的,我们将假设应用程序是一个钱包。
网络
比特币数据的生命始于两种方式 - 由对等网络中的另一个节点发送给我们,或者当我们自己创建交易。
Networking API的最低级别是实现 ClientConnectionManager的对象。该接口提供了打开新连接的方法,并要求关闭一些(随机选择的)连接。要打开新连接,必须提供实现 StreamParser 接口的对象以及要连接的网络地址。客户端连接管理器将设置一个Socket并管理对它的reads/writes操作。这里没有关于线程的保证 - 管理员可以在任意数量的线程或者只有一个线程上运行提供的 StreamParser 的方法。有两个实现提供:BlockingClientManager 和 NioClientManager。如果你创建了一个高级别的 PeerGroup 对象,那么默认情况下会创建一个 NioClientManager,尽管你也可以通过构造函数提供你自己的。它们之间的区别在于,NioClientManager 使用单个线程和基于异步 epoll/select 的IO,而BlockingClientManager 使用每个连接的线程与标准Java阻塞Socket。为什么bitcoinj支持这两种方法?
- 当你需要功能时,阻塞IO很有用。 Java可以透明地支持SSL,SOCKS代理,并通过Orchid、Tor,但仅在使用阻塞套接字时才支持。
- 当您想要同时处理数千个连接时,异步IO非常有用,而且每个连接没有额外的线程内存压力。
请注意,对于许多类型的应用程序,特别是钱包或商家应用程序,您不需要大量的同时连接,因此两者之间的性能差异在很大程度上无关紧要。另外,尽管线程每个连接和异步IO之间的可伸缩性差异在近期曾经非常大,但更好的内核调度器和多核系统的出现意味着差异往往不再那么明确。在仔细关注线程堆栈大小的情况下,可能会出现这样的情况:每个连接的线程不像以前那样昂贵。
理论上,NioClientManager 可以很容易地同时支持异步IO和多个线程,但是当前的实现不支持。
序列化与反序列化
如上所述,客户端管理器类需要实现 StreamParser 接口。这个接口提供了一种方法,用于通知连接打开或关闭,接收原始字节缓冲区并获得 MessageWriteTarget 接口的实现。 StreamParser 被赋予从网络中读取的数据包,不需要任何形式的分帧或解析。例如,在StreamParser 的前门上显示一条消息是有效的。解析器缓存数据,处理成帧并以某种方式消耗数据。
当给客户端管理一个新的parser时,它将内部对象设置为 MessageWriteTarget 。这个接口公开了一个写字节的方法,并关闭了连接。因此parser对象通常管理启动的连接的生命周期。
抽象 PeerSocketHandler 类通过提供缓冲,校验和字节流解析到 Message 对象来实现比特币P2P网络协议的 StreamParser。这是通过使用 BitcoinSerializer 类来完成的,该类知道如何从线上读取消息的类型和校验,然后构建适当的对象来表示该类消息。它有一个Object 类型的静态的隐射。它可以构造的每个对象都是 Message 类的后代。每个消息类都负责从原始字节实现自己的反序列化。
一旦 Message 被完全构造并完成反序列化,它就被传递给 PeerSocketHandler 上的抽象方法。因此,如果您只想访问经过分析的消息流,则应该在此处进行子类化。
消息的序列化是由Satoshi设计的自定义二进制格式。它具有最小的开销,因此灵活性最小。
对等逻辑
您的应用很可能不想处理原始比特币协议消息流,而是在更高级别上运行。为此,Peer 子类 PeerSocketHandler 跟踪与连接有关的状态并处理传入的消息。它提供高级操作,如下载区块,整个链,交易,执行ping等。
它还将消息分发给与其连接的各种其他对象,具体为:
- 任何已注册的 PeerEventListener 。
- 一个 MemoryPool,如果提供了一个(见下文)。
- 任何连接到它的 钱包。
- 提供的块链对象(如果有的话)。
在接收到消息时,每个 PeerEventListener 都有机会读取并截获消息,可能会修改消息,用不同的消息替换消息或完全禁止进一步处理。如果处理未被抑制,则会发生以下情况:
- 如果Peer已被指示下载数据,则收到的发送新块或交易的“inv”消息将导致发送“getdata”, MemoryPool 将被通知关于“inv”的逻辑。
- 收到的包含交易数据的“tx”消息首先通过 MemoryPool 传递。然后通过 isPendingTransactionRelevant询问每个 电子钱包 是否关注该特定交易,以及是否所有交易挂起的依存关系都是递归下载的。递归下载完成后,事务和所有挂起的依赖关系将传递给 Wallet.receivePending()。最后调用 PeerEventListener.onTransaction。
- 已接收的块,已过滤的块或块标题将发送到 AbstractBlockChain对象以供进一步处理。
- 如果远程节点使用“getdata”请求我们的交易数据,则轮询钱包和监听器以查看是否有任何可以提供的数据,如果有的话则发送该数据作为响应。
- 诸如ping或警报之类的杂项消息将根据情况进行处理。
内存池
知道有多少Peer(以及哪个Peer)宣布了特定的交易可能很方便。查看关于bitcoinj SecurityModel的文章,了解其原理很有趣。为了实现这个,MemoryPool 类跟踪已经发现的交易和交易哈希。
例如,如果Peer我们发送了一个“inv”,表示它有哈希值为 87c79f8d77fe2078333c612e2bdf1735127c6c02 的交易,则Peer将通知MemoryPool,并记录该Peer已经看到该交易。我们最终可能会下载给定的交易,以确定它是否属于我们,并且此时它还会被提供给 MemoryPool ,以便应用程序的某些部分对此感兴趣。随着进一步invs进来,交易confidence对象被更新。
这也可能是同一个“tx”消息多次发送给我们。通常情况下,这不应该发生。但是如果这样做,MemoryPool会对它们进行重复数据删除操作,以确保只有一个Java对象处于浮动状态,即使它被反序列化了多次。
Chains 与 stores
AbstractBlockChain 的一个子类负责接收块,将它们放在一起,并对它们进行验证。 BlockChain 类进行SPV级别验证,FullPrunedBlockChain 按名称暗示进行完整验证。
您将块链传递给 Peer 或 PeerGroup ,块数据将通过该连接从网络流向块链对象,并转向 BlockStore 接口的实现。有多种类型的块存储,但它们都采用块数据并至少保存头文件,并可能(对于完整存储)保存交易数据。对于SPV客户端,SPVBlockStore是典型的选择,对于全模式客户端,需要实现 FullPrunedBlockStore ,例如 H2FullPrunedBlockStore。
stores直接与数据库或磁盘文件对话。他们下面没有其他对象了。
Chains在其 BlockChainListener 上调用回调。尽管建议使用更具体的 BlockChain.addWallet() 方法(它与addListener()做同样的事情,但将来可能会更改),Wallet是块链监听器的一个示例。
监听者被调用以下事件:
- notifyNewBestBlock :在找到共识度最搞的链的新块时调用。这是系统的正常延续。块参数只是块头 - 没有交易数据可用。
- reorganize :在收到扩展侧链并使其成为新的最佳链的块时调用。重组导致一个时间表被另一个时间表取代,在该时间表中,交易可能已被重新排序,替换或完全放弃。出于这个原因,在听到重组后,听众必须更新其内部簿记来说明新现实。重组方法给出了已更改的块链段,以便他们可以找出要执行的操作。如果您正在实现自己的侦听器,并且您的应用似乎可以正常工作,但忽略重组可能会让您的应用受到安全攻击和数据损坏,这可能很容易忽略。
- isTransactionRelevant :调用块中的每个交易来确定一个侦听器是否对它感兴趣。这是一个可能在将来被删除的优化步骤 - 它允许块链在SPV模式下具有完整(未过滤)块时避免验证merkle树,除非该块中有实际的交易可能与我们的钱包有关(向我们的钥匙发送金钱)。这对手机产生了很大的影响,但随着Bloom过滤的推出,它将变得越来越有用。
- receiveFromBlock :在接收到包含它的块时调用每个先前交易相关的交易。该块可能或可能不在最佳链上,参数会告诉你它是哪一个。请注意,当Bloom过滤器处于活动状态时,并不是每个交易都可能出现在这里 - 如果交易先前由对等方发送给我们,那么当包含它的块被打包时,它们不会再次发送它,我们只会发送Hash。这是为了节省带宽。因此,还有一个…
- notifyTransactionIsInBlock :这与 receiveFromBlock 相同,但提供的是Hash而不是完整的交易。预计监听器此时已经拥有交易数据的副本。
为了让每个交易都有一个新的完整块,最好的链触发器是 TransactionTransactionRelevant ,receiveFromBlock 然后是 notifyNewBestBlock。最佳链触发器上的新过滤块是 TransactionRelevant,它是 receiveFromBlock 或 notifyTransactionIsInBlock 的混合,然后是 notifyNewBestBlock 。扩展侧链的新块具有相同的序列,但不包含 notifyNewBestBlock ,并且扩展侧链并导致重新组织的新块具有相同的序列,但在最后调用 reorganize 而不是 notifyNewBestBlock 。
对于SPV模式应用程序,块存储被赋予所有非孤立块,而不管它们连接的位置如何,并且在新的最佳链头改变时通知块存储,以便将其写入磁盘。它只能存储标题。
精简数据
对于完全验证的节点,store需要做更多的事情,并且必须实现 FullPrunedBlockStore 接口。chain和store一起实施ultraprune算法,就像比特币0.8+一样。然而,与比特币0.8不同的是,store实际上会在一段时间后永久删除不需要的数据,因此它不能为其他节点提供服务,但所使用的磁盘空间要低得多。
精简的节点不会尝试存储整个块链。相反,它只存储未使用的交易输出集(UTXO集)。一旦交易输出被使用,其数据不再需要,并且可以被删除。重组事件有点复杂,因为它们可以重写历史记录,因此精简存储库也应该保留一定数量的“撤销块”,允许撤销对UTXO集合的更改。存储的撤消块数量是所使用的磁盘空间与可处理的最大重新组织之间的折衷。如果撤消块被过于激进地扔掉,那么大的重组可能会永久性地将节点从链上剔除,迫使从头开始重新初始化,所以最好保守。
FullPrunedBlockStore 接口提供添加,删除和测试UTXO集的方法。它还具有插入块和撤消块以及开始/结束数据库事务的方法(注意:与比特币事务不同)。
钱包
Wallet类充当块链监听器并接收来自链对象的数据和事件。 它将接收到的数据保存在自身中,并跟踪所有可能对钱包用户感兴趣的交易,例如将钱存入密钥的交易。 可以使用WalletProtobufSerializer将钱包保存到协议缓冲区,并且可以随时自动更改钱包的功能。
目前,电子钱包没有任何方式将自己存储到数据库中。 这将是一个很好的未来。
钱包还负责更新放置在其中的交易的Confidence水平。钱包外的一个交易可能会被新节点公布的内存池更新,但最终不会了解其在链中的位置。