使用go语言构建区块链 Part4.事务1

英文源地址

简介

事务是比特币的核心, 区块链的唯一目的是以安全可靠的方式存储交易, 因此在交易创建后没有人可以修改. 今天我们开始实现事务, 但由于这是一个相当大的主题, 我将它分成两部分: 在这一部分中, 我们将实现事务的通用机制, 在第二部分中, 我们将研究细节.
此外, 由于代码的变化是巨大的, 在这里描述它们是没有意义的. 你可以在这里查看到所有的变化.

There is no spoon(黑客帝国台词)

如果你曾经开发过一个web应用程序, 为了实现支付, 你可能会在数据库中创建这些表: 账户和交易.一个账户将存储有关用户的信息, 包括他们的个人信息和余额, 一次交易将存储一个账户到另一个账户的资金转移的信息. 在比特币中, 交易以完全不同的方式实现. 它们是:

  1. 没有账户
  2. 没有余额
  3. 没有地址
  4. 没有硬币
  5. 没有发送者和接收者

由于区块链是一个公开和开放的数据库, 我们不想存储关于钱包所有者的任何敏感信息.硬币不在账户中收纳. 交易不会把钱从一个地址转移到另一个地址.没有保存账户余额的字段或属性. 只有交易, 但是交易里面有什么呢?

比特币交易
交易是输入和输出的组合

type Transaction struct {
	ID   []byte
	Vin  []TXInput
	Vout []TXOutput
}

对于每一笔新的交易, 它的输入会引用前一笔交易的输出(这里有个例外, coinbase交易), 引用就是花费的意思.所谓引用之前的一个输入, 也就是将之前的一个输出包含在另一笔交易的输入当中, 就是花费之前的交易输出. 交易的输出, 就是币实际存储的地方. 下面的图示阐释了交易之间的互相关联.
在这里插入图片描述
需要注意的是:

  1. 有一些输出并没有被关联到某个输入上
  2. 一笔交易的输入可以引用之前多笔交易的输出
  3. 一个输入必须引用一个输出

贯穿全文, 我们将会使用像’money’, ‘coin’, ‘spend’, ‘send’, 'account’等等这样的词. 但是在比特币中, 其实并不存在这些概念. 交易仅仅是通过一个脚本(script)来锁定(lock)一些值(value), 而这些值只可以被锁定它们的人解锁(unlock).
(每一笔比特币交易都会创造输出,输出都会被区块链记录下来。给某个人发送比特币,实际上意味着创造新的 UTXO 并注册到那个人的地址,可以为他所用。)

交易输出

先从输出开始

type TXOutput struct {
	Value        int
	ScriptPubKey string
}

输出主要包含两部分:

  1. 一定量的比特币(Value)
  2. 一个锁定脚本(ScriptPubKey), 要花这笔钱, 必须解锁该脚本

实际上, 正式输出里面存储了’币’(注意, 也就是上面的Value字段). 而这里的存储, 指的是用一个数学难题对输出进行锁定, 这个难题被存储在ScriptPubKey里面. 在内部, 比特币使用了一个叫做Script的脚本语言, 用它来定义锁定和解锁输出的逻辑. 虽然这个语言相当的原始(这是为了避免潜在的黑客攻击和滥用而有意为之), 并不复杂, 但是我们也并不会讨论它的细节. 你可以在这里找到详细解释.

在比特币中, value字段存储的是satoshi的数量, 而不是BTC的数量. 一个satoshi等于一亿分之一的BTC(0.00000001BTC), 这也是比特币里面最小的货币单位(就像是一分的硬币)

由于没有实现地址(address), 所以目前我们会避免涉及逻辑相关的完整脚本. ScriptPubKey将会存储一个任意的字符串(用户定义的钱包地址).

顺表说一下, 有了一个这样的脚本语言, 也意味着比特币其实可以作为一个智能合约平台.

关于输出, 非常重要的一点是: 它们是不可再分的(indivisible). 也就是说, 你无法仅引用其中的一部分. 要么不用, 如果要用, 必须一次性用完. 当一个新的交易中引用了某个输出, 那么这个输出必须被全部花费. 如果它的值比需要的值大, 那么就会产生一个找零, 找零会返还给发送方.这跟现实世界的场景十分类似, 当你像要支付时, 如果一个东西值1美元, 而你给了一个5美元的纸币, 那么你会得到一个4美元的找零.

交易输入

这里是输入

type TXInput struct {
	Txid      []byte
	Vout      int
	ScriptSig string
}

正如之前所提到的, 一个输入引用了一个输出: Txid存储的时之前交易的ID, Vout存储的时该输出在那笔交易中所有输出的索引(因为一笔交易可以有多个输出, 需要有信息指明时具体的哪一个).ScriptSig是一个脚本, 提供了可解锁输出结构里面ScriptPubKey字段的数据. 如果ScriptSig提供的数据是正确的, 那么输出就会解锁, 然后被解锁的值可以被用于产生新的输出; 如果数据不正确, 输出就无法被引用在输入中, 或者说, 无法使用这个输出. 这种机制, 保证了用户无法花费属于其他人的币.
再次强调, 由于我们还没有实现地址, 所以目前ScriptSig将仅仅存储一个用户自定义的任意钱包地址. 我们会在下一篇文章中实现公钥(public key)和签名(signature).
来简要总结一下.输出, 就是’币’存储的地方. 每个输出都会带有一个解锁脚本, 这个脚本定义了解锁该输出的逻辑. 每笔新的交易, 必须至少有一个输入与输出. 一个输入引用了之前一笔的输出, 并提供了解锁数据(也就是ScriptSig字段), 该数据会被用于在输出的解锁脚本中解锁输出, 解锁完成后即可使用它的值去产生新的输出.
每一笔输入都是之前一笔交易的输出, 那么假设从某一笔交易开始不断往前追溯, 它所涉及的输入和输出到底是谁先存在呢?换个说法, 这是个鸡和蛋谁先谁后的问题, 是先有蛋还是先有鸡呢?

先有蛋

在比特币中, 是先有蛋, 然后才有鸡的. 输入引用输出的逻辑, 是经典的’蛋还是鸡’的问题: 输入先产生输出, 然后输出使得输入成为可能. 在比特币中, 最先有输出, 然后才有输入. 换而言之, 每一笔交易只有输出, 没有输入.
当miner挖出一个新的区块时, 它会向新的区块添加一个coinbase交易. coinbase交易是一种特殊的交易, 它不需要引用之前一笔交易的输出. 它’凭空’产生了币(也就是产生了新币), 这是miner获得挖出mining的奖励, 也可以理解为’发行新币’.
在区块链的最初, 也就是第一个块, 叫做创世区块. 正是这个创世区块, 产生了区块链最开始的输出.对于创世区块, 不需要引用之前交易的输出.因为在创世区块之前根本不存在交易, 也就是不存在交易输出.
来创建一个coinbase交易:

func NewCoinbaseTX(to, data string) *Transaction {
	if data == "" {
		data = fmt.Sprintf("Reward to %s", to)
	}

	txin := TXInput{[]byte{}, -1, data}
	txout := TXOutput{subsidy, to}
	tx := Transaction{nil, []TXInput{txin}, []TXOutput{txout}}
	tx.SetID()

	return &tx
}

coinbase交易只有一个输出, 没有输入. 在我们的实现中, 它表现为Txid为空, Vout等于-1.并且, 在当前实现中, coinbase交易也没有再ScriptSig中存储脚本, 而只是存储了一个任意的字符串data.

在比特币中, 第一笔coinbase交易包含了如下信息: “The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”。可点击这里查看

subsidy是挖出新区块的奖励金. 在比特币中, 实际并没有存储这个数字, 而是基于区块总数进行计算而得: 区块总数除以210000就是subsidy. 挖出创世区块的奖励是50BTC, 每挖出210000个区块后, 奖励减半. 在我们的实现中, 这个奖励值将会是一个常量(至少目前是).

将交易保存到区块链

从现在开始, 每个区块必须存储至少一笔交易. 如果没有交易, 也就不可能出新的块. 这意味着我们应该溢出Block的Data字段, 取而代之的是存储交易:

type Block struct {
	Timestamp     int64
	Transactions  []*Transaction
	PrevBlockHash []byte
	Hash          []byte
	Nonce         int
}

NewBlock和NewGenesisBlock也必须做出相应的改变:

func NewBlock(transactions []*Transaction, prevBlockHash []byte) *Block {
	block := &Block{time.Now().Unix(), transactions, prevBlockHash, []byte{}, 0}
	pow := NewProofOfWork(block)
	nonce, hash := pow.Run()

	block.Hash = hash[:]
	block.Nonce = nonce

	return block
}

func NewGenesisBlock(coinbase *Transaction) *Block {
	return NewBlock([]*Transaction{coinbase}, []byte{})
}

接下来修改创建区块链的函数:

func CreateBlockchain(address string) *Blockchain {
	var tip []byte
	db, _ := bolt.Open(dbFile, 0600, nil)

	_ = db.Update(func(tx *bolt.Tx) error {
		cbtx := NewCoinbaseTX(address, genesisCoinbaseData)
		genesis := NewGenesisBlock(cbtx)

		b, _ := tx.CreateBucket([]byte(blocksBucket))

		if b == nil {
			b, _ := tx.CreateBucket([]byte(blocksBucket))
			_ = b.Put(genesis.Hash, genesis.Serialize())
			_ = b.Put([]byte("l"), genesis.Hash)
			tip = genesis.Hash
		} else {
			tip = b.Get([]byte("l"))
		}
		return nil
	})
	bc := Blockchain{tip, db}

	return &bc
}

现在, 这个函数会接受一个地址作为参数, 这个地址将会被用来接收挖出创世区块的奖励.

工作量证明

工作量证明算法必须要将存储在区块里面的交易考虑进去, 从而保证区块链交易存储的一致性和可靠性. 所以, 我们必须修改ProofOfWork.prepareData方法:

func (pow *ProofOfWork) prepareData(nonce int) []byte {
	data := bytes.Join(
		[][]byte{
			pow.block.PrevBlockHash,
			pow.block.HashTransactions(), // This line was changed
			IntToHex(pow.block.Timestamp),
			IntToHex(int64(targetBits)),
			IntToHex(int64(nonce)),
		},
		[]byte{},
	)

	return data
}

不像之前使用pow.block.Data, 现在我们使用pow.block.HashTransactions():

func (b *Block) HashTransactions() []byte {
	var txHashes [][]byte
	var txHash [32]byte

	for _, tx := range b.Transactions {
		txHashes = append(txHashes, tx.ID)
	}
	txHash = sha256.Sum256(bytes.Join(txHashes, []byte{}))
	return txHash[:]
}

通过哈希值提供数据的唯一表示, 这种做法我们已经不是第一次遇到了. 我们想要通过仅仅一个哈希值, 就可以识别一个块里面的所有交易. 为此, 先获得每笔交易的哈希值, 然后将它们关联起来, 最后获得一个连接后的组合哈希值.

比特币使用了一个更加复杂的技术: 它将一个区块里面包含的所有交易表示为一个Merkle tree, 然后在工作量证明系统中使用树的根哈希(root hash),这个方法能够让我们快速检索一个块里面是否包含了某笔交易, 即只需root hash而无需下载所有交易即可完成判断.

来检查目前为止是否正确:

╰─ ./blockchain_impl_in_go createblockchain -address Ivan                  ─╯ 
00000060b65eb7a78b68206835d12e06c8b00940da37b5c773c1d465a8a3a35f

Done!

很好, 我们已经获得了第一笔mining奖励, 但是, 我们要如何查看余额呢?

未花费的交易输出

我们需要找到所有的未花费交易输出(unspent transactions outputs, UTXO), 未花费(unspent)指的是这个输出还没有被包含在任何交易的输入中, 或者说没有被任何输入引用. 在上面图示中, 未花费的输出是
在这里插入图片描述

  1. tx0, output 1;
  2. tx1, output 0;
  3. tx3, output 0;
  4. tx4, output 0.

当然了, 检查余额时, 我们并不需要知道整个区块链上所哟䣌UTXO, 只需要关注那些我们能解锁的那些UTXO(目前我们还没有实现密钥, 所以我们将会使用用户定义的地址来代替). 首先, 让我们定义在输入和输出上的锁定和解锁方法:

func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool {
	return in.ScriptSig == unlockingData
}

func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool {
	return out.ScriptPubKey == unlockingData
}

在这里, 我们只是将script字段与unlockingData进行了比较. 在后续文章我们基于私钥实现了地址以后, 会对这部分进行改进.
下一步, 找到包含未花费输出的交易, 这一步其实相当困难:

func (bc *Blockchain) FindUnspentTransactions(address string) []Transaction {
	var upsentTXs []Transaction
	spentTXOs := make(map[string][]int)
	bci := bc.Iterator()

	for {
		block := bci.Next()

		for _, tx := range block.Transactions {
			txID := hex.EncodeToString(tx.ID)
		Outputs:
			for outIdx, out := range tx.Vout {
				if spentTXOs[txID] != nil {
					for _, spentOut := range spentTXOs[txID] {
						if spentOut == outIdx {
							continue Outputs
						}
					}
				}
				if out.CanBeUnlockedWith(address) {
					upsentTXs = append(upsentTXs, *tx)
				}
			}
			if tx.IsCoinbase() == false {
				for _, in := range tx.Vin {
					if in.CanUnlockOutputWith(address) {
						inTxID := hex.EncodeToString(in.Txid)
						spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)
					}
				}
			}
		}
		if len(block.PrevBlockHash) == 0 {
			break
		}
	}

	return upsentTXs
}

由于交易被存储在区块里, 所以我们不得不检查区块链里的每一笔交易.从输出开始:

if out.CanBeUnlockedWith(address) {
	unspentTXs = append(unspentTXs, tx)
}

如果一个输出被一个地址锁定, 并且这个地址恰好是我们要找的地址, 那么这个输出就是我们想要的. 不过在获得它之前, 我们需要检查该输出是否已经被包含在一个交易的输出中, 也就是检查它是否已经被花费了:

if spentTXOs[txID] != nil {
	for _, spentOut := range spentTXOs[txID] {
		if spentOut == outIdx {
			continue Outputs
		}
	}
}

我们跳过那些已经被包含在其他输入中的输出(这说明这个输出已经被花费, 无法再使用了). 检查完输出以后, 我们将给定地址所有能够解锁输出的输入聚合起来(这并不适用于coinbase交易, 因为它们不解锁输出)

if tx.IsCoinbase() == false {
    for _, in := range tx.Vin {
        if in.CanUnlockOutputWith(address) {
            inTxID := hex.EncodeToString(in.Txid)
            spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)
        }
    }
}

这个函数返回了一个交易列表, 里面包含了未花费输出.为了计算余额, 我们还需要一个函数将这些交易作为输入, 然后返回一个输出:

func (bc *Blockchain) FindUTXO(address string) []TXOutput {
       var UTXOs []TXOutput
       unspentTransactions := bc.FindUnspentTransactions(address)

       for _, tx := range unspentTransactions {
               for _, out := range tx.Vout {
                       if out.CanBeUnlockedWith(address) {
                               UTXOs = append(UTXOs, out)
                       }
               }
       }

       return UTXOs
}

就是这么多了!现在我们来实现getbalance命令

func (cli *CLI) getBalance(address string) {
	bc := NewBlockchain(address)
	defer bc.db.Close()

	balance := 0
	UTXOs := bc.FindUTXO(address)

	for _, out := range UTXOs {
		balance += out.Value
	}

	fmt.Printf("Balance of '%s': %d\n", address, balance)
}

账户余额就是由账户地址锁定的所有未花费交易输出的综合.
在挖出创世区块后, 来检查一下我们的余额:

╰─ ./blockchain_impl_in_go getbalance -address Ivan                        ─╯ 
Balance of Ivan: 10

这就是我们的第一笔钱!

发送币

现在, 我们想要给其他人发送一些币. 为此, 我们需要创建一笔新的交易, 将它放到一个区块里, 然后挖出这个区块. 之前我们只实现了coinbase交易(这是一种特殊的交易), 现在我们需要一种通用的普通交易.

func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction {
	var inputs []TXInput
	var outputs []TXOutput

	acc, validOutputs := bc.FindSpendableOutputs(from, amount)
	
	if acc < amount {
		log.Panic("ERROR: Not enough funds")
	}

	for txid, outs := range validOutputs {
		txID, _ := hex.DecodeString(txid)
		
		for _, out := range outs {
			input := TXInput{txID, out, from}
			inputs = append(inputs, input)
		}
	}
	outputs = append(outputs, TXOutput{amount, to})
	if acc >amount {
		outputs = append(outputs, TXOutput{acc - amount, from})
	}
	tx := Transaction{nil, inputs, outputs}
	tx.SetID()
	
	return &tx
}

在创建新的输出前, 我们首先必须找到所有的未花费输出, 并且确保它们有足够的价值(value),这就是FindSpendableOutputs方法要做的事情. 随后, 对于每个找到的输出, 会创建一个引用该输出的输入. 接下来, 我们创建两个输出:

  1. 一个由接收者地址锁定. 这是给其他地址实际转移的币
  2. 一个由发送者地址锁定.这是一个找零.只有当未花费输出超过新交易所需时产生. 记住: 输出是不可再分的.

FindSpendableOutputs方法基于之前定义的FindUnspentTransactions方法:

func (bc *Blockchain) FindSpendableOutputs(address string, amount int) (int, map[string][]int) {
	unspentOutputs := make(map[string][]int)
	unspentTXs := bc.FindUnspentTransactions(address)
	accumulated := 0
Work:
	for _, tx := range unspentTXs {
		txID := hex.EncodeToString(tx.ID)

		for outIdx, out := range tx.Vout {
			if out.CanBeUnlockedWith(address) && accumulated < amount {
				accumulated += out.Value
				unspentOutputs[txID] = append(unspentOutputs[txID], outIdx)

				if accumulated >= amount {
					break Work
				}
			}
		}
	}
	return accumulated, unspentOutputs
}

这个方法对所有未花费交易进行迭代, 并对它的值进行累加. 当累加值大于或等于我们想要传送的值时, 它就会停止并返回累加值, 同时返回的还有通过交易ID进行分组的输出索引. 我们只需取出足够支付的钱就够了.
现在我们可以修改Blockchain.MineBlock方法

func (bc *Blockchain) MineBlock(transactions []*Transaction) {
	var lastHash []byte

	_ = bc.db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(blocksBucket))
		lastHash = b.Get([]byte("l"))

		return nil
	})

	newBlock := NewBlock(transactions, lastHash)

	_ = bc.db.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(blocksBucket))
		_ = b.Put(newBlock.Hash, newBlock.Serialize())
		_ = b.Put([]byte("l"), newBlock.Hash)
		bc.tip = newBlock.Hash

		return nil
	})
}

最后, 让我们实现send方法:

func (cli *CLI) send(from, to string, amount int) {
	bc := NewBlockchain(from)
	defer bc.db.Close()

	tx := NewUTXOTransaction(from, to, amount, bc)
	bc.MineBlock([]*Transaction{tx})
	fmt.Println("Success!")
}

发送币意味着创建新的交易, 并通过挖出新的区块的方式将交易打包到区块链中. 不过比特币并不是一连串立刻完成这些事情(虽然我们目前的实现时这么做的). 相反, 它会将所有新的交易放到一个内存池中(mempool),然后当miner准备挖出一个新区块时, 它从内存池中取出所有交易, 创建一个候选块. 只有当包含这些交易的区块被挖出来, 并添加到区块链以后, 里面的交易才开始确认.
让我们检查一下发送币是否能工作:

$ blockchain_go send -from Ivan -to Pedro -amount 6
000000655594c9b0c6c1034ec0236d91d2115bbd74ed008901ea81c29f231d7f

Success!

╰─ ./blockchain_impl_in_go getbalance -address Ivan                        ─╯ 
Balance of Ivan: 4

╰─ ./blockchain_impl_in_go getbalance -address Pedro                       ─╯ 
Balance of Pedro: 6

很好!现在, 让我们创建更多的交易, 确保从多个输出中发送币也正常工作:

╰─ ./blockchain_impl_in_go send -from Pedro -to Helen -amount 2            ─╯ 
0000003d3c12819d42b9c9a2968a803b651a775af1af262d51384d6c2577f8e1

Success!

╰─ ./blockchain_impl_in_go send -from Ivan -to Helen -amount 2             ─╯ 
00000050e41ef1982c2aab6ad43d748b7f65c720a34a6fb9f144960d911e4711

Success!

现在, Helen的币被锁定在了两个输出中: 一个来自Pedro, 一个来自Lvan.让我们把它们发送给其他人:

╰─ ./blockchain_impl_in_go send -from Helen -to Rachel -amount 3           ─╯ 
000000417eabcad236c9d42c5c33d72c5e63a293c2b5491390f362d1d3fcdb89

Success!

$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 2

$ blockchain_go getbalance -address Pedro
Balance of 'Pedro': 4

$ blockchain_go getbalance -address Helen
Balance of 'Helen': 1

$ blockchain_go getbalance -address Rachel
Balance of 'Rachel': 3

看起来没问题!现在, 来测试一些失败的情况:

$ blockchain_go send -from Pedro -to Ivan -amount 5
panic: ERROR: Not enough funds

$ blockchain_go getbalance -address Pedro
Balance of 'Pedro': 4

$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 2

总结

虽然不容易, 但是现在终于实现交易了!不过, 我们依然缺少了一些像比特币那样的一些关键特性:

  1. 地址(address). 我们现在还没有基于私钥(private key)的真实地址.
  2. 奖励(reward). 现在mining时肯定无法盈利的!
  3. UTXO集. 获取余额需要扫描整个区块链, 而当区块非常多时, 这么做就会花费很长时间. 并且, 如果我们想要验证后续交易, 也需要花费很长时间. 而UTXO集就是为了解决这些问题, 加快交易相关的操作.
  4. 内存池(mempool). 在交易被打包到区块之前, 这些交易被存储在内存池里面. 在我们目前的实现中,一个块仅仅包含一笔交易, 这是相当低效的.

link

Full source codes
Transaction
Merkle tree
Coinbase

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/22484.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Go语言从入门到实战】基础篇

Go语言从入门到实战 — 基础篇 First Go Program 编译 & 运行 基本程序结构 应用程序入口 package mainimport "fmt"func main() {fmt.Println("Hello World") }退出返回值 package mainimport ("fmt""os" )func main() {fmt.Pr…

Unity3D :使用 UXML 实例作为模板

推荐&#xff1a;将 NSDT场景编辑器 加入你的3D工具链 3D工具集&#xff1a; NSDT简石数字孪生 使用 UXML 实例作为模板 您可以将现有 UXML 文档实例化为 UXML 文档中的模板作为模板实例&#xff0c;类似于预制件 在 Unity 中工作。 使用 UXML 文档作为模板 要将项目中的现有…

MySQL高级篇——覆盖索引、前缀索引、索引下推、SQL优化、主键设计

导航&#xff1a; 【Java笔记踩坑汇总】Java基础进阶JavaWebSSMSpringBoot瑞吉外卖SpringCloud黑马旅游谷粒商城学成在线MySQL高级篇设计模式牛客面试题 目录 8. 优先考虑覆盖索引 8.1 什么是覆盖索引&#xff1f; 8.1.0 概念 8.0.1 覆盖索引情况下&#xff0c;“不等于”…

Fourier分析入门——第4章——频率域

目录 第 4 章 频率域(The Frequency Domain) 4.1 频谱分析(Spectral Analysis) 4.2 物理单位(Physics units) 4.3 笛卡尔坐标形式与极坐标形式对比 4.4 频谱分析的复数形式 4.5 复数值Fourier系数 4.6 复数值的和三角的Fourier系数之间的关系 4.7 2维或多维离散Fouri…

内容好但流量差?B站流量密码可能就在这

B站知名数码UP主老师好我叫何同学&#xff08;以下简称“何同学”。&#xff09;时隔两个月再次更新&#xff0c;这支标题为《为了找到流量密码&#xff0c;我们做了个假B站...》的视频不仅吸引了观众的围观&#xff0c;更是获得了众多B站UP主们的“声援”。 如题所见&#xf…

K8s in Action 阅读笔记——【3】Pods: running containers in Kubernetes

K8s in Action 阅读笔记——【3】Pods: running containers in Kubernetes 3.1 Introducing pods 在Kubernetes中&#xff0c;Pod是基本构建块之一&#xff0c;由容器集合组成。与独立部署容器不同&#xff0c;你总是要部署和操作一个Pod。Pod并不总是包含多个容器&#xff0…

如何高效地在网上找开源项目

开源项目是发展技能、分享想法和成为开发社区一员的好方法。开源意味着软件功能背后的源代码与所有想要阅读它的人公开共享。这意味着你可以准确地看到一个系统是如何工作的——一旦你愿意冒险&#xff0c;就为它做出贡献。除了向所有人开放贡献外&#xff0c;这种开放代码库通…

经典JavaScript手写面试题和答案

文章目录 实现一个函数去重&#xff1f;实现一个函数&#xff0c;判断指定元素在数组中是否存在&#xff1f;实现一个函数&#xff0c;将给定字符串反转&#xff1f;实现一个函数&#xff0c;检测指定字符串是否为回文&#xff08;即从前往后和从后往前的字符序列都相同&#x…

Systrace系列4 —— SystemServer 解读

本文主要是对 SystemServer 进行简单介绍,介绍了 SystemServer 中几个比较重要的线程,由于 Input 和 Binder 比较重要,所以单独拿出来讲,在这里就没有再涉及到。 窗口动画 Systrace 中的 SystemServer 一个比较重要的地方就是窗口动画,由于窗口归 SystemServer 来管,那么…

react学习3 生命周期

componentDidMount()与render()一个级别的&#xff0c;在组件挂载完成之后调用 卸载组件&#xff1a;REACTDOM.unmountComponentAtNode() componentWillUnmount() 组件马上被卸载的时候 老生命周期&#xff1a; 新的生命周期&#xff1a; 废弃了&#xff08;加上Unsate_还是…

RabbitMQ --- 死信交换机(一)

前言 当我们在使用消息队列时&#xff0c;难免会遇到一些消息被拒绝&#xff0c;重复投递或者超时等异常情况。这些异常消息如果不被正确处理&#xff0c;将会阻碍整个消息系统的正常运行。而此时&#xff0c;死信交换机&#xff08;Dead Letter Exchange&#xff0c;简称DLX&…

FAT NTFS Ext3文件系统有什么区别

10 年前 FAT 文件系统还是常见的格式&#xff0c;而现在 Windows 上主要是 NTFS&#xff0c;Linux 上主要是Ext3、Ext4 文件系统。关于这块知识&#xff0c;一般资料只会从支持的磁盘大小、数据保护、文件名等各种维度帮你比较&#xff0c;但是最本质的内容却被一笔带过。它们最…

MySQL-索引(2)

本文主要讲解MySQL-索引相关的知识点 联合索引前缀索引覆盖索引索引下推索引的优缺点什么时候适合创建索引,什么时候不适合?如何优化索引 ? 索引失效场景 ? 为什么SQL语句使用了索引,却还是慢查询 ? 使用索引有哪些注意事项 ? InnoDB引擎中的索引策略 目录 联合索引 联合…

【C++】函数重载 - 给代码增添多彩的魔法

欢迎来到博主 Apeiron 的博客&#xff0c;祝您旅程愉快 &#xff01; 时止则止&#xff0c;时行则行。动静不失其时&#xff0c;其道光明。 目录 1、缘起 2、函数重载概述 3、函数重载注意事项 4、总结 1、缘起 函数重载&#xff0c;是编程世界中的一抹迷人色彩&#xff0c…

动态规划-状态压缩DP

[SCOI2005] 互不侵犯 题目描述 https://www.luogu.com.cn/problem/P1896 在NN的棋盘里面放K个国王&#xff0c;使他们互不攻击&#xff0c;共有多少种摆放方案。国王能攻击到它上下左右&#xff0c;以及左上左下右上右下八个方向上附近的各一个格子&#xff0c;共8个格子。 …

堪比ChatGPT,Claude注册和使用教程

新建了一个网站 https://ai.weoknow.com/ 每天给大家更新可用的国内可用chatGPT资源 Claude简介 Claude是一款人工智能聊天机器人。主要有以下特征: 使用自己的模型与训练方法,而不是基于GPT-3等开源框架。模型采用Transformer编码器与解码器的结构,并使用对话上下文的双向…

实验六 自动驾驶建模与仿真

【实验目的】 了解Matlab/Simulink软件环境&#xff0c;熟悉Simulink建模步骤&#xff1b;了解车辆运动控制的基本原理&#xff0c;学会简单的车辆运动控制建模及仿真&#xff1b;了解自动驾驶建模的基本过程&#xff0c;了解典型ADAS系统模型的应用特点。了解自动驾驶相关函数…

老司机解读香农定理、奈奎斯特定理、编码与调制

工程师都会考虑一个问题&#xff1a;信道上到底可以传输多大的数据&#xff0c;或者指定的信道上的极限传输率是多少。这就是信道容量的问题。例如&#xff0c;在xDSL系统中&#xff0c;我们使用的传输介质是仅有几兆带宽的电话线&#xff0c;而上面要传送几兆、十几兆甚至几十…

用 Python 写 3D 游戏

vizard介绍 Vizard是一款虚拟现实开发平台软件&#xff0c;从开发至今已走过十个年头。它基于C/C&#xff0c;运用新近OpenGL拓展模块开发出的高性能图形引擎。当运用Python语言执行开发时&#xff0c;Vizard同时自动将编写的程式转换为字节码抽象层(LAXMI)&#xff0c;进而运行…

WorkPlus AI助理 | 将企业业务场景与ChatGPT结合

近年来&#xff0c;人工智能成为了企业数字化转型的热门话题&#xff0c;作为被训练的语言模型&#xff0c;ChatGPT具备模拟对话、回答问题、写代码、写小说、进行线上内容创作的能力&#xff0c;还能根据聊天的上下文进行互动。作为一款新兴的人工智能应用程序&#xff0c;对于…