面向企业的区块链教程(二)

150 阅读48分钟

面向企业的区块链教程(二)

原文:zh.annas-archive.org/md5/71bd99f39f23fd60e3875318ad23711a

译者:飞龙

协议:CC BY-NC-SA 4.0

第四章:使用 web3.js 入门

在上一章中,我们学习了如何使用 Solidity 编写和部署智能合约。在本章中,我们将学习有关 web3.js 的知识,以及如何导入它,连接到 geth,并在 Node.js 或客户端 JavaScript 中使用它。我们还将学习如何使用 web3.js 构建一个网络客户端,用于上一章中创建的智能合约。

在本章中,我们将涵盖以下主题:

  • 在 Node.js 和客户端 JavaScript 中导入 web3.js

  • 连接到 geth

  • 探索 web3.js

  • 发现 web3.js 最常用的 API

  • 为所有权合约构建 Node.js 应用程序

本章与作者之前的书籍 Blockchain for Projects 中的章节相同。这不是第二版书籍,而是用于向读者解释基本概念。

web3.js 简介

web3.js 为我们提供了与 geth 通信的 JavaScript API。它在内部使用 JSON-RPC 与 geth 通信。web3.js 也可以与支持 JSON-RPC 的任何其他类型的以太坊节点通信。它将所有 JSON-RPC API 公开为 JavaScript API。它不仅支持与以太坊相关的所有 API,还支持与 WhisperSwarm 相关的 API。

随着我们构建各种项目,你将继续学习更多关于 web3.js 的知识。不过,现在让我们先了解一些 web3.js 最常用的 API。稍后,我们将使用 web3.js 为我们在上一章中创建的所有权智能合约构建一个前端。

在撰写本文时,web3.js 的最新版本是 1.0.0-beta.18。我们将使用此版本学习所有内容。

web3.js 托管在 github.com/ethereum/web3.js,完整文档托管在 github.com/ethereum/wiki/wiki/JavaScript-API

导入 web3.js

只需在项目目录中运行 npm install web3 即可在 Node.js 中使用 web3.js。在源代码中,可以使用 require("web3"); 进行导入。

要在客户端 JavaScript 中使用 web3.js,可以将 web3.js 文件入队,该文件位于项目源代码的 dist 目录中。现在,web3 对象将全局可用。

连接到节点

web3.js 可以使用 HTTP 或 IPC 与节点通信,并允许我们连接多个节点。我们将使用 HTTP 进行节点通信。web3 的一个实例表示与节点的连接。该实例公开了 API。

当应用程序在 mist 中运行时,它会自动创建一个连接到 mist 节点的 web3 实例。实例的变量名为 web3

这是连接到节点的基本代码:

if (typeof web3 !== 'undefined') { 
  web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); 
} 

首先,我们通过检查 web3 是否为 undefined 来验证代码是否在 mist 中运行。如果 web3 已定义,那么我们使用已有的实例;否则,我们通过连接到自定义节点来创建一个实例。如果你想无论应用是否在 mist 中运行都连接到自定义节点,可以从上述代码中移除 if 条件。在这里,我们假设我们的自定义节点在本地的 8545 端口上运行。

Web3.providers 对象暴露了构造函数(在这个上下文中称为providers),用于建立连接并使用各种协议传输消息。Web3.providers.HttpProvider 允许我们建立 HTTP 连接,而 Web3.providers.IpcProvider 允许我们建立 IPC 连接。

web3.currentProvider 属性会自动分配给当前的提供者实例。在创建一个 web3 实例后,你可以使用 web3.setProvider() 方法来更改它的提供者。它接受一个参数,即新提供者的实例。

请记住,默认情况下,geth 禁用了 HTTP-RPC。因此,在运行 geth 时通过传递 --rpc 选项来启用它。HTTP-RPC 默认运行在 8545 端口上。

web3 暴露了一个 isConnected() 方法,用于检查是否连接到节点。根据连接状态,它返回一个 truefalse 的值。

API 结构

web3 包含一个专门用于以太坊区块链交互的 eth 对象(web3.eth)以及一个用于 Whisper 交互的 shh 对象(web3.shh)。大部分 web3.js API 都在这两个对象内部。

所有 API 默认情况下都是同步的。对于异步请求,你可以为大多数函数的最后一个参数传递一个可选的回调。所有回调都使用错误优先的回调风格。

一些 API 对于异步请求有一个别名。例如,web3.eth.coinbase() 是同步的,而 web3.eth.getCoinbase() 是异步的。

这里有一个例子:

//sync request 
try 
{ 
  console.log(web3.eth.getBlock(48)); 
} 
catch(e) 
{ 
  console.log(e); 
} 

//async request 
web3.eth.getBlock(48, function(error, result){ 
    if(!error) 
        console.log(result) 
    else 
        console.error(error); 
}) 

getBlock 用于使用其编号或哈希获取块的信息。或者它可以接受字符串,例如 "earliest"(创世块)、"latest"(区块链的顶块)或 "pending"(正在挖矿的块)。如果不传递参数,则默认为 web3.eth.defaultBlock,默认分配为 "latest"

所有需要块标识作为输入的 API 默认情况下可以接受数字、哈希或可读字符串之一。如果未传递值,这些 API 默认使用 web3.eth.defaultBlock

BigNumber.js

JavaScript 在处理大数时表现很差。因此,对于需要处理大数并进行精确计算的应用程序,请使用 BigNumber.js 库。

web3.js 也依赖于 BigNumber.js,并自动添加它。web3.js 总是返回 BigNumber 对象作为数字值。它可以接受 JavaScript 数字、数字字符串和 BigNumber 实例作为输入。

让我们来演示一下,如下所示:

web3.eth.getBalance("0x27E829fB34d14f3384646F938165dfcD30cFfB7c")
  .toString(); 

在这里,我们使用 web3.eth.getBalance() 方法获取地址的余额。此方法返回一个 BigNumber 对象。我们需要对 BigNumber 对象调用 toString() 来将其转换为数字字符串。

BigNumber.js 无法正确处理具有超过 20 个浮点数字的数字。因此,建议您将余额存储在 wei 单位中,并在显示时将其转换为其他单位。web3.js 本身总是以 wei 单位返回和接受余额。例如,getBalance() 方法返回以 wei 为单位的地址余额。

单位转换

web3.js 提供了将 wei 余额转换为任何其他单位以及反之的 API。

web3.fromWei() 方法将 wei 数字转换为另一个单位,而 web3.toWei() 方法将任何其他单位的数字转换为 wei。以下是一个示例来演示这一点:

web3.fromWei("1000000000000000000", "ether"); 
web3.toWei("0.000000000000000001", "ether"); 

在第一行中,我们将 wei 转换为 ether;在第二行中,我们将 ether 转换为 wei。这两个方法中的第二个参数可以是以下字符串之一:

  • kweiada

  • mweibabbage

  • gweishannon

  • szabo

  • finney

  • ether

  • kether / grand / einstein

  • mether

  • gether

  • tether

检索 gas 价格、余额和交易详细信息

让我们来看一下检索 gas 价格、地址余额以及已挖掘交易信息的 API:

//It's sync. For async use getGasPrice 
console.log(web3.eth.gasPrice.toString()); 

console.log(web3.eth.getBalance("0x407d73d8a49eeb85d32cf465507dd71d5071
  00c1", 45).toString()); 

console.log(web3.eth.getTransactionReceipt("0x9fc76417374aa880d4449a1f7
  f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b")); 

输出将以此格式显示:

20000000000 
30000000000 
{ 
 "transactionHash": "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b ", 
 "transactionIndex": 0, 
 "blockHash": "0xef95f2f1ed3ca60b048b4bf67cde2195961e0bba6f70bcbea9a2c4e133e34b46", 
 "blockNumber": 3, 
 "contractAddress": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", 
 "cumulativeGasUsed": 314159, 
 "gasUsed": 30234 
} 

以下是前述方法的工作原理:

  • web3.eth.gasPrice(): 通过 x 最新区块的中位数 gas 价格确定 gas 价格。

  • web3.eth.getBalance(): 返回给定地址的余额。所有的哈希都应该以十六进制字符串(而不是十六进制字面值)的形式提供给 web3.js API。对于 Solidity 地址类型的输入也应该是十六进制字符串。

  • web3.eth.getTransactionReceipt(): 用于使用其哈希获取有关交易的详细信息。如果在区块链中找到了交易,则返回一个交易收据对象;否则返回 null。交易收据对象包含以下属性:

    • blockHash: 交易所在区块的哈希。

    • blockNumber: 此交易所在的区块编号。

    • transactionHash: 交易的哈希。

    • transactionIndex: 交易在区块中的位置的整数。

    • from: 发送者的地址。

    • to: 接收方的地址;当它是一个合约创建交易时,这个参数被设为 null

    • cumulativeGasUsed: 在该交易在区块中执行时使用的总 gas 量。

    • gasUsed: 仅由此特定交易使用的 gas 量。

    • contractAddress: 如果交易是合约创建,则创建的合约地址。否则,这将被设为 null

    • logs: 此交易生成的日志对象数组。

发送 ether

让我们看看如何向任何地址发送ether。要发送ether,您需要使用web3.eth.sendTransaction()方法。此方法可用于发送任何类型的交易,但主要用于发送ether。这是因为使用此方法部署合约或调用合约的方法很麻烦,因为您需要手动生成交易数据而不是自动生成。它接受一个具有以下属性的交易对象:

  • from:发送账户的地址。如果未指定,则使用web3.eth.defaultAccount属性。

  • to:这是可选的。这是消息的目的地地址,对于合约创建交易则未定义。

  • value:这是可选的。交易的价值以 wei 为单位进行转移,如果是合约创建交易,则还包括捐赠金。

  • gas:这是可选的。这是用于交易的燃气量(未使用的燃气将退还)。如果未提供,则会自动确定。

  • gasPrice:这是可选的。这是交易的燃气价格,以 wei 为单位,默认为平均网络燃气价格。

  • data:这是可选的。它要么是包含消息相关数据的字节字符串,要么在合约创建交易的情况下是初始化代码。

  • nonce:这是可选的。这是一个整数。每个交易都与nonce相关联。nonce是一个计数器,表示发送者所做交易的数量。如果未提供,它将自动确定。它有助于防止重放攻击。此nonce不是与块相关联的nonce。如果我们使用的nonce大于交易应该具有的nonce,则该交易将放入队列,直到其他交易到达。例如,如果下一个交易的nonce应为四,而我们将nonce设置为十,则 geth 将等待其余的六个交易到达后再广播此交易。nonce为十的交易称为排队的交易,而不是待处理的交易。

下面是一个发送ether到地址的示例:

var txnHash = web3.eth.sendTransaction({ 
  from: web3.eth.accounts[0], 
  to: web3.eth.accounts[1], 
  value: web3.toWei("1", "ether") 
}); 

在这里,我们从账户编号0发送了一个ether到账户编号1。我们需要确保在运行 geth 时使用unlock选项来解锁两个账户。geth 交互式控制台会提示输入密码,但是在交互式控制台之外使用的 web3.js API 如果账户被锁定,将会抛出错误。此方法返回交易的事务哈希。然后,您可以使用getTransactionReceipt()方法检查交易是否已被挖掘。

你还可以使用web3.personal.listAccounts()web3.personal.unlockAccount(addr, pwd)web3.personal.newAccount(pwd) API 在运行时管理账户。

与合约工作

让我们学习如何部署新合同,使用其地址获取已部署合同的引用,向合同发送ether,发送交易以调用contract方法,并估算方法调用的 gas。

要部署新合同或获取对已部署合同的引用,您需要首先使用web3.eth.contract()方法创建一个contract对象。它以合同 ABI 作为参数,并返回contract对象。

这是创建contract对象的代码:

var proofContract = web3.eth.contract([{"constant":false,"inputs":
  [{"name":"fileHash","type":"string"}],"name":"get","outputs":
  [{"name":"timestamp","type":"uint256"},
  {"name":"owner","type":"string"}],"payable":false,"type":"function"},
  {"constant":false,"inputs":[{"name":"owner","type":"string"},
  {"name":"fileHash","type":"string"}],"name":"set","outputs":
  [],"payable":false,"type":"function"},{"anonymous":false,"inputs":
  [{"indexed":false,"name":"status","type":"bool"},
  {"indexed":false,"name":"timestamp","type":"uint256"},
  {"indexed":false,"name":"owner","type":"string"}, 
  {"indexed":false,"name":"fileHash","type":"string"}],"name"
  :"logFileAddedStatus","type":"event"}]);

一旦您获得了合同,您可以使用contract对象的new方法部署它,或者使用at方法获取与 ABI 匹配的已部署合同的引用。

让我们看一个部署新合同的示例,如下所示:

 var proof = proofContract.new({
         from: web3.eth.accounts[0],
         data: "0x606060405261068...",
         gas: "4700000"
     },
     function(e, contract) {
         if (e) {
             console.log("Error " + e);
         } else if (contract.address != undefined) {
             console.log("Contract Address: " + contract.address);
         } else {
             console.log("Txn Hash: " + contract.transactionHash)
         }
     })

这里,new方法是异步调用的,因此如果交易成功创建并广播,则回调将被调用两次。第一次,在交易广播后调用,第二次,在交易挖掘后调用。如果您不提供回调函数,则proof变量的address属性将设置为undefined。一旦contract被挖掘,address属性将被设置。

proof合同中,没有构造函数,但如果有构造函数,则构造函数的参数应该放在new方法的开头。我们传递的对象包含from地址,合同的字节码和要使用的最大gas。这三个属性必须存在才能创建交易。此对象可以具有传递给sendTransaction()方法的对象中存在的属性,但在这里,data是合同的字节码,to属性被忽略。

您可以使用at方法获取已部署合同的引用。以下是演示此操作的代码:

var proof = 
  proofContract.at("0xd45e541ca2622386cd820d1d3be74a86531c14a1");

现在让我们看一下发送交易以调用合同方法的情况。以下是演示此操作的示例:

proof.set.sendTransaction("Owner Name", 
  "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", { 

from: web3.eth.accounts[0], 
}, function(error, transactionHash){ 

if (!err) 

console.log(transactionHash); 
}) 

在这里,我们为同名方法调用对象的sendTransaction方法。传递给此sendTransaction方法的对象具有与web3.eth.sendTransaction()相同的属性,只是忽略了datato属性。

如果您想调用节点本身的方法,而不是创建交易并广播它,那么您可以使用sendTransaction而不是sendTransaction。如下所示:

var returnValue = proof.get.call
  ("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); 

有时,有必要了解调用方法所需的 gas 量,以便您可以决定是否调用它。您可以使用web3.eth.estimateGas来实现此目的。然而,使用web3.eth.estimateGas()需要您直接生成交易的数据;因此,我们可以使用合同对象的estimateGas()方法。以下是演示此操作的示例:

var estimatedGas = proof.get.estimateGas
  ("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); 

要向合同发送一些ether而不调用任何方法,您可以简单地使用web3.eth.sendTransaction方法。

检索和监听合约事件

监听事件非常重要,因为通常通过触发事件返回交易调用的方法结果。

在学习如何检索和监听事件之前,我们需要了解事件的索引参数。 事件的最多三个参数可以具有 indexed 属性。 此属性用于指示节点将其索引,以便应用客户端可以搜索具有匹配返回值的事件。 如果不使用 indexed 属性,则必须从节点检索所有事件并过滤所需的事件。 例如,您可以这样编写 logFileAddedStatus 事件:

event logFileAddedStatus(bool indexed status, uint indexed timestamp,
  string owner, string indexed fileHash); 

下面是一个示例,演示如何监听合约事件:

var event = proof.logFileAddedStatus(null, {
 fromBlock: 0,
 toBlock: "latest"
});
event.get(function(error, result) {
 if (!error) {
 console.log(result);
 } else {
 console.log(error);
 }
})
event.watch(function(error, result) {
 if (!error) {
 console.log(result.args.status);
 } else {
 console.log(error);
 }
})
setTimeout(function() {
 event.stopWatching();
}, 60000)
var events = proof.allEvents({
 fromBlock: 0,
 toBlock: "latest"
});
events.get(function(error, result) {
 if (!error) {
 console.log(result);
 } else {
 console.log(error);
 }
})
events.watch(function(error, result) {
 if (!error) {
 console.log(result.args.status);
 } else {
 console.log(error);
 }
})
setTimeout(function() {
 events.stopWatching();
}, 60000)

以下是上述代码的工作原理:

  • 首先,我们通过在合约实例上调用事件同名方法来获取 event 对象。 此方法接受两个对象作为参数,用于筛选事件:

    • 第一个对象用于通过索引返回值筛选事件,例如,{'valueA': 1, 'valueB': [myFirstAddress, mySecondAddress]}。 所有筛选值默认设置为 null。 这意味着它们将匹配来自此合约的给定类型的任何事件。

    • 下一个对象可以包含三个属性:fromBlock(“最早”的块;默认情况下为"latest");toBlock(“最新”的块;默认情况下为"latest");和 address(仅从中获取日志的地址列表;默认情况下为合约地址)。

  • event 对象公开三种方法:getwatchstopWatchingget 用于获取区块范围内的所有事件。 watch 类似于 get,但它在获取事件后监视更改。 stopWatching 可用于停止监视更改。

  • 然后,我们有了合约实例的 allEvents 方法。 它用于检索合约的所有事件。

每个事件都由一个对象表示,其中包含以下属性:

  • args:包含事件参数的对象。

  • event:表示事件名称的字符串。

  • logIndex:表示块中的日志索引位置的整数。

  • transactionIndex:表示创建此索引位置日志的事务的整数。

  • transactionHash:表示创建此日志的交易的哈希的字符串。

  • address:表示此日志来源地址的字符串。

  • blockHash:表示包含此日志的块的哈希的字符串。 当处于待定状态时,此字段为 null

  • blockNumber:此日志所在的块号。 当处于待定状态时,此字段为 null

web3.js 提供了一个web3.eth.filter API 来检索和监视事件。您可以使用这个 API,但是在前一种方法中处理事件的方式要简单得多。您可以在github.com/ethereum/wiki/wiki/JavaScript-API#web3ethfilter了解更多信息。

为所有权合约构建客户端

在上一章中,我们为所有权合约编写了 Solidity 代码。在上一章和本章中,我们学习了 web3.js 以及如何使用 web3.js 调用合约的方法。现在,是时候为我们的智能合约构建一个客户端,以便用户可以轻松地使用它。

我们将构建一个客户端,企业用户选择文件,输入所有者细节,然后点击提交来广播一个交易来调用合约的set方法,使用文件哈希和所有者细节。一旦成功广播交易,我们将显示交易哈希。用户还可以选择一个文件,并从智能合约获取所有者的细节。客户端还将实时显示最近的set交易。

我们将在前端使用 sha1.js 来获取文件哈希,使用 jQuery 进行 DOM 操作,并使用 Bootstrap 4 来创建响应式布局。我们将在后端使用 Express.js 和 web3.js。我们将使用socket.io,这样后端就可以将最近挖掘到的交易推送到前端,而无需前端周期性地请求数据。

项目结构

在本章的练习文件中,您会找到两个目录:FinalInitialFinal包含项目的最终源代码,而Initial包含空白源代码文件和库,以便您快速开始构建应用程序。

要测试Final目录,您需要在其中运行npm install,并将app.js中的硬编码合约地址替换为部署合约后获得的合约地址。然后,使用Final目录内的node app.js命令运行应用程序。

Initial目录中,您会找到一个public目录和两个名为app.jspackage.json的文件。package.json包含我们应用的后端依赖,app.js是您放置后端源代码的地方。

public目录包含与前端相关的文件。在public/css目录内,您会找到bootstrap.min.css,这是 Bootstrap 库;在public/html目录内,您会找到index.html,您将在其中放置应用程序的 HTML 代码;而在public/js目录内,您会找到用于 jQuery、sha1 和 socket.io 的 JS 文件。在public/js目录内,您还会找到一个main.js文件,您将在其中放置我们应用的前端 JS 代码。

构建后端

首先,在 Initial 目录内运行 npm install 安装我们后端所需的依赖项。在开始编写后端之前,请确保 geth 正在运行,并启用了 rpc。最后,请确保账户 0 存在并已解锁。

在开始编码之前,你需要做的最后一件事是使用我们在前一章中看到的代码部署所有权合同,并复制合同地址。

现在让我们创建一个单独的服务器,该服务器将为浏览器提供 HTML,并接受 socket.io 连接:

var express = require("express"); 
var app = express(); 
var server = require("http").createServer(app); 
var io = require("socket.io")(server); 
server.listen(8080); 

在这里,我们将 expresssocket.io 服务器整合到一个运行在端口 8080 上的服务器中。

现在让我们创建路由来提供静态文件并为应用程序的主页创建路由。以下是执行此操作的代码:

app.use(express.static("public")); 
app.get("/", function(req, res){ 
  res.sendFile(__dirname + "/public/html/index.html"); 
}) 

这里,我们使用 express.static 中间件来提供静态文件服务。我们要求它在 public 目录中查找静态文件。

现在让我们连接到 geth 节点,并获取已部署的合同的引用,以便我们可以发送交易并监听事件。以下是执行此操作的代码:

var Web3 = require("web3"); 

web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); 

var proofContract = web3.eth.contract([{"constant":false,"inputs":[{"name":"fileHash","type":"string"}],"name":"get","outputs":[{"name":"timestamp","type":"uint256"},{"name":"owner","type":"string"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"owner","type":"string"},{"name":"fileHash","type":"string"}],"name":"set","outputs":[],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"status","type":"bool"},{"indexed":false,"name":"timestamp","type":"uint256"},{"indexed":false,"name":"owner","type":"string"},{"indexed":false,"name":"fileHash","type":"string"}],"name":"logFileAddedStatus","type":"event"}]); 

var proof = 
  proofContract.at("0xf7f02f65d5cd874d180c3575cb8813a9e7736066"); 

代码很直观。只需用你得到的合同地址替换原来的地址即可。

现在让我们创建路由来广播交易并获取有关文件的信息。以下是执行此操作的代码:

app.get("/submit", function(req, res){
  var fileHash = req.query.hash;
  var owner = req.query.owner;
  var pkeys = req.query.pkeys;

  pkeys = pkeys.split(",")

  proof.set.sendTransaction(owner, fileHash, {
    from: web3.eth.accounts[0],
    privateFor: pkeys
  }, function(error, transactionHash){
    if (!error)
    {
      res.send(transactionHash);
    }
    else
    {
      res.send("Error");
    }
  })
})

app.get("/getInfo", function(req, res) {
    var fileHash = req.query.hash;
    var details = proof.get.call(fileHash);
    res.send(details);
})

这里,/submit 路由用于创建和广播交易。一旦我们获得了交易哈希,我们就将其发送给客户端。我们不会做任何等待交易挖矿的操作。/getInfo 路由调用节点上合同的 get 方法,而不是创建一个交易。它只是简单地将收到的任何响应发送回去。

现在让我们监听来自合同的事件,并将它们广播给所有客户端。以下是执行此操作的代码:

proof.logFileAddedStatus().watch(function(error, result) {
    if (!error) {
        if (result.args.status == true) {
            io.send(result);
        }
    }
})

在这里,我们检查 status 是否为 true,只有当它为 true 时,我们才将事件广播到所有连接的 socket.io 客户端。

构建前端

让我们从应用程序的 HTML 开始。将此代码放在 index.html 文件中,如下所示:

<!DOCTYPE html> 
<html lang="en"> 
    <head> 
        <meta name="viewport" content="width=device-width, initial-
          scale=1, shrink-to-fit=no"> 
        <link rel="stylesheet" href="/css/bootstrap.min.css"> 
    </head> 
    <body> 
        <div class="container"> 
            <div class="row"> 
                <div class="col-md-6 offset-md-3 text-xs-center"> 
                    <br> 
                    <h3>Upload any file</h3> 
                    <br> 
                    <div> 
                        <div class="form-group"> 
                            <label class="custom-file text-xs-left"> 
                                <input type="file" id="file" 
                                  class="custom-file-input"> 
                                <span class="custom-file-control">
                                </span> 
                            </label> 
                        </div> 
                        <div class="form-group"> 
                            <label for="owner">Enter owner name</label> 
                            <input type="text" class="form-control"
                              id="owner"> 
                        </div> 
                        <div class="form-group">
                            <label for="owner">Enter Public Keys
                            <small>(comma Seperated)</small></label>
                            <input type="text" class="form-control"
                             id="pkeys">
                        </div>
                        <button onclick="submit()" class="btn btn-
                         primary">Submit</button> 
                        <button onclick="getInfo()" class="btn btn-
                        primary">Get Info</button> 
                        <br><br> 
                        <div class="alert alert-info" role="alert" 
                         id="message"> 
                            You can either submit the file's details or
                             get information about it. 
                        </div> 
                    </div> 
                </div> 
            </div> 
            <div class="row"> 
                <div class="col-md-6 offset-md-3 text-xs-center"> 
                    <br> 
                    <h3>Live Transactions Mined</h3> 
                    <br> 
                    <ol id="events_list">No Transaction Found</ol> 
                </div> 
            </div> 
        </div> 
        <script type="text/javascript" src="img/sha1.min.js"></script> 
        <script type="text/javascript" src="img/jquery.min.js">
          </script> 
        <script type="text/javascript" src="img/socket.io.min.js">
          </script> 
        <script type="text/javascript" src="img/main.js"></script> 
    </body> 
</html> 

以下是代码的工作原理:

  • 首先,我们显示 Bootstrap 的文件输入字段,以便用户可以选择文件。

  • 然后,我们显示一个文本字段,用户可以输入所有者的详细信息。

  • 然后我们有两个按钮。第一个按钮用于在合同中存储文件哈希和所有者的详细信息,第二个按钮用于从合同中获取文件的信息。点击 Submit 按钮会触发 submit() 方法,点击 Get Info 按钮会触发 getInfo() 方法。

  • 接下来,我们有一个警告框来显示消息。

  • 最后,我们显示一个有序列表,以显示用户在页面上时挖矿的合同的交易。

现在,让我们编写 getInfo()submit() 方法的实现,建立与服务器的 socket.io 连接,并监听来自服务器的 socket.io 消息。将此代码放在 main.js 文件中:

function submit()
{
  var file = document.getElementById("file").files[0];

  if(file)
  {
    var owner = document.getElementById("owner").value;

    if(owner == "")
    {
      alert("Please enter owner name");
    }
    else
    {
      var publicKeys = document.getElementById("pkeys").value;

      if(publicKeys == "")
      {
        alert("Please enter the other enterprise's public keys");
      }
      else
      {
        var reader = new FileReader();
        reader.onload = function (event) {
            var hash = sha1(event.target.result);

            $.get("/submit?hash=" + hash + "&owner=" + owner + 
            "&pkeys=" + encodeURIComponent(publicKeys), function(data){
              if(data == "Error")
              {
                $("#message").text("An error occured.");
              }
              else
              {
                $("#message").html("Transaction hash: " + data);
              }
            });
        };
        reader.readAsArrayBuffer(file);
      }
    }
  }
  else
  {
    alert("Please select a file");
  }
}

function getInfo()
{
  var file = document.getElementById("file").files[0];

  if(file)
  {
    var reader = new FileReader();
    reader.onload = function (event) {
        var hash = sha1(event.target.result);

        $.get("/getInfo?hash=" + hash, function(data){
          if(data[0] == 0 && data[1] == "")
          {
            $("#message").html("File not found");
          }
          else
          {
            $("#message").html("Timestamp: " + data[0] + " Owner: " + 
              data[1]);
          }
        });
    };
    reader.readAsArrayBuffer(file);
  }
  else
  {
    alert("Please select a file");
  }
}

var socket = io("http://localhost:8080");

socket.on("connect", function () {
  socket.on("message", function (msg) {
    if($("#events_list").text() == "No Transaction Found")
    {
      $("#events_list").html("<li>Txn Hash: " + msg.transactionHash + 
        "\nOwner: " + msg.args.owner + "\nFile Hash: " +
           msg.args.fileHash + "</li>");
    }
    else
    {
      $("#events_list").prepend("<li>Txn Hash: " + msg.transactionHash 
        + "\nOwner: " + msg.args.owner + "\nFile Hash: " + 
          msg.args.fileHash + "</li>");
    }
    });
});

这是前述代码的工作原理:

  • 首先,我们定义了submit()方法。在submit()方法中,我们确保选择了一个文件并且文本字段不为空。然后,我们将文件内容读取为一个数组缓冲区,并将数组缓冲区传递给 sha1.js 中暴露的sha1()方法,以便获得数组缓冲区内的内容的哈希值。一旦我们获得了哈希值,我们就使用 jQuery 向/submit路由发出 AJAX 请求,然后在警告框中显示事务哈希值。

  • 紧接着我们定义了getInfo()方法。它首先确保选择了一个文件。然后,它生成像之前生成的那样的哈希,并请求/getInfo端点以获取有关该文件的信息。

  • 最后,我们使用socket.io库提供的io()方法建立socket.io连接。然后,我们等待连接事件触发,这表示连接已经建立。连接建立后,我们监听来自服务器的消息,并显示交易的详细信息给用户。

我们不把文件存储在以太坊区块链上。存储文件非常昂贵,因为需要很多 gas。在我们的情况下,我们不需要存储文件,因为网络中的节点将能够看到文件;因此,如果用户想保持文件内容的机密性,那么他们将不能。我们应用的目的仅仅是证明文件的所有权,而不是像云服务一样存储和提供文件。

测试客户端

现在运行app.js节点来运行应用程序服务器。打开你喜欢的浏览器,并访问http://localhost:8080/。你将在浏览器中看到这个输出:

现在选择一个文件,输入所有者的姓名,然后点击提交。浏览器窗口将会变成这样:

在下图中,你可以看到交易哈希值已显示。现在等待直到交易被挖掘。一旦交易被挖掘,你将能够在实时交易列表中看到交易。浏览器窗口应该如下所示:

现在再次选择相同的文件,然后点击获取信息按钮。你将会看到以下输出:

在上个截图中,你可以看到时间戳和所有者的详细信息。现在我们已经完成了为我们的第一个 DApp 构建客户端。

摘要

在本章中,我们首先学习了 web3.js 的基础知识,并查看了一些示例。我们了解了连接到节点、基本 API、发送各种类型的交易以及监听事件。最后,我们为我们的所有权合约构建了适当的生产用客户端。现在,您应该能够轻松编写智能合约并为其构建 UI 客户端,以便简化它们的使用。

在下一章中,我们将学习使用零知识安全层实现隐私。

第五章:建立互操作性区块链

有各种许可和公共区块链代表不同的资产、信息和业务流程。使用这些网络的主要需求之一是使它们能够彼此通信,这是一个需要克服的主要挑战。如果只有一个区块链能够统治它们所有,那将是很棒的,但绝对不会发生,因为只有一个区块链无法在安全性、隐私性、效率性、灵活性、平台复杂性、开发人员易用性等方面取得胜利。未来的区块链将是数个公共和许可区块链之间的互操作。本章中,我们将探讨如何实现多个夸梦网络之间的互操作性。

在本章中,我们将涵盖以下主题:

  • 理解区块链互操作性及相关的各种流行项目

  • 研究互操作性区块链可以实现的用例

  • 鉴于可以用于实现区块链互操作性的各种技术和模式

  • 建立代表联邦硬币的互操作性区块链网络

理解区块链互操作性

互操作性的区块链是可以彼此通信的区块链。每个区块链可以读取另一个区块链的状态。很多时候,你可能想要启用你的智能合约与中心化或其他去中心化的应用交互,当我们谈论 DApp 之间的互操作性时,我们在谈论与区块链之间的互操作性有所不同。

使以太坊智能合约能够检查文件是否存在于 IPFS 中,这就实现了 DApp 之间的互操作性,而使以太坊智能合约能够获取比特币账户余额则实现了区块链之间的互操作性。

让以太坊智能合约调用 REST API 以调用中心化应用被认为是在以太坊和 WWW 之间实现互操作性。但在本章中,我们将了解区块链之间的互操作性,特别是夸梦网络之间的互操作性。

目前正在开发的各种流行项目旨在采用去中心化机制实现区块链之间的互操作性。各种流行项目,如CosmosPolkadotInterledgerBlock Collider等,正在积极开发,以使区块链互操作化去中心化和易于实现,但在本章中我们不会涵盖这些项目,因为它们的目标是将互操作性带给公共区块链。然而,我们将学习创建互操作性区块链所使用的策略,正在被这些项目使用。

互操作性的区块链可以实现什么?

在进一步研究如何实现区块链互操作性之前,我们需要知道互操作性区块链可以实现哪些事情。显然,互操作性区块链有许多用例,但我们将重点关注互操作性区块链主要旨在实现的用例。它可以实现以下一个或多个用例:

  • 可移植资产:在不同区块链之间来回转移资产。这也被称为一对一挂钩双向挂钩

  • 支付对支付和支付对交付:技术上称为原子交换。当两个用户交换存在于两个不同区块链中的资产时,需要一个保证,即要么两个转账都发生,要么都不发生。例如,如果一个区块链持有数字化美元,另一个区块链持有数字化欧元,那么用户应该能够原子地交换这些资产。

  • 获取信息并对事件做出反应:一个区块链能够读取另一个区块链上存在的信息,或者对另一个区块链上发生的交易做出反应。例如,一个区块链代表租赁合同,另一个代表锁定的安全押金,因此当第一个区块链中的租赁合同到期时,第二个区块链应自动释放安全押金。

实现区块链互操作性的策略

让我们看看您可以实现前述一个或多个用例的各种策略。我们将学习可以在 Quorum 中实施的策略,但不是所有公共或其他区块链都可用的策略。我们还将看一些如何实现这些策略的示例。

单一托管者

实现互操作性的最简单方法是通过一个集中式第三方,使区块链能够相互通信。基本上,您需要信任这个第三方。

然而,像Oraclize这样的集中式互操作性项目已解决了信任问题。Oraclize 使您的以太坊智能合约能够与万维网进行通信;也就是说,它使您能够进行 REST API 调用,获取比特币账户余额,检查 IPFS 中的文件状态等等。Oraclize 还为智能合约提供了证明,证明结果没有被篡改;因此,Oraclize 解决了信任问题,但单点故障仍然存在。Oraclize 适用于权限和公共以太坊。Oraclize 的主要成就是为以太坊智能合约提供了进行 REST API 调用的能力,但它的目的从未是提供区块链之间的互操作性,因此它不提供此功能。

如果信任对你不是问题的话,那么单一托管策略绝对是你应该考虑的一件事,因为这支持我们先前讨论的所有三种用例。 您可以轻松编写一个集中式应用程序,以响应一个区块链上的事件并在另一个区块链上调用操作。 在得到许可的区块链中,通常有监管机构或权威机构可以选择承载该集中式应用程序。 单一托管方应发送使用预先编程为信任的区块链签名的交易。

多重签名联盟

在区块链之间实现互操作性的更好选择是由一组公证人(或权威)控制多重签名,其中大多数公证人必须批准某项操作才能进行。 这种设置比拥有单一托管方更好,但仍然集中控制。 要实现真正的去中心化,应谨慎选择公证人,至少具有以下特性:

  • 公证人的数量不应低—例如,至少 10 个。

  • 公证人的数量不应太高—例如,少于 30 个,这样用户可以验证公证人的真实性和诚实度。

  • 公证人应分布在不同的法律司法管辖区和国家,以防止国家攻击、胁迫和审查。

  • 为了防止自然灾害发生时基础设施的故障,公证人应该在地理上分布。

  • 公证人应是有名望的。

  • 公证人不应受(或依赖于)较低数量的实体控制。 例如,公证人不能是同一银行的不同分支。

  • 公证人应通过物理和逻辑保护以及所需的安全程序实现和保持指定级别的安全性。

侧链或中继

侧链是一种在一个区块链内部的系统,可以验证和读取其他区块链中的事件和/或状态。 中继是一种更直接的促进互操作性的方法,而不是依赖于可信中介向另一个区块链提供信息,区块链有效地承担了这一任务。 目前,使用Merkle 树来实现侧链系统。

通用的方法如下。假设在区块链 B 上执行的智能合约想要了解区块链 A 上是否发生了某个特定事件,或者在区块链 A 的状态中的某个特定对象在某个特定时间是否包含了某个值。我们可以在区块链 B 上创建一个合约,该合约接受区块链 A 的其中一个区块头,并使用区块链 A 共识算法的标准验证过程来验证这个区块头——在 IBFT 中,这将涉及验证超过 75% 的验证者签名已经签署了区块头。一旦中继验证了区块头已经被确定,中继就可以验证任何所需的交易或账户/状态条目,通过对梅克尔树的单个分支进行区块头验证。

所谓的 轻客户端验证 技术的应用对中继而言是理想的,因为区块链在资源上基本上是受限的。事实上,在同一时间,内部机制无法完全验证区块链 A 并使内部机制完全验证区块链 B,因为数学上简单的原因是两个盒子无法同时包含彼此:A 需要重新运行重新运行 A 的那部分部分,包括重新运行 B 的那部分 A,等等。然而,通过轻客户端验证,一个协议,其中区块链 A 包含区块链 B 的小片段,并且区块链 B 包含区块链 A 的小片段,这些片段是按需拉取的,是完全可行的。在区块链 B 上的一个中继的智能合约想要验证区块链 A 上的特定交易、事件或状态信息时,就像传统的轻客户端一样,会验证区块链 A 的加密哈希树的一个分支,然后通过区块头验证这个分支的根是否在内部,如果这两个检查通过,它会接受该交易、事件或状态信息是正确的。

注意,由于区块链是完全自包含的环境,并且没有自然访问外部世界的能力,因此链 A 的相关数据需要由用户输入到链 B 中;但是,因为数据在密码学意义上是自验证的,输入这些信息的用户无需受信任。

哈希锁定

哈希锁定是一种实现资产的原子交换的技术。它不需要任何中介。哈希锁定的工作原理如下:

  • 假设在两个不同的区块链中有两个名为 AB 的资产。资产 A 的所有者是 X,资产 B 的所有者是 Y

  • 如果他们两个都想交换这些资产,那么首先,X 必须生成一个秘密值 S,并计算秘密值的哈希 H。之后,XY 共享 H

  • 现在,X 锁定资产 A,声明如果 YN 秒内揭示 HS,则资产的所有权将转移到 Y;否则,资产将在 N 秒后解锁。

  • 接下来,Y 锁定资产 B,声明如果 XN/2 秒内揭示 HS,则资产的所有权将转移给 X;否则,资产将在 N/2 秒后解锁。

  • 所以现在,资产 AB 在分别 N 秒和 N/2 秒后被锁定。现在,在 N/2 秒内,XB 的区块链揭示 S 以主张资产的所有权。现在,Y 有同等的时间来了解 S 并向区块链 A 揭示 S 以主张所有权。

X 得到与 Y 给予的时间的一半的原因是只有 X 知道密码,而 Y 锁定资金后,X 可以等到 N 秒快结束时主张资产,这将不给 Y 足够的时间来主张他们的资金。因此,X 可以成功地窃取资产 AB。为了避免这种情况,我们给予 Y N 秒和 X N/2 秒,以便 Y 有与 X 相同的时间来主张资产。

这种技术的缺点是,如果 XN/2 秒和 N 秒之间将 S 揭示给区块链 B,那么 X 将无法主张对 B 的所有权,但 Y 将了解 S 并有时间主张对 A 的所有权。然而,这将是 X 的错,可以避免。

创建 FedCoin

FedCoin 是由中央银行发行的数字货币,与其法定货币一比一进行对冲。使用区块链数字化法定货币有几个好处,例如可以实现便捷的跨境支付,节省了对账工作等。

让我们在两个不同的区块链网络上构建一些数字化的印度卢比和美元。然后,让我们创建一些原子交换合约,以实现这些货币在银行之间的原子交换。这个用例需要您创建两个不同的 Quorum 网络,使用 IBFT 共识。在每个网络中有一个权威,即中央银行,以及 N 个同行,即其他银行。因此,您可以假设在第一个网络中,美联储系统FRS)是权威,美国银行BOA)和 ICICI 银行是同行。同样,在第二个网络中,印度储备银行RBI)是权威,BOA 和 ICICI 银行是同行。

您现在不必构建此网络,因为在构建和测试智能合约时,您只能使用具有四个以太坊账户地址的一个节点。这足以模拟整个场景。

用于数字化法定货币的智能合约

这里是一个基本的智能合约,用于在区块链上创建数字化美元。这个智能合约允许我们发行和转移数字化货币:

pragma solidity ⁰.4.19;

contract USD {

    mapping (address => uint) balances;
    mapping (address => mapping (address => uint)) allowed;
    address owner;

    function USD() {
        owner = msg.sender;
    }

    function issueUSD(address to, uint amount) {
        if(msg.sender == owner) {
            balances[to] += amount;
        } 
    }

    function transferUSD(address to, uint amount) {
        if(balances[msg.sender] >= amount) {
            balances[msg.sender] -= amount;
            balances[to] += amount;
        }
    }

    function getUSDBalance(address account) view returns 
      (uint balance) {
        return balances[account];
    }

    function approve(address spender, uint amount) {
        allowed[spender][msg.sender] = amount;
    }

    function transferUSDFrom(address from, address to, uint amount) {
        if(allowed[msg.sender][from] >= amount && balances[from]
          >= amount) {
            allowed[msg.sender][from] -= amount;
            balances[from] -= amount;
            balances[to] += amount;
        }
    }
}

这是上述代码的工作原理:

  • 首先,我们定义了一个映射来存储每家银行持有的美元数量。每家银行可以有多个地址以实现隐私。这些地址不一定是银行;它们也可以是其他智能合约,因为每个智能合约也有address

  • 接下来,我们假设中央银行部署了合约。因此,我们将中央银行定义为发行方,通过将其address分配给owner

  • 然后,我们定义了一个名为issueUSD的函数,中央银行可以利用它向其他银行发行美元。

  • 然后,我们定义了另一个名为transferUSD的函数,银行可以利用它在彼此之间转移美元。

  • 接下来,我们有一个函数用于读取账户的余额。

  • 最后,我们有两个重要的函数:approvetransferUSDFromtransferUSDFrom函数允许合约代表您发送美元。换句话说,您为同一区块链上的其他智能合约提供了管理您资金的 API。approve函数用于为智能合约提供管理您资金的批准。在调用approve时,您指定了该合约可以管理多少您的资金。

在这里,我们正在使用一个名为view的内置修饰符。view表示该函数无法修改存储,但将读取存储(因此查看)。view函数无法发送或接收以太币。类似地,还有另一个名为pure的修饰符,表示返回值只能依赖于输入参数,即它们甚至不能读取存储,也不能发送或接收以太币。您应该使用这些修饰符,因为它们具有多种好处——例如,在生成用于与合约交互的 UI 表单时,Remix IDE 会寻找这些修饰符。

现在在第二个网络中部署一个类似的合约以数字化印度卢比。在前述合约中用INR替换USD并部署它。应该是这样的:

pragma solidity ⁰.4.19;

contract INR {

    mapping (address => uint) balances;
    mapping (address => mapping (address => uint)) allowed;
    address owner;

    function INR() {
        owner = msg.sender;
    }

    function issueINR(address to, uint amount) {
        if(msg.sender == owner) {
            balances[to] += amount;
        } 
    }

    function transferINR(address to, uint amount) {
        if(balances[msg.sender] >= amount) {
            balances[msg.sender] -= amount;
            balances[to] += amount;
        }
    }

    function getINRBalance(address account) view returns 
      (uint balance) {
        return balances[account];
    }

    function approve(address spender, uint amount) {
        allowed[spender][msg.sender] = amount;
    }

    function transferINRFrom(address from, address to, uint amount) {
        if(allowed[msg.sender][from] >= amount && balances[from] 
          >= amount) {
            allowed[msg.sender][from] -= amount;
            balances[from] -= amount;
            balances[to] += amount;
        }
    }
}

原子交换智能合约

我们已成功数字化了法定货币。现在是实现哈希锁定机制的原子交换智能合约的时候了。我们在每个区块链上都有一个原子交换智能合约部署——也就是说,第一个区块链上的原子交换智能合约将美元锁定一段时间,并期望印度银行(这里是 ICICI 银行)在规定的时间内使用密钥来认领它。同样,第二个区块链上的原子交换合约将在规定的时间内锁定印度卢比,并期望美国银行(这里是 BOA)在规定的时间内使用密钥来认领它。

以下是用于锁定美元的原子交换智能合约:

pragma solidity ⁰.4.19;

import "./USD.sol";

contract AtomicSwap_USD {

    struct AtomicTxn {
        address from;
        address to;
        uint lockPeriod;
        uint amount;
    }

    mapping (bytes32 => AtomicTxn) txns;
    USD USDContract;

    event usdLocked(address to, bytes32 hash, uint expiryTime, 
      uint amount);
    event usdUnlocked(bytes32 hash);
    event usdClaimed(string secret, address from, bytes32 hash);

    function AtomicSwap_USD(address usdContractAddress) {
        USDContract = USD(usdContractAddress); 
    }

    function lock(address to, bytes32 hash, uint lockExpiryMinutes,
      uint amount) {
        USDContract.transferUSDFrom(msg.sender, address(this), amount);
        txns[hash] = AtomicTxn(msg.sender, to, block.timestamp + 
         (lockExpiryMinutes * 60), amount);
        usdLocked(to, hash, block.timestamp + (lockExpiryMinutes * 60),
          amount);
    }

    function unlock(bytes32 hash) {
        if(txns[hash].lockPeriod < block.timestamp) {
            USDContract.transferUSD(txns[hash].from, 
              txns[hash].amount);
            usdUnlocked(hash);
        }
    }

    function claim(string secret) {
        bytes32 hash = sha256(secret);
        USDContract.transferUSD(txns[hash].to, txns[hash].amount);
        usdClaimed(secret, txns[hash].from, hash);
    }

    function calculateHash(string secret) returns (bytes32 result) {
        return sha256(secret);
    }
}

以下是前述智能合约的工作原理:

  • 部署智能合约时,我们提供了USD合约的合约地址,以便它调用其函数来转移资金。

  • lock方法用于使用hash锁定资金。显然,在调用lock方法之前,BOA 必须批准此原子互换合约地址,以便能够访问其一定数量的资金。它接受hash并锁定资金一段时间。 amount被指定为指示锁定多少美元,此金额应小于等于批准的金额。 to地址指定印度银行的地址—即 ICICI 银行。因此,当 ICICI 银行来索取资金时,它们将去到此地址。该函数实际上将资金转移到其合约地址(即address(this))并触发事件,以便 ICICI 银行可以看到资金已被锁定。

  • unlock方法可以被 BOA 用来在hash过期后解锁资金,如果资金没有被索取。

  • claim方法由 ICICI 银行使用秘密索取资金。

  • 最后,我们使用calculateHash方法来计算秘密的hash

这里,我们以 BOA 和 ICICI 为例来简单解释,但之前的智能合约可以与任意数量的货币和银行配合良好运行。

在前述合同中将USD更改为INR,以为第二个区块链提供原子交换智能合约。以下是代码的外观:

pragma solidity ⁰.4.19;

import "./INR.sol";

contract AtomicSwap_INR {

    struct AtomicTxn {
        address from;
        address to;
        uint lockPeriod;
        uint amount;
    }

    mapping (bytes32 => AtomicTxn) txns;
    INR INRContract;

    event inrLocked(address to, bytes32 hash, uint expiryTime,
      uint amount);
    event inrUnlocked(bytes32 hash);
    event inrClaimed(string secret, address from, bytes32 hash);

    function AtomicSwap_INR(address inrContractAddress) {
        INRContract = INR(inrContractAddress); 
    }

    function lock(address to, bytes32 hash, uint lockExpiryMinutes, 
      uint amount) {
        INRContract.transferINRFrom(msg.sender, address(this), amount);
        txns[hash] = AtomicTxn(msg.sender, to, block.timestamp + 
         (lockExpiryMinutes * 60), amount);
        inrLocked(to, hash, block.timestamp + (lockExpiryMinutes * 60), 
          amount);
    }

    function unlock(bytes32 hash) {
        if(txns[hash].lockPeriod < block.timestamp) {
            INRContract.transferINR(txns[hash].from, 
              txns[hash].amount);
            inrUnlocked(hash);
        }
    }

    function claim(string secret) {
        bytes32 hash = sha256(secret);
        INRContract.transferINR(txns[hash].to, txns[hash].amount);
        inrClaimed(secret, txns[hash].from, hash);
    }

    function calculateHash(string secret) returns (bytes32 result) {
        return sha256(secret);
    }
}

测试

现在我们已经准备好在两个不同区块链的资产之间进行原子交换的智能合约了。接下来,让我们编写一些 JavaScript 代码来测试前面的合约并进行原子交换。以下代码允许您执行此操作。为了测试和模拟目的,您可以在一个单独的 Quorum 节点上运行以下代码,该节点具有四个帐户:

var generateSecret = function () {
    return Math.random().toString(36).substr(2, 9);
};

var web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));

var RBI_Address = "0x92764a01c43ca175c0d2de145947d6387205c655";
var FRS_Address = "0xbc37e7ba9f099ba8c61532c6fce157072798fe77";
var BOA_Address = "0x104803ea6d8696afa6e7a284a46a1e71553fcf12";
var ICICI_Address = "0x84d2dab0d783dd84c40d04692e303b19fa49bf47";

var usdContract_ABI = /* Put JSON here */;
var usdContract_Bytecode = "0x606..."
var atomicswapUSD_ABI = /* Put JSON here */;
var atomicswapUSD_Bytecode = "0x606..."
var inrContract_ABI = /* Put JSON here */;
var inrContract_Bytecode = "0x606..."
var atomicswapINR_ABI = /* Put JSON here */;
var atomicswapINR_Bytecode = "0x606..."

var usdContract = web3.eth.contract(usdContract_ABI);
var usd = usdContract.new({
  from: FRS_Address, 
   data: usdContract_Bytecode, 
   gas: "4700000"
}, function (e, contract){
  if (typeof contract.address !== 'undefined') {
    var usdContractAddress = contract.address;
    var usdContractInstance = usdContract.at(usdContractAddress)
    var atomicswap_usdContract = web3.eth.contract(atomicswapUSD_ABI);
    var atomicswap_usd = atomicswap_usdContract.new(usdContractAddress, {
        from: FRS_Address, 
        data: atomicswapUSD_Bytecode, 
        gas: "4700000"
    }, function (e, contract){
        if (typeof contract.address !== 'undefined') {
            var atomicSwapUSDAddress = contract.address;
            var atomicSwapUSDContractInstance =
              atomicswap_usdContract.at(atomicSwapUSDAddress);

            var inrContract = web3.eth.contract(inrContract_ABI);

        var inr = inrContract.new({
            from: RBI_Address, 
            data: inrContract_Bytecode, 
            gas: "4700000"
        }, function (e, contract){
            if(typeof contract.address !== 'undefined') {
                var inrContractAddress = contract.address;
                var inrContractInstance = 
                  inrContract.at(inrContractAddress)
            var atomicswap_inrContract =
              web3.eth.contract(atomicswapINR_ABI);
            var atomicswap_inr = atomicswap_inrContract.new(
                inrContractAddress, {
                from: RBI_Address, 
                data: atomicswapINR_Bytecode, 
                gas: '4700000'
            }, function (e, contract){
                if (typeof contract.address !== 'undefined') {
                    var atomicSwapINRAddress = contract.address;
                    var atomicSwapINRContractInstance = 
                      atomicswap_inrContract.at(atomicSwapINRAddress);

                }
            })
            }
        })
        }
    })
  }
})

首先,我们部署了USD合约,然后通过将USD合约的地址作为参数来部署了 USD 的原子交换合约。我们将这些合约部署为 FRS。然后,我们部署了INR合约,然后通过将INR合约的地址作为参数来部署了 INR 的原子交换合约。我们将这些合约部署为 RBI。

将以下代码放在提到的连续位置:

//Issue USD
usdContractInstance.issueUSD.sendTransaction(BOA_Address, 1000,
  {from: FRS_Address}, function(e, txnHash){

  //Fetch USD Balance
  console.log("Bank of America's USD Balance is : " + 
    usdContractInstance.getUSDBalance.call(BOA_Address).toString())

  //Issue INR
  inrContractInstance.issueINR.sendTransaction(ICICI_Address, 1000,
   {from: RBI_Address}, function(e, txnHash){

    //Fetch INR Balance
    console.log("ICICI Bank's INR Balance is : " + 
      inrContractInstance.getINRBalance.call(ICICI_Address).toString())

    //Generate Secret and Hash
    var secret = generateSecret();
    var hash = atomicSwapUSDContractInstance.calculateHash.call(secret,
      {from: BOA_Address});

    //Give Access to Smart Contract
    usdContractInstance.approve.sendTransaction(atomicSwapUSDAddress,
      1000, {from: BOA_Address}, function(e, txnHash){

      //Give Access to Smart Contract
      inrContractInstance.approve.sendTransaction(atomicSwapINRAddress,
        1000, {from: ICICI_Address}, function(e, txnHash){

        //Lock 1000 USD for 30 min
        atomicSwapUSDContractInstance.lock.sendTransaction(ICICI_Address, hash, 
  30, 1000, {from: BOA_Address, gas: 4712388}, function(e, txnHash){

          //Fetch USD Balance
          console.log("USD Atomic Exchange Smart Contracts holds : " + 
            usdContractInstance.getUSDBalance.call
            (atomicSwapUSDAddress).toString())

          //Lock 1000 INR for 15 min
          atomicSwapINRContractInstance.lock.sendTransaction(BOA_Address,
  hash, 15, 1000, {from: ICICI_Address, gas: 4712388},
  function(e, txnHash){

            //Fetch INR Balance
            console.log("INR Atomic Exchange Smart Contracts holds : "
              + inrContractInstance.getINRBalance.call
              (atomicSwapINRAddress).toString())

            atomicSwapINRContractInstance.claim(secret, {
              from: BOA_Address, gas: 4712388
            }, function(error, txnHash){

              //Fetch INR Balance
              console.log("Bank of America's INR Balance is : " +
                inrContractInstance.getINRBalance.call
                (BOA_Address).toString())

              atomicSwapUSDContractInstance.claim(secret, {
                from: ICICI_Address, gas: 4712388
              }, function(error, txnHash){

                //Fetch USD Balance
                console.log("ICICI Bank's USD Balance is : " +
                  usdContractInstance.getUSDBalance.call
                  (ICICI_Address).toString())
              })

            })

          })
        })
      })

    })
  })

}) 

以下是前面的代码如何运行的:

  1. 这里,FRS 向 BOA 发行了 USD,然后 RBI 向 ICICI 银行发行了 INR。

  2. 然后,BOA 生成了一个秘密。我们使用一个非常基本的函数来生成一个秘密。显然,在实际场景中,您应该使用某种基于硬件的工具来生成这些类型的安全秘密。

  3. 接下来,我们计算了秘密的哈希。

  4. BOA 和 ICICI 银行分别授予了 USD 原子交换和 INR 原子交换合约对其资金的访问权限。

  5. BOA 将 USD 锁定在 USD 原子交换合约中,锁定时间为 30 分钟,并声明只有 ICICI 银行可以索取资金。

  6. 同样,ICICI 银行将 INR 锁定在 INR 原子交换合约中,锁定时间为 15 分钟,并声明只有 BOA 可以索取资金。

  7. 最后,BOA 前去索取 INR。一旦 ICICI 知道了秘密,它立即索取了 USD。

要测试上述合约,首先复制你的以太坊地址,并替换我在之前示例中生成的地址。然后确保在你的节点上解锁所有四个账户。最后,编译合约并填充ABIBytecode变量。

这里,我们使用 Solidity 函数来计算hash,但你也可以使用 JavaScript 来计算hash。如果你想计算sha256哈希值,那么你可以使用任何一个 JavaScript 库,但如果你想像 Solidity 在 JavaScript 中计算sha3(也就是keccak256)一样,那么你需要使用web3.utils库,它提供了一个名为soliditySha3的函数。这个函数会以与 Solidity 相同的方式计算给定输入参数的sha3。这意味着参数将被ABI转换和紧密打包后再进行哈希运算。

摘要

在本章中,我们探讨了构建可互操作的区块链的各种选项。总结起来,单一托管方、多签名联邦以及哈希锁定易于实现,而侧链则复杂且需要大量工程工作。很快,我们将拥有内置侧链支持的生产许可区块链平台。

最后,我们通过模拟两家中央和商业银行来实现了哈希锁定。你可以继续尝试构建两个不同的网络,并尝试进行原子交换。

在下一章中,我们将学习如何构建一个用于 Quorum 的区块链服务器。在构建过程中,我们还将学习 DevOps 和云计算的概念。

第六章:构建 Quorum 作为服务平台

随着使用Kubernetes(K8s)部署容器化应用程序的增长,现在是学习如何将 Quorum 容器化以部署到 K8s 的正确时机。 在本章中,我们将构建一个平台即服务PaaS)以便轻松创建 Quorum 网络。 我们将从云计算、Docker 和 K8s 的基础知识开始,并最终建立一个Quorum 即服务QaaS)平台。 在本章中,我们将构建一个极简的区块链即服务BaaS),与 Azure、AWS 和 BlockCluster 等各种云平台提供的服务相比。

在本章中,我们将涵盖以下主题:

什么是云计算?

  • 公共、私有和混合云之间的区别

  • IaaS、PaaS 和 SaaS 之间的区别

  • 什么是 Docker 和应用程序的容器化?

  • 微服务架构简介

  • 了解 K8s 的基本原理及其优势

  • 在本地计算机上安装 minikube

  • 在 K8s 中部署一个简单的 Hello World Node.js 应用程序

  • 将 Quorum 容器化为 K8s

  • 使用 Docker 和 K8s 构建 QaaS 平台

云计算简介

简单来说,云计算是通过互联网提供计算服务(服务器、存储、数据库、网络、软件等)的按需交付。

云计算提供了更容易访问服务器、存储、数据库和广泛的应用服务的途径,这些服务都可以通过互联网获得。 云服务平台,如亚马逊网络服务微软 Azure,拥有并维护这些应用服务所需的网络连接硬件,而您则通过 Web 应用程序进行配置和使用所需的资源。

以下是云计算的优势:

  • 成本:云计算节省了很多成本,因为您不必购买硬件和软件。 它还节省了您在现场数据中心的设置和运行成本。 即使您设置了自己的数据中心,您也需要能够管理它们的 IT 专家,以及全天候的电力和冷却,这会增加额外的成本。 相比之下,云计算非常便宜。 在云计算中,您只有在使用资源时才需要付费,并且您只需要支付您使用的数量。

  • 速度:云计算可以节省时间,因为您可以在需要时立即运行服务; 它提供按需提供计算服务。

  • 全球扩展:您可以轻松地在多个地区部署您的应用程序。 这让您的应用程序靠近用户。

还有其他各种好处,取决于您使用的云计算提供商。

私有与公共与混合云

云解决方案可以是私有的、公共的或混合的,根据数据中心的所有权和位置。 云解决方案通常是公共的,也就是说,任何有互联网访问权限的人都可以使用云提供的计算服务。 我们之前看到的所有好处都是公共云提供的好处。

尽管公共云在配置计算服务时允许您选择您的区域,但可用区域的总数仍然非常有限。这是银行、军队和政府等实体的一个关注点,因为它们要么不希望数据离开他们的国家,要么不希望云提供商能够看到数据。因此,这些实体要么选择私有云,要么选择混合云。

当云托管在企业自己的数据中心时,称为私有云。在这种情况下,企业无法享受成本和多区域扩展的好处,因为它们负责配置和维护数据中心。

混合云术语用于企业根据技术和业务需求使用私有云和公共云的混合组合。企业可能选择将应用程序托管在公共云上,同时由于合规性或安全问题,将与应用程序相关的一些数据保留在私有云中。

IaaS 与 PaaS 和 SaaS 之间的区别

基础设施即服务IaaS)、平台即服务PaaS)和软件即服务SaaS)是基于您管理的内容以及云提供商为您管理的内容而划分的三种不同类别的云解决方案。

在 IaaS 中,云提供商为客户提供按需访问基本计算服务,即存储、网络和服务器。其他所有事项都由您来配置和管理。Amazon AWS、Google Cloud、Azure、Linode 和 Rackspace 是 IaaS 的示例。

在 PaaS 中,云提供商管理操作系统、编程语言的运行时、数据库和 Web 服务器——也就是为开发、测试和管理应用程序提供环境。简单来说,你只需要担心编写代码和业务方面的可扩展性。应用程序开发和部署的其余基础设施由云提供商处理。Heroku、Redhat 的 OpenShift、Apache Stratos 和 Google App Engine 是 PaaS 的示例。

数据库即服务DBaaSBaaS)属于 PaaS 类别。因此,在本章中,我们将创建一个简单的 PaaS:QaaS。任何管理应用程序依赖的服务(如数据库、区块链或消息队列)的云解决方案都是 PaaS。

在 SaaS 中,云提供商管理一切,包括数据和应用程序。您不需要编写任何代码来构建应用程序。云提供商提供一个界面,根据您的需求定制应用程序并部署它。使用 SaaS 往往会通过消除技术人员定期管理、编写代码和升级软件的需求来降低软件拥有成本。您只需担心业务逻辑。Salesforce、Google Apps 和 WordPress.com 是 SaaS 的示例。

上述图像可用于轻松确定云解决方案是 IaaS、PaaS 还是 SaaS。

一些云解决方案提供了 IaaS 和 PaaS 的功能。例如,AWS 最初是一个 IaaS,现在它还提供了各种按需服务(如区块链和弹性搜索)。

什么是容器?

如果你正在使用 PaaS 或 SaaS 来创建你的应用程序,那么你不会遇到容器,因为它们会负责容器化你的应用程序。PaaS 只是让你将应用程序的源代码推送到云端,并为你构建和运行应用程序。

如果你正在使用 IaaS 来构建你的应用程序,那么如果不将你的应用程序容器化,要扩展和管理你的应用程序将变得几乎不可能。让我们来看一个场景,并试着理解为什么我们需要容器。

在 IaaS 中,要部署你的应用程序,你需要执行以下步骤:

配置虚拟机(Virtual MachinesVMs

  1. 安装应用程序的所有依赖项和运行时环境

  2. 运行应用程序

  3. 如果应用程序开始接收的流量超过 VM 能够处理的范围,你将会开始创建新的 VM,并使用负载均衡器分发流量

  4. 对于每个新的 VM,你需要在运行新的应用程序实例之前按照相同的流程安装依赖项和运行时环境

这种滚动创建新的 VM 并在其中运行应用程序实例的过程容易出错且耗时。这就是容器发挥作用的地方。

简而言之,容器是一种打包应用程序的方式。容器的特殊之处在于当你将它们移动到新的机器或环境中时不会出现意外错误。你的应用程序的所有代码、库和依赖项都被打包在容器中作为一个不可变的工件。你可以将运行容器看作运行 VM,但不会带来启动整个操作系统的开销。因此,将应用程序打包在容器而不是 VM 中,将显著提高启动时间。容器比 VM 轻量级得多,使用的资源也少得多。

因此,对于上述示例,你需要为你的应用程序创建一个容器,并在每个 VM 中运行容器。显然,根据你的应用程序架构,一个 Docker 容器可以运行多个进程,一个 VM 可以运行多个容器。

在内部,PaaS 和 SaaS 使用容器来打包和部署你的应用程序。容器还有许多其他用途。例如:一个编码测试应用实际上在执行之前会将你的代码容器化,以便在隔离的环境中执行代码。

通过容器化应用程序及其依赖项,OS 分发和底层基础设施的差异被抽象化了。容器可以在裸机系统、云实例和 Linux、Windows 和 macOS 上的 VM 上运行。

Docker 简介

Docker 帮助您在容器内创建和部署软件。它是一套开源工具集,可帮助您构建、发布和运行任何应用程序。使用 Docker,您可以在应用程序源代码目录中创建一个特殊的文件,称为 Dockerfile。 Dockerfile 定义了一个构建过程,当输入到docker build 命令时,将生成一个不可变的 Docker 镜像。您可以将 Docker 镜像视为 VM 镜像。当您想要启动它时,只需使用 docker run 命令在 Docker 守护程序受支持并运行的任何地方运行它。Docker 容器是 Docker 镜像的运行实例。

在 Dockerfile 中,您需要提及一个应该运行的命令,然后容器启动。这就是容器内部执行实际应用程序的方式。如果命令存在,则容器也会关闭。当容器关闭时,所有写入容器卷中的数据都会丢失。

Docker 还提供了一个名为 Docker Hub 的基于云的仓库。您可以将其视为 Docker 镜像的 GitHub。您可以使用 Docker Hub 创建、存储和分发您构建的容器镜像。

构建一个 Hello World Docker 容器

让我们创建一个 Docker 镜像,该镜像打包了一个简单的 Node.js 应用程序,该应用程序公开了一个端点以打印 Hello World。在继续之前,请确保您已在本地计算机上安装了 Docker CE(社区版)。您可以在docs.docker.com/install/找到根据不同操作系统安装和启动 Docker 的说明。

现在创建一个名为hello-world 的目录,并在其中创建一个名为 app.js 的文件。在该文件中放置以下内容:

const http = require('http');

const name = 'node-hello-world';
const port = '8888';

const app = new http.Server();

app.on('request', (req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.write('Hello World');
  res.end('\n');
});

app.listen(port, () => {
  console.log(`${name} is listening on port ${port}`);
});

现在在相同的目录中创建一个名为 Dockerfile 的文件,并将以下内容放入其中:

FROM node:carbon

WORKDIR /usr/src/app

COPY . ./

EXPOSE 8888

CMD [ "node", "app.js" ]

我们将构建 Docker 镜像的指令放在 Docker 文件中。您可以在docs.docker.com/engine/reference/builder/找到指令列表。

以下是之前的 Dockerfile 如何工作的:

  • 首先,您需要定义从哪个镜像构建。在这里,我们将使用 Docker Hub 提供的最新的 长期支持(LTS) 版本的 Node 的碳镜像。该镜像已经安装了Node.jsnpm

  • 接下来,我们创建一个目录来存放图像内部的应用程序代码; 这将是您应用程序的工作目录。

  • 要将您的应用程序源代码捆绑到 Docker 镜像中,我们使用COPY 指令。这里表示我们正在从当前主机操作系统的工作目录复制到 Docker 的工作目录。

  • 您的应用程序绑定到端口8888,因此您将使用 EXPOSE 指令让 Docker 守护程序进行映射。

  • 最后但同样重要的是,使用 CMD 定义运行应用程序的命令,该命令定义了您的运行时。

这是如何构建 Docker 镜像的:

  1. 使用docker build -t nodejs-hello-world . 命令构建 Docker 镜像。

  2. 要运行容器,请运行docker run -p 8090:8888 -d nodejs-hello-world 命令。

  3. -p选项将容器的端口8888绑定到主机机器的127.0.0.1的 TCP 端口8090。您也可以指定 udp 和 sctp 端口。在您的 web 浏览器中访问http://localhost:8090/,您将看到 Hello World 消息。

在 Dockerfile 中,使用ENTRYPOINT定义命令,使用CMD定义命令的参数。默认入口点是["/bin/sh", "-c'],实际运行的是sh shell。因此,在上述 Dockerfile 中,主命令是启动sh shell,并将要运行的应用程序命令作为子命令传递。-c选项接受要在sh shell 内运行的命令。

理解微服务架构

微服务架构是用于构建企业级应用程序的应用程序架构。要理解微服务架构,首先需要了解其相反的单体架构。在单体架构中,服务器端应用的不同功能组件(例如支付处理,账户管理,推送通知等组件)都融合在单个单元中。

例如,应用程序通常分为三个部分。部分是在用户计算机上运行的 HTML 页面或本地 UI,一种在服务器上运行的服务器端应用,以及在服务器上也运行的数据库。服务器端应用负责处理 HTTP 请求,在数据库中检索和存储数据,并执行算法。如果服务器端应用是一个单独的可执行文件(即,运行在单个进程中)来完成所有这些任务,那么我们说服务器端应用是单体的。这是构建服务器端应用的常见方式。几乎每个主要的 CMS、Web 服务器和服务器端框架都是使用单体架构构建的。这种架构可能看起来很成功,但当您的应用程序庞大复杂时,可能会出现问题。

在微服务架构中,服务器端应用被分为服务。一个服务(或微服务)是完整服务器端应用的特定功能的小型独立进程的组成部分。例如,您可以有一个用于支付处理的服务,另一个用于账户管理的服务,依此类推;服务需要通过网络相互通信。

服务可以通过 REST API 或消息队列相互通信,具体取决于您是否需要通信是同步还是异步的。

以下是使用微服务架构的一些好处:

  • 因为服务通过网络通信,它们可以使用不同的编程语言和不同的框架编写

  • 对服务进行更改只需要重新部署特定的服务,而不是所有服务,这是一种更快的流程。

  • 由于每个服务在不同的进程中运行,因此更容易衡量每个服务消耗了多少资源。

  • 它变得更容易测试和调试,因为可以分析每个服务。

  • 服务可以通过网络调用与其他应用程序重复使用。

  • 小团队可以并行工作,比大团队可以更快地迭代。

  • 较小的组件占用的资源较少,可以按需缩放以满足该组件的需求增加

不必在不同的虚拟机中运行每个微服务,也就是说,可以在单个虚拟机中运行多个服务。服务器与服务的比例取决于不同的因素。一个常见的因素是所需的资源和技术的数量和类型。例如,如果一个服务需要大量的 RAM 和 CPU 时间,最好是在服务器上单独运行它。如果有一些不需要太多资源的服务,可以一起在单个服务器上运行它们。

深入 K8s

一旦创建了几个 Docker 容器,您会意识到缺少了一些东西。如果要在多台机器上运行多个容器 - 如果您使用微服务,这是必须要做的 - 那么仍然有很多工作要做。

您需要在正确的时间启动正确的容器,找出它们如何相互通信,处理存储方面的考虑,并处理失败的容器或硬件。如果手动执行所有这些工作将是一场噩梦。幸运的是,这就是 K8s 发挥作用的地方。

K8s 是一个开源的容器编排平台,可以使大量的容器在一起协同工作,从而减轻运维负担。它有助于诸如:

  • 在许多不同的机器上运行容器。

  • 根据需求的变化增加或删除容器以进行扩展或缩减。

  • 保持多个应用程序实例的存储一致。

  • 在容器之间分配负载。

  • 如果有什么失败,可以在不同的机器上启动新容器,也就是自动修复。

  • 与 K8s 兼容的应用程序可以在不更改应用程序源代码的情况下轻松地由一个 IaaS 移动到另一个 IaaS。应用程序部署在 K8s 集群上,K8s 集群部署在 IaaS 上。

从开发者的角度来看,在 K8s 集群中有两种类型的机器:主节点和节点(也称为工作节点)。我们的应用程序在节点上运行,而主节点控制节点并公开 K8s API。可以在裸机上或在虚拟机上安装 K8s。还有可用的 Kubernetes 作为服务云解决方案,可以按需为您创建集群。例如:Google Cloud 的 Kubernetes Engine,Azure Kubernetes ServiceAKS)和亚马逊弹性容器服务 for KubernetesAmazon EKS)。

进入资源对象

您可以使用 K8s API 通过 K8s API 端点读取、写入和更新 K8s 资源对象。K8s 资源对象是用于表示集群状态的实体。我们需要使用清单来定义资源对象。在 API 调用中,我们传递清单文件的内容。

这是 K8s API 提供的资源的基本类别的高级概述。它们的主要功能如下:

  • 工作负载:这些资源用于在集群上管理和运行您的容器。例如:部署、Pod、作业和副本集。

  • 发现和负载平衡:这些资源用于将您的工作负载组合成一个外部可访问的、负载平衡的服务。例如:服务和入口。

  • 配置和存储:这些资源用于向您的应用程序注入初始化数据,并持久保存容器外的数据。例如:配置映射、秘密和卷。

  • 集群:这些对象定义了集群本身的配置方式;这些通常只被集群操作员使用。

  • 元数据:这些资源用于配置集群中其他资源的行为。例如:网络策略和命名空间。

Dockerfile 允许您指定关于如何运行容器的大量信息,比如要公开的端口、环境变量以及容器启动时要运行的命令。但是 K8s 建议您将这些信息移到 K8s 清单文件中,而不是 Dockerfile 中。现在,Dockerfile 只指定了如何构建和打包应用程序。此外,K8s 清单会覆盖 Dockerfile 中的指令。

部署和 Pod

K8s 鼓励您将部署视为微服务的表示。例如:如果您有五个微服务,您需要创建五个部署,而一个 Pod 是一个微服务的实例。假设您想运行三个微服务实例并在它们之间分配流量,那么在部署中您将定义您需要三个副本,这将创建三个 Pod。一个 Pod 运行一个或多个代表微服务的容器。

在创建部署时,您可以指定微服务需要的计算资源量,比如内存和 CPU,而不是让它消耗所有可用资源。您还可以指定一个节点名称来运行 Pod,而不是由 K8s 决定。

在创建部署时,您可以指定要公开的 Docker 容器的哪些端口、环境变量和其他各种在 Dockerfile 中也指定的内容。

服务

默认情况下,部署之间无法相互通信。服务被创建用于启用微服务之间的通信,并可选地允许从集群外部访问微服务。我们需要为每个部署创建一个服务。服务具有内置的负载均衡功能:如果一个微服务有三个 pod,那么 K8s 服务会自动在它们之间分配流量。以下是各种类型的服务:

  • ClusterIP:这是默认的服务类型。在集群中的内部 IP 上暴露服务。此类型使得服务仅可从集群内部访问。

  • NodePort:使服务可以从集群外访问。它是ClusterIP的超集。当我们创建一个类型为NodePort的服务时,K8s 会在30000-32767范围内打开一个或多个端口(取决于 Docker 容器暴露的端口数量),并将它们映射到所有工作节点的容器端口。因此,如果一个微服务的实例没有在运行,比如说在第三台机器上,仍然可以在第三台机器上暴露端口。K8s 处理内部路由。因此,您可以使用任何工作节点的公共 IP 与分配的端口来访问微服务。如果您不希望 K8s 在外部暴露时在30000-32767之间选择随机端口,则可以指定同一范围内的一个端口。

  • LoadBalancer:也用于在集群外部暴露服务。它将在服务前面启动一个负载均衡器。这仅在支持的云平台上有效,例如 AWS、GCP 和 Azure。

Ingress 控制器和资源

Ingress 是一个用于在集群外部负载均衡和暴露微服务的 K8s 功能。与 NodePort 和 LoadBalances 相比,它是功能丰富且推荐的负载均衡和暴露微服务的方式。Ingress 为您提供了一种根据请求主机或路径路由请求到服务的方式,从而将许多服务集中到单个入口点中,这样更容易管理大型应用程序。Ingress 还支持 SSL 卸载、URL 重写和许多其他功能,因此您不必在创建每个微服务时集成所有这些功能。

Ingress 分为两个主要部分:Ingress 控制器和资源。Ingress 控制器是暴露在集群外部的实际反向代理,Ingress 资源是控制器的配置。Ingress 控制器本身是一个微服务,也就是说,它是一个部署,并为其创建了一个类型为NodePortLoadBalancer的服务。Ingress 控制器具有读取 Ingress 资源并重新配置自身的能力。

有各种不同的 Ingress 控制器实现可用,你应该选择最适合你目的的那个。它们根据特性和使用的负载均衡器和反向代理软件而变化。K8s 官方开发了 NGINX Ingress 控制器,它是 K8s 最常见的 Ingress 控制器。该 Ingress 控制器实现使用了 NGINX 反向代理和负载均衡器。

在部署 Ingress 控制器时,你可以有一个以上的副本以获得高可用性和 Ingress 的负载均衡。你也可以部署多个 Ingress,它们使用类别进行区分。

配置映射和密码

几乎每个应用程序在运行之前都需要传递某种类型的配置。例如,当启动一个 Node.js 应用程序时,你可能需要传递 MongoDB 的 URL,因为你不能硬编码它,因为它在开发和生产环境中不同。这些配置通常作为环境变量或配置文件提供。

K8s 允许你在部署清单中指定环境变量。但是,如果你想要更改它们,你必须修改部署。更糟糕的是,如果你想要在多个部署中使用变量,你必须复制数据。K8s 提供了配置映射(用于非机密数据)和密码(用于机密数据)来解决这个问题。

密码和配置映射的主要区别在于密码使用 Base64 编码进行混淆。现在,你可以将配置映射和密码作为部署清单的环境变量传递。当配置映射或密码发生更改时,环境变量也会相应更改,无需任何重启或手动操作。

如果你的应用程序使用配置文件而不是环境变量,它们也可以使用配置映射和密码进行传递。

绑定挂载和卷

在 K8s 和 Docker 中,绑定挂载是将主机上的文件或目录挂载到容器中的过程。文件或目录通过主机上的完整或相对路径引用。

在计算机数据存储中,卷是具有单个文件系统的持久存储区域,通常(尽管不一定)驻留在硬盘的单个分区上。IaaS 提供商允许我们创建卷并附加到 VM。K8s 提供了名为持久卷和持久卷声明的功能,可以自动创建特定云提供商的卷并附加到 pod。当你的应用程序需要保存(持久化)数据时,卷是必需的。这些卷通过绑定挂载在 Docker 容器内部访问。

在 K8s 中,有一个名为StatefulSets的资源对象,它类似于部署。如果你的部署需要持久性存储,并且你有多个副本,那么你必须创建 StatefulSets 而不是部署,因为部署不能为每个 pod 分配单独的持久性卷。

标签和选择器

标签是附加到资源对象(例如 pod、service 和 deployment)的键/值对。标签用于指定对象的识别属性,这些属性对用户来说是有意义且相关的。标签可用于组织和选择对象的子集。在创建时间或随后的任何时间,可以向对象添加和修改标签。每个对象可以定义一组键/值标签。例如,在创建服务时,我们使用标签和选择器指定应该暴露的 pod 列表。

开始使用 minikube

当您构建真实的应用程序时,正确使用 K8s 的方式是在本地或云端创建一个开发集群,具体取决于您是将应用程序托管在本地还是云上。但是,为了对 K8s 进行实验和玩耍,您可以使用 minikube。

Minikube 是一个工具,可以方便地在本地运行 K8s。Minikube 在您的笔记本电脑上的虚拟机内运行单个工作节点 K8s 集群,供用户尝试 K8s 或进行日常开发使用。在撰写本书时,minikube 的最新版本是 0.26.1。Minikube 可以安装在 Windows、macOS 和 Ubuntu 上。

在 macOS 上安装 minikube

首先,安装 minikube 支持的虚拟机监视程序。在 macOS 上,建议使用 hyperkit。使用以下命令安装 hyperkit 驱动程序:

curl -LO https://storage.googleapis.com/minikube/releases/latest/docker-machine-driver-hyperkit \
&& chmod +x docker-machine-driver-hyperkit \
&& sudo mv docker-machine-driver-hyperkit /usr/local/bin/ \
&& sudo chown root:wheel /usr/local/bin/docker-machine-driver-hyperkit \
&& sudo chmod u+s /usr/local/bin/docker-machine-driver-hyperkit

然后安装 kubectlkubectl 是一个命令行工具,用于部署和管理 K8s 上的应用程序。以下是安装它的命令:

brew install kubectl

现在,使用以下命令安装 minikube:

curl -Lo minikube https://storage.googleapis.com/minikube/releases/v0.26.1/minikube-darwin-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/

在 Ubuntu 上安装 minikube

在 Ubuntu 上,建议使用 hyperkit。使用以下命令安装 hyperkit:

curl -LO https://storage.googleapis.com/minikube/releases/latest/docker-machine-driver-hyperkit \
&& chmod +x docker-machine-driver-hyperkit \
&& sudo mv docker-machine-driver-hyperkit /usr/local/bin/ \
&& sudo chown root:wheel /usr/local/bin/docker-machine-driver-hyperkit \
&& sudo chmod u+s /usr/local/bin/docker-machine-driver-hyperkit

然后安装 kubectl。以下是安装它的命令:

sudo snap install kubectl --classic

现在,使用以下命令安装 minikube

curl -Lo minikube https://storage.googleapis.com/minikube/releases/v0.26.1/minikube-linux-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/

在 Windows 上安装 minikube

在 Windows 上,建议使用 VirtualBox 虚拟机监视程序。从 www.virtualbox.org/wiki/Downloads 下载并安装 VirtualBox。

然后从 storage.googleapis.com/kubernetes-release/release/v1.10.0/bin/windows/amd64/kubectl.exe 下载 kubectl 命令。

最后,通过下载并运行 minikube 安装程序来安装 minikube,网址为 github.com/kubernetes/minikube/releases/download/v0.26.1/minikube-installer.exe

启动 minikube

在 Linux 和 macOS 上,使用以下命令启动 minikube:

minikube start --vm-driver=hyperkit

在 Windows 上,使用以下命令启动 minikube:

minikube start --vm-driver=virtualbox

如果您使用不同的虚拟机监视程序,请更改 --vm-driver 选项的值。启动 minikube 需要几分钟。

停止和删除 minikube

如果您想要随时停止 minikube 集群,可以使用以下命令:

minikube stop

你可以使用上述 minikube start 命令重新启动相同的集群。如果想删除整个集群,则可以使用以下命令:

minukube delete

Minikube 状态

要检查 minikube 的状态,即集群是否正在运行,可以使用以下命令:

minikube status

如果成功运行,你将看到类似于这样的响应:

minikube: Running
cluster: Running
kubectl: Correctly Configured: pointing to minikube-vm at 192.168.64.7

注意这里你会看到一个不同的 IP 地址。这是 minikube 虚拟机的 IP 地址;也就是说,主节点和工作节点在这个虚拟机内运行。你将从这个 IP 访问你的应用。

访问 K8s 仪表板

K8s 仪表板是一个通用的、基于 Web 的 K8s 集群 UI。它允许用户管理运行在集群中的应用程序,并对其进行故障排除,还有集群本身。要访问仪表板,请运行此命令:

minikube dashboard

它将在新的浏览器窗口中打开仪表板。K8s 仪表板将类似于以下内容:

在 k8s 上部署 Hello World 应用

让我们将之前构建的 Hello World Docker 镜像部署到我们刚创建的 K8s 集群上。要创建一个部署和服务,你需要创建一个包含有关部署和服务的所有详细信息的清单文件,然后使用 kubctl 命令将其传递给 K8s。在清单文件中,你需要提供 Docker 镜像的远程 URL,以便 K8s 拉取并运行这些镜像。K8s 可以从公共 Docker 注册表(即 Docker Hub)或私有 Docker 注册表中拉取镜像。

将镜像推送到 Docker Hub

在我们推送镜像之前,让我们了解一些与 Docker 相关的基本术语:

  • 注册表:存储你的 Docker 镜像的服务。

  • 仓库:不同 Docker 镜像的集合,它们具有相同的名称但具有不同的标签(版本)。

  • 标签:你可以使用它来区分 Docker 镜像的不同版本,以便保留旧副本。当我们之前创建 Docker 镜像时,我们没有提供标签,因此默认标签是 latest。可以使用 docker tag [:HOST|:USERID]IMAGE_NAME[:TAG_NAME] [:HOST|:USERID]IMAGE_NAME[:TAG_NAME] 命令从另一个镜像创建一个带标签的新镜像。主机前缀是可选的,用于指示 Docker 注册表的主机名,如果镜像属于私有 Docker 注册表。如果镜像用于 Docker Hub,则提及你的 Docker Hub 帐户的用户名。

要将镜像推送到 Docker Hub,你首先需要创建一个 Docker Hub 帐户。访问 hub.docker.com 并创建一个帐户。登录后,你将看到类似以下的屏幕:

现在点击“创建仓库”并填写以下表格:

可见性指示存储库是私有还是公共的。私有存储库不对所有人可见。如果您有权限访问它,则需要登录到 Docker Hub 才能拉取它。您只能在 Docker Hub 上创建一个免费私有存储库。创建存储库后,您将看到一个类似于此的屏幕:

要推送您在本地计算机上的映像,您需要首先从命令行登录到 Docker Hub。要执行此操作,请运行以下命令:

docker login

然后在提示时键入您的 Docker Hub 帐户的用户名和密码。您应该看到登录成功的消息。现在使用以下命令为您的映像打标签:

docker tag nodejs-hello-world:latest narayanprusty/nodejs-hello-world

现在运行以下命令来推送映像:

docker push narayanprusty/nodejs-hello-world

根据您的互联网带宽不同,推送可能需要几分钟时间。推送完成后,点击存储库上的 Tags 选项卡,您将看到一个类似于此的屏幕:

创建部署和服务

现在,让我们创建包含有关部署和服务信息的主要清单文件。我们可以为我们的部署和服务创建两个不同的或单个部署文件。主要清单文件可以用 YAML 或 JSON 格式编写。首选 YAML,因此我们也将以 YAML 格式编写。

创建一个名为 helloWorld.yaml 的文件,并将以下内容放入其中:

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: hello-world
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: hello-world
    spec:
      containers:
      - name: nodejs-hello-world
        image: narayanprusty/nodejs-hello-world
        command: [ 'node', 'app.js']
        workingDir: /usr/src/app
        imagePullPolicy: Always
        ports:
        - containerPort: 8888
---

kind: Service
apiVersion: v1
metadata:
  name: hello-world
spec:
  ports:
    - name: api
      port: 8888
  selector:
      app: hello-world
  type: NodePort

在前面的主要清单文件中,大多数内容都是不言自明的。在这里,您会注意到我们有一个名为 imagePullPolicy 的字段。默认的映像拉取策略是 IfNotPresent,这会导致 K8s 如果映像已存在则跳过拉取。如果您想始终强制拉取,可以使用 Always 策略、:latest 标签或不带标签。

command 在 K8s 中与 Dockerfile 的 ENTRYPOINT 相同。K8s 中的 arguments 与 Dockerfile 中的 CMD 相同。如果未为容器提供命令或 args,则使用 Docker 映像中定义的默认值。如果为 Container 提供了命令但没有 args,则仅使用提供的命令。忽略 Docker 映像中定义的默认 ENTRYPOINT 和默认 CMD。如果仅为 Container 提供了 args,则使用 Docker 映像中定义的默认 ENTRYPOINT 运行您提供的 args。如果提供了命令和 args,则忽略 Docker 映像中定义的默认 ENTRYPOINT 和默认 CMD。将使用您的 args 运行您的命令。

现在使用以下命令将主要清单提供给 K8s:

kubectl apply -f helloWorld.yaml

apply 子命令用于将主要清单文件提供给 K8s。如果要更新部署或服务配置,请更改文件并重新运行命令。在成功执行上述命令后,打开 K8s 仪表板,您将看到部署和服务已成功创建。

现在,要向容器发出 HTTP 请求,我们需要服务提供的 worker 节点 IP 和端口号。使用 minikube ip 命令查找 IP,并在 K8s 仪表板中打开服务以查找暴露的端口号,如下面的屏幕截图所示:

在我的情况下,端口号是 31474。您将看到不同的端口号。使用端口号和 IP 在浏览器中发出请求,您将看到Hello World消息。

要删除部署,使用 kubectl delete deployment deployment_name 命令,并且要删除服务,请使用 kubectl delete svc service_name 命令。

构建 QaaS

现在让我们开始构建一个 QaaS 平台,这样我们就可以通过点击按钮来部署、创建和加入网络。正如您所知,启动 Quorum 节点需要许多手动步骤,如创建 genesis.json 文件、 static-nodes.json 文件和 enode。由于我们的目标是自动执行所有这些步骤,因此我们需要编写自动化脚本来执行这些步骤。因此,我们将使用Quorum Network ManagerQNM),它允许用户轻松创建和管理 Quorum 网络,无需任何手动步骤。

QNM 是 Quorum 的开源封装,旨在简化 Quorum 网络的设置。当您使用 QNM 时,您不再需要担心 enode、钱包、创世文件、static-nodes.json 文件等。您可以在官方 QNM 仓库找到。目前,QNM 的最新版本是 v0.7.5-beta

请注意,QNM 目前仅与 Ubuntu 16.04 兼容。

在我们的 QaaS 中,我们将 Quorum 节点部署为 K8s 中的部署。每当您想要启动网络或加入现有网络时,都会创建一个新的部署。QNM 未经容器化,因此我们构建 QaaS 的第一步是对其进行容器化。

QNM 如何工作?

在将 QNM 容器化之前,让我们了解一下它的工作原理。第一步是安装 QNM。有两种安装 QNM 的方式:通过运行提供的安装脚本(setup.sh 文件)或手动安装。我们将通过运行脚本来安装它。该脚本会负责安装使用 QNM 所需的所有内容。

您可以使用 node setupFromConfig.js 命令使用 QNM 启动 Quorum 节点。运行 QNM 节点时提供配置的两种方式:使用 config.js 文件或使用环境变量。您还可以使用 node index.js 命令启动节点,这将提供一个交互式方式来配置节点。

在 QNM 中,要创建一个网络,您必须执行以下步骤:

  1. 创建一个协调节点

  2. 动态地向网络添加节点

网络的第一个节点应该是协调节点;其他动态添加的节点是非协调节点。其他动态添加的节点连接到协调节点以获取与网络相关的信息和配置。

唯一需要注意的是,在启动第一个节点时,确保它是一个协调节点。在启动其他动态节点时,请确保提供协调节点的 IP 地址。

剩下的流程由 QNM 自动处理。

将 QNM 容器化

Dockerfile 用于将 QNM Docker 化,将涉及安装 QNM。以下是 Dockerfile 的内容:

FROM ubuntu:16.04

#Install Utilities
RUN apt-get update
RUN apt-get install -y --no-install-recommends vim less net-tools inetutils-ping wget curl git telnet nmap socat dnsutils netcat tree htop unzip sudo software-properties-common jq psmisc iproute python ssh rsync gettext-base

# Install QNM
RUN mkdir -p workspace && cd workspace && wget https://raw.githubusercontent.com/ConsenSys/QuorumNetworkManager/v0.7.5-beta/setup.sh && chmod +x setup.sh && ./setup.sh
ENV LANGUAGE=en_US.UTF-8 LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
RUN apt-get install -y locales && locale-gen en_US.UTF-8

WORKDIR /workspace/QuorumNetworkManager
ENTRYPOINT ["/bin/bash", "-i", "-c"]

以下是前述 Dockerfile 的工作方式:

  • 我们使用了 Ubuntu 16.04 基础镜像。

  • 我们安装了几个基本工具。

  • 我们使用给定的命令安装了 QNM,命令位于 github.com/ConsenSys/QuorumNetworkManager/releases/tag/v0.7.5-beta

  • 我们将工作目录设置为 workspace/QuorumNetworkManager,在其中有启动节点的 QNM 文件。

  • 我们更改了入口点以使用 bash shell 而不是 sh shell,因为 QNM 在 sh shell 上不起作用。QNM 在交互模式下执行时将路径设置为 ~/.bashrc 文件中的各种二进制文件,该文件由 bash shell 加载。

继续将镜像推送到 Docker Hub。我已经将镜像推送到 narayanprusty/qnm

创建 QNM 部署和服务主清单文件

让我们编写为创建 QNM 的部署和服务的主清单文件。我们将仅创建用于创建 Raft 网络的部署,但是你可以轻松扩展以支持 IBFT。

下面是为基于 Raft 的协调节点创建部署和服务的主清单文件:

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: coordinator
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: coordinator
    spec:
      containers:
      - name: qnm
        image: narayanprusty/qnm
        args: ['node setupFromConfig.js']
        workingDir: /workspace/QuorumNetworkManager
        imagePullPolicy: Always
        env: 
        - name: IP
          value: 0.0.0.0
        ports:
        - containerPort: 50000
        - containerPort: 50010
        - containerPort: 50020
        - containerPort: 20000
        - containerPort: 20010
        - containerPort: 20020
        - containerPort: 40000
        - containerPort: 30303
        - containerPort: 9000
---

kind: Service
apiVersion: v1
metadata:
  name: coordinator
spec:
  ports:
    - name: remote-communication-node
      port: 50000
    - name: communication-node-rpc
      port: 50010
    - name: communication-node-ws-rpc
      port: 50020
    - name: geth-node
      port: 20000
    - name: geth-node-rpc
      port: 20010
    - name: geth-node-ws-rpc
      port: 20020
    - name: raft-http
      port: 40000
    - name: devp2p
      port: 30303
    - name: constellation
      port: 9000
  selector:
      app: coordinator
  type: NodePort

在这里,环境变量 IP 用于指示节点应该侦听的 IP。0.0.0.0 表示任何 IP。然后,我们暴露了由 QNM 打开的端口。前面的主清单文件中的所有内容都是不言自明的。

现在让我们为一个动态节点创建主清单文件:

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: non-coordinator
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: non-coordinator
    spec:
      containers:
      - name: qnm
        image: narayanprusty/qnm
        args: ['node setupFromConfig.js']
        workingDir: /workspace/QuorumNetworkManager
        imagePullPolicy: Always
        env:
        - name: COORDINATING_IP
          value: 10.97.145.237
        - name: ROLE
          value: dynamicPeer 
        - name: IP
          value: 0.0.0.0
        ports:
        - containerPort: 50000
        - containerPort: 50010
        - containerPort: 50020
        - containerPort: 20000
        - containerPort: 20010
        - containerPort: 20020
        - containerPort: 40000
        - containerPort: 30303
        - containerPort: 9000
---

kind: Service
apiVersion: v1
metadata:
  name: non-coordinator
spec:
  ports:
    - name: remote-communication-node
      port: 50000
    - name: communication-node-rpc
      port: 50010
    - name: communication-node-ws-rpc
      port: 50020
    - name: geth-node
      port: 20000
    - name: geth-node-rpc
      port: 20010
    - name: geth-node-ws-rpc
      port: 20020
    - name: raft-http
      port: 40000
    - name: devp2p
      port: 30303
    - name: constellation
      port: 9000
  selector:
      app: non-coordinator
  type: NodePort

这个主清单文件看起来与之前的主清单文件非常相似,只是环境变量不同。在这里,我们提供了协调节点的 IP 地址。IP 地址是由协调节点服务暴露的集群 IP。对于你来说应该是不同的。然后,我们有 ROLE 环境变量,以指示 QNM 是动态节点而不是协调节点。

使用 K8s API 创建节点

K8s 主服务器公开了可用于读取和写入 K8s 资源对象的 API。你可以在 kubernetes.io/docs/reference/ 找到 API 参考文档。对于 QaaS,你需要创建一个前端,内部调用这些 API 来创建部署和服务。

访问 K8s APIs 最简单的方式是通过 HTTP 代理。Kubectl 允许你在本地主机和 K8s API 服务器之间创建代理服务器。所有进入的数据都通过一个端口进入,并转发到远程 K8s API 服务器端口,除了与静态内容路径匹配的路径。要创建代理服务器,请使用以下命令:

kubectl proxy --address="0.0.0.0" -p 8000

让我们看一个使用Node.js为协调节点创建部署的示例:

var request = require("request");

var options = {
    method: 'POST',
    url: 'http://127.0.0.1:8000/apis/apps/v1beta1/namespaces/
      default/deployments',
    headers: {
        'Content-Type': 'application/json'
    },
    body: {
        apiVersion: 'apps/v1beta1',
        kind: 'Deployment',
        metadata: {
            name: 'coordinator'
        },
        spec: {
            replicas: 1,
            template: {
                metadata: {
                    labels: {
                        app: 'coordinator'
                    }
                },
                spec: {
                    containers: [{
                        name: 'qnm',
                        image: 'narayanprusty/qnm',
                        args: ['node setupFromConfig.js'],
                        workingDir: '/workspace/QuorumNetworkManager',
                        imagePullPolicy: 'Always',
                        env: [{
                            name: 'IP',
                            value: '0.0.0.0'
                        }],
                        ports: [{
                                containerPort: 50000
                            },
                            {
                                containerPort: 50010
                            },
                            {
                                containerPort: 50020
                            },
                            {
                                containerPort: 20000
                            },
                            {
                                containerPort: 20010
                            },
                            {
                                containerPort: 20020
                            },
                            {
                                containerPort: 40000
                            },
                            {
                                containerPort: 30303
                            },
                            {
                                containerPort: 9000
                            }
                        ]
                    }]
                }
            }
        }
    },
    json: true
};

request(options, function(error, response, body) {
    if (error) throw new Error(error);

    console.log(body);
});

类似地,让我们看一个使用Node.js为协调节点创建服务的示例:

var request = require("request");

var options = {
    method: 'POST',
    url: 'http://127.0.0.1:8000/api/v1/namespaces/default/services',
    headers: {
        'Content-Type': 'application/json'
    },
    body: {
        kind: 'Service',
        apiVersion: 'v1',
        metadata: {
            name: 'coordinator'
        },
        spec: {
            ports: [{
                    name: 'remote-communication-node',
                    port: 50000
                },
                {
                    name: 'communication-node-rpc',
                    port: 50010
                },
                {
                    name: 'communication-node-ws-rpc',
                    port: 50020
                },
                {
                    name: 'geth-node',
                    port: 20000
                },
                {
                    name: 'geth-node-rpc',
                    port: 20010
                },
                {
                    name: 'geth-node-ws-rpc',
                    port: 20020
                },
                {
                    name: 'raft-http',
                    port: 40000
                },
                {
                    name: 'devp2p',
                    port: 30303
                },
                {
                    name: 'constellation',
                    port: 9000
                }
            ],
            selector: {
                app: 'coordinator'
            },
            type: 'NodePort'
        }
    },
    json: true
};

request(options, function(error, response, body) {
    if (error) throw new Error(error);

    console.log(body);
});

摘要

在本章中,我们通过示例学习了云计算和容器化的基础知识。我们看到了容器化的重要性以及如何使用 Docker 对应用程序进行容器化。然后,我们看到了 K8s 的重要性,以及它如何使基于微服务架构的应用程序开发变得容易。之后,我们学习了如何安装 minikube 并在 K8s 上部署容器。

最后,我们利用学到的所有技能来开发一个基于 QNM 的 QaaS 服务。在下一章中,我们将创建一个调用 K8s API 创建和加入网络的 QaaS 的基本 UI。