NFT老炮CryptoPunks解析与实现

文章目录

  • NFT老炮CryptoPunks解析与实现
  • 前言
  • 一、CryptoPunks设计解析
  • 二、CryptoPunks链上智能合约解析
    • 1.CryptoPunksMarket交易合约
    • 2.Migrations迁移合约
  • 三、模拟CryptoPunks测试环境部署
    • 1.环境搭建
    • 2.部署过程
  • 四、模拟CryptoPunks图层制作
  • 五、总结与展望
  • 六、感谢

前言

CryptoPunks是NFT头像的老炮代表。它为其他所有跟随其脚步的项目开创了先例。CryptoPunks由Larva Labs在2017年6月发布,是以太坊区块链上第一批NFT项目之一。当时,CryptoPunks共有1万个独特生成的24×24像素的头像,任何拥有以太坊钱包的人都可以免费领取。

现在,CryptoPunks已经取得了传奇性的地位,进入了主流艺术界,并在佳士得和苏富比等主要艺术机构的拍卖会上成为了头条。CryptoPunks本质上是NFT的劳力士。即使是价格最低的Punks也能卖到数万美元。最稀有的,比如僵尸、猿人和外星人的价格可以高达数百万美元。

本文将对CryptoPunks的结构与内容进行深入解析、修改与部署。


一、CryptoPunks设计解析

作为以太坊区块链上第一批NFT项目之一的CryptoPunks在设计流程上可以说非常单一、线性化。作为头像NFT先驱者,为避免链上证明花费大量gas,Larva Labs工作室将图像在链下制作完毕后计算图像上的SHA256哈希值,将该图像哈希值与放在智能合约中的哈希值比对来证明Token的获得者的真实性。
如此一来,项目流程就分为了链下图像制作与链上智能合约对交易的设计。下图1.1、图1.2为Larva Labs工作室网站对朋克图像存储的解释。
图1.1:

图1.2:

链下图像制作部分简介:Larva Labs工作室制作了10000个Token对应10000个24×24像素图片,由基本图层人头、肤色、发型、五官、配饰、背景色这六个图层的组合构成。例如将现有的CryptoPunks十个角色各图层分离再创作就能生成4032张不同的图像。Larva Labs将制作好的10000张CryptoPunks图像合在一起叫做punks.png,作者将图附在源代码中如下图1.3:

对图1.3即punks.png进行哈希,将哈希值放在智能合约上来证明Token所有者拥有的是真实的CryptoPunks头像。
链上智能合约部分简介
因为区块链的透明性、信息不可篡改性给予这些图像新的价值。作为老炮的CryptoPunks起初是任何拥有以太坊钱包的人都可以免费领取的,但10000个CryptoPunks头像很快就被全部领取了,因此现在只能通过遵循智能合约中的交易规则来购买他人拥有的CryptoPunks。图1.4、1.5:


二、CryptoPunks链上智能合约解析

1.CryptoPunksMarket交易合约

CryptoPunksMarket.sol代码解析如下:

pragma solidity ^0.4.8;
contract CryptoPunksMarket {// 上文提到的punks.png的hash值,用于验证token所有者的正确性string public imageHash = "ac39af4793119ee46bbff351d8cb6b5f23da60222126add4268e261199a2921b";//声明合约拥有者address owner;
//定义并声明token中各变量类型string public standard = 'CryptoPunks';string public name;string public symbol;uint8 public decimals;
//声明token类型uint256 public totalSupply;
//从0开始分配Punk的tokenuint public nextPunkIndexToAssign = 0;
//当token发放完毕停止发送函数bool public allPunksAssigned = false;
//剩余未分配token数量uint public punksRemainingToAssign = 0;
//将token索引映射到拥有者的地址,punkIndexToAddress[0]即得到索引为0的token的拥有者地址mapping (uint => address) public punkIndexToAddress;
//用户所拥有的token的数量mapping (address => uint256) public balanceOf;//声明出售token的结构体struct Offer {bool isForSale;//bool值为true代表token正在出售uint punkIndex;address seller;//卖方uint minValue; //卖出最低价address onlySellTo;//用于指定买方,若为空则任何人都可以}//声明竞标token的结构体struct Bid {bool hasBid;//是否正在竞标uint punkIndex;address bidder;uint value;}//通过token索引来查看该token的卖出信息mapping (uint => Offer) public punksOfferedForSale;
//通过token索引来查看该token的最高价竞标信息mapping (uint => Bid) public punkBids;
//用户的临时账户,存放其卖token的回报或者竞标失败退回的竞标额mapping (address => uint) public pendingWithdrawals;//用于事件监听event Assign(address indexed to, uint256 punkIndex);event Transfer(address indexed from, address indexed to, uint256 value);event PunkTransfer(address indexed from, address indexed to, uint256 punkIndex);event PunkOffered(uint indexed punkIndex, uint minValue, address indexed toAddress);event PunkBidEntered(uint indexed punkIndex, uint value, address indexed fromAddress);event PunkBidWithdrawn(uint indexed punkIndex, uint value, address indexed fromAddress);event PunkBought(uint indexed punkIndex, uint value, address indexed fromAddress, address indexed toAddress);event PunkNoLongerForSale(uint indexed punkIndex);//定义token各参数的函数function CryptoPunksMarket() payable {owner = msg.sender;totalSupply = 10000;                  //token总数为10000punksRemainingToAssign = totalSupply; //余量为10000name = "CRYPTOPUNKS";                 // 显示名称symbol = "Ͼ";                         // 显示标志decimals = 0;                         // 显示token数量后小数}//合约拥有者通过调用该函数将token送给某些用户function setInitialOwner(address to, uint punkIndex) {if (msg.sender != owner) throw;   //函数调用者必须是合约拥有者if (allPunksAssigned) throw;      //token发放函数未上锁,即token仍有余量if (punkIndex >= 10000) throw;    //punkIndex即token的索引要小于token总数10000if (punkIndexToAddress[punkIndex] != to) {     //token的主人不能是要送给的用户if (punkIndexToAddress[punkIndex] != 0x0) {//送出去的token可以收回再送给别的用户balanceOf[punkIndexToAddress[punkIndex]]--;} else {punksRemainingToAssign—;   //该token之前没被赠送过,可分配token数减1}punkIndexToAddress[punkIndex] = to;   //更换token拥有者为tobalanceOf[to]++;   //to用户的token拥有总数加1Assign(to, punkIndex);   //事件event Assign,监听到该事件则说明该函数调用成功}}//同时将多个token送给多个用户function setInitialOwners(address[] addresses, uint[] indices) {if (msg.sender != owner) throw;uint n = addresses.length;for (uint i = 0; i < n; i++) {setInitialOwner(addresses[i], indices[i]);}}
//将变量allPunksAssigned设置为true,用以停止上面两个函数的使用,开始下面函数的使用function allInitialOwnersAssigned() {if (msg.sender != owner) throw;allPunksAssigned = true;}//任何用户都可以通过该函数来获得无主的tokenfunction getPunk(uint punkIndex) {if (!allPunksAssigned) throw;  //allPunksAssigned为trueif (punksRemainingToAssign == 0) throw;   //剩下能够分配的token数要大于0if (punkIndexToAddress[punkIndex] != 0x0) throw; //只能得到无主tokenif (punkIndex >= 10000) throw;   //punkIndex即token的索引,要小于token总数10000punkIndexToAddress[punkIndex] = msg.sender;balanceOf[msg.sender]++;punksRemainingToAssign--;Assign(msg.sender, punkIndex);   //事件event Assign,监听到该事件则说明该函数调用成功}// 转赠tokenfunction transferPunk(address to, uint punkIndex) {if (!allPunksAssigned) throw;   //allPunksAssigned为trueif (punkIndexToAddress[punkIndex] != msg.sender) throw;  //函数调用者必须是token的主人if (punkIndex >= 10000) throw;  //punkIndex即token的索引要小于token总数10000if (punksOfferedForSale[punkIndex].isForSale) {  //如果之前想要sell这个token,则先取消sellpunkNoLongerForSale(punkIndex);}punkIndexToAddress[punkIndex] = to;balanceOf[msg.sender]--;balanceOf[to]++;Transfer(msg.sender, to, 1);  //事件event Transfer,监听到该事件则说明该函数调用成功PunkTransfer(msg.sender, to, punkIndex); //事件event PunkTransfer,监听到该事件则说明该函数调用成功
//查看被赠方是否之前有对该token进行投标,有则取消,并将其投标的金额放进其临时账户中Bid bid = punkBids[punkIndex];if (bid.bidder == to) {// Kill bid and refund valuependingWithdrawals[to] += bid.value;punkBids[punkIndex] = Bid(false, punkIndex, 0x0, 0);//清空投标信息,并设置状态为false}}//取消之前设置的token卖出信息function punkNoLongerForSale(uint punkIndex) {if (!allPunksAssigned) throw;   //allPunksAssigned为trueif (punkIndexToAddress[punkIndex] != msg.sender) throw; //函数调用者必须是token的主人if (punkIndex >= 10000) throw;  //punkIndex即token的索引要小于token总数10000
//清空卖出offer信息,并设置状态为false       punksOfferedForSale[punkIndex] = Offer(false, punkIndex, msg.sender, 0, 0x0);PunkNoLongerForSale(punkIndex);}//想将某token卖出,将相应信息写入offer中,用以告知大家function offerPunkForSale(uint punkIndex, uint minSalePriceInWei) {if (!allPunksAssigned) throw;   //allPunksAssigned为trueif (punkIndexToAddress[punkIndex] != msg.sender) throw;  //函数调用者必须是token的主人if (punkIndex >= 10000) throw;   //punkIndex即token的索引要小于token总数10000
//将相应的信息写入结构体offer中punksOfferedForSale[punkIndex] = Offer(true, punkIndex, msg.sender, minSalePriceInWei, 0x0);PunkOffered(punkIndex, minSalePriceInWei, 0x0);//事件PunkOffered,用以告知函数调用成功}//声明某token想要卖给某个特定的用户function offerPunkForSaleToAddress(uint punkIndex, uint minSalePriceInWei, address toAddress) {if (!allPunksAssigned) throw; //allPunksAssigned为trueif (punkIndexToAddress[punkIndex] != msg.sender) throw; //函数调用者必须是token的主人if (punkIndex >= 10000) throw;    //punkIndex即token的索引要小于token总数10000
//将相应的信息写入结构体offer中punksOfferedForSale[punkIndex] = Offer(true, punkIndex, msg.sender, minSalePriceInWei, toAddress);PunkOffered(punkIndex, minSalePriceInWei, toAddress);//事件PunkOffered,用以告知函数调用成功}//购买在offer中设置为卖出的tokenfunction buyPunk(uint punkIndex) payable {if (!allPunksAssigned) throw;      //allPunksAssigned为trueOffer offer = punksOfferedForSale[punkIndex];  //token的卖出信息if (punkIndex >= 10000) throw;  //punkIndex即token的索引要小于token总数10000if (!offer.isForSale) throw;                // token的卖出状态要为true
//token指定的卖出用户要是函数调用者或者没有指定卖出用户if (offer.onlySellTo != 0x0 && offer.onlySellTo != msg.sender) throw;if (msg.value < offer.minValue) throw;      // 你的出价必须大于或等于offer中标出的价格if (offer.seller != punkIndexToAddress[punkIndex]) throw; //offer中表明的卖方必须还是token的主人address seller = offer.seller;punkIndexToAddress[punkIndex] = msg.sender;balanceOf[seller]--;balanceOf[msg.sender]++;Transfer(seller, msg.sender, 1); //事件Transfer,说明买卖成功punkNoLongerForSale(punkIndex);  //清空卖出offer信息pendingWithdrawals[seller] += msg.value;  //将卖出的钱msg.value写入seller的临时账户PunkBought(punkIndex, msg.value, seller, msg.sender); //事件//如果该函数调用者之前有投标过该token,则取消,并将投标钱放入临时账户中Bid bid = punkBids[punkIndex];if (bid.bidder == msg.sender) {// Kill bid and refund valuependingWithdrawals[msg.sender] += bid.value;punkBids[punkIndex] = Bid(false, punkIndex, 0x0, 0);//清空投标信息}}//将临时账户中的钱拿出,其实就是要求合约地址send回钱function withdraw() {if (!allPunksAssigned) throw;   //allPunksAssigned为trueuint amount = pendingWithdrawals[msg.sender];// Remember to zero the pending refund before// sending to prevent re-entrancy attackspendingWithdrawals[msg.sender] = 0;  //清空临时账户msg.sender.transfer(amount);  //合约地址send回钱amount给msg.sender,即合约调用者}//进入某个token的投标市场function enterBidForPunk(uint punkIndex) payable {if (punkIndex >= 10000) throw;  //punkIndex即token的索引要小于token总数10000if (!allPunksAssigned) throw;       //allPunksAssigned为trueif (punkIndexToAddress[punkIndex] == 0x0) throw;  //该token要有主if (punkIndexToAddress[punkIndex] == msg.sender) throw;  //该token的主人不是函数调用者if (msg.value == 0) throw;   //投标价格一定要大于0Bid existing = punkBids[punkIndex];   //之前的最高价的投标信息if (msg.value <= existing.value) throw;   //出的投标价高于之前的最高价时,该投标才成功if (existing.value > 0) {//之前投标的人的投标价会返回到它的临时账户中// Refund the failing bidpendingWithdrawals[existing.bidder] += existing.value;}punkBids[punkIndex] = Bid(true, punkIndex, msg.sender, msg.value); //覆盖投标信息PunkBidEntered(punkIndex, msg.value, msg.sender); //事件}//接受目前出最高投标价的投标者的投标function acceptBidForPunk(uint punkIndex, uint minPrice) {if (punkIndex >= 10000) throw;  //punkIndex即token的索引要小于token总数10000if (!allPunksAssigned) throw;    //allPunksAssigned为trueif (punkIndexToAddress[punkIndex] != msg.sender) throw; //只有token主人才能接受投标address seller = msg.sender;  //卖方地址Bid bid = punkBids[punkIndex]; //投标信息if (bid.value == 0) throw;  //投标价等于0说明没人投标if (bid.value < minPrice) throw;  //投标价要大于接受投标的最小价格punkIndexToAddress[punkIndex] = bid.bidder;balanceOf[seller]--;balanceOf[bid.bidder]++;Transfer(seller, bid.bidder, 1);
//成功后,无论是卖出offer还是投标bid信息都要被清空,offer中的主人会换成投标者punksOfferedForSale[punkIndex] = Offer(false, punkIndex, bid.bidder, 0, 0x0);uint amount = bid.value;punkBids[punkIndex] = Bid(false, punkIndex, 0x0, 0);pendingWithdrawals[seller] += amount;  //seller赚到的钱放入临时账户PunkBought(punkIndex, bid.value, seller, bid.bidder);//事件}//取消对某个token的投标function withdrawBidForPunk(uint punkIndex) {if (punkIndex >= 10000) throw;  //punkIndex即token的索引要小于token总数10000if (!allPunksAssigned) throw;       //allPunksAssigned为true         if (punkIndexToAddress[punkIndex] == 0x0) throw;   //该token要有主if (punkIndexToAddress[punkIndex] == msg.sender) throw;  //该token的主人不是函数调用者Bid bid = punkBids[punkIndex];  //投标信息if (bid.bidder != msg.sender) throw;  //投标者即函数调用者PunkBidWithdrawn(punkIndex, bid.value, msg.sender); //事件uint amount = bid.value;punkBids[punkIndex] = Bid(false, punkIndex, 0x0, 0); //清空投标信息// Refund the bid moneymsg.sender.transfer(amount);  //直接将投标价send给投标者,不放入临时账户}}

2.Migrations迁移合约

Migrations.sol用来处理部署(迁移)智能合约,迁移是一个额外特别的合约用来保存合约的变化。代码如下:

pragma solidity ^0.4.4;contract Migrations {address public owner;uint public last_completed_migration;modifier restricted() {if (msg.sender == owner) _;}function Migrations() {owner = msg.sender;}function setCompleted(uint completed) restricted {last_completed_migration = completed;}function upgrade(address new_address) restricted {Migrations upgraded = Migrations(new_address);upgraded.setCompleted(last_completed_migration);}
}

在使用truffle migration进行部署后,在migrations文件夹下已经有一个1_initial_migration.js部署脚本,用来部署Migrations.sol,Migrations.sol用来确保不会部署相同的合约。2_deploy_contracts.js中的内容只导入了ConvertLib库,并且部署它,ConvertLib部署后会关联到MetaCoin合约,并部署MetaCoin合约。

三、模拟CryptoPunks测试环境部署

系统环境:win11
开发工具:npm、Truffle、Ganache
语言:go

1.环境搭建

Truffle框架依赖Node,需要使用npm来安装,首先需要安装node,npm会同时安装,在官网下载:Nodejs。使用npm安装Truffle框架,安装命令 :

npm install -g truffle

验证Truffle安装:

truffle --version

安装Ganache用来搭建本地区块链测试网络测试程序,Ganache下载路径:Ganache,下图为Ganache界面:
图3.1:
ACCOUNTS: 账号界面,显示了自动生成所有的账号及其余额。
BLOCKS: 区块界面,显示了再本地区块链网络上挖掘的每一个区块,及其Gas成本和包含的交易。
TRANSACTIONS: 交易页面,列出了再本地区块链上发生的所有交易
CONTRACTS: 合约页面
EVENTS: 事件页面
LOGS: 日志页面

2.部署过程

初始化项目命令:

mkdir punk
cd punk
truffle init

初始化完成后可以看到目标目录已经生成基本文件,其中truffle-config.js文件用于配置网络。在contract目录下创建与cryptopunks目录下contracts相同的CryptoPunksMarket.sol、Migrations.sol、ConvertLib.sol合约文件。
完成后使用truffle compile命令编译项目:

truffle compile

编译项目后出现错误如下:

Error: Truffle is currently using solc 0.8.15, but one or more of your contracts specify “pragma solidity ^0.4.4”.
Please update your truffle config or pragma statement(s).
(See https://trufflesuite.com/docs/truffle/reference/configuration#compiler-configuration for information on
configuring Truffle to use a specific solc compiler version.)

报错原因是因为CryptoPunks开发时使用Solidity为0.4版本,目前truffle默认Solidity版本为0.8。因此解决办法有两个:将编译目录下truffle-config.js文件中compiler选项中version版本更改为0.4.24,或将合约中声明的版本更改为符合truffle的0.8版本,但这样做会导致新版本语法与原本0.4版本冲突,需要重新修改。
笔者这里暂时更改truffle设置降为0.4.24版本,找到编译目录下truffle-config.js,更改参数如下:

// Configure your compilers
compilers: {
solc: {
version: “0.4.24”, // Fetch exact version from solc-bin (default: truffle’s version)
// docker: true, // Use “0.5.1” you’ve installed locally with docker (default: false)
// settings: { // See the solidity docs for advice about optimization and evmVersion
// optimizer: {
// enabled: false,
// runs: 200
// },
// evmVersion: “byzantium”
// }
}
},

编译成功以后会发现目录下新增build文件夹,这个文件下是智能合约的ABI文件,代表“抽象二进制接口”,主要有以下两个作用:作为可以在以太坊虚拟机(EVM)运行的可执行文件,包含智能合约函数的JSON表示,以便外部客户端调用这些函数。
更新配置文件用来连接到本地区块链网络,打开根目录下的truffle-config.js文件,修改内容如下:

module.exports = {
networks: {
development: {
host: “127.0.0.1”, // ip地址
port: 8545, // 端口
network_id: “*” // Match any network id
}
},
solc: {
optimizer: {
enabled: true,
runs: 200
}
}
}

将对应参数填写到Ganache的设置里Server配置中,如下图:
图3.2:
将CryptoPunks中迁移文件加入本地工作目录中migrations中用来部署全部三个合约,完成后进行迁移命令:

truffle migrate

至此合约基本部署成功。

四、模拟CryptoPunks图层制作

上文简介说到CryptoPunks图像构成由基本图层人头、肤色、发型、五官、配饰、背景色这六个图层的组合形成。我们通过构建这六大图层并排列就可以轻松获得超过10000个NFT的像素图,也可以获得目前CryptoPunks组合没有出现过但是存在的图像。当然,图层没有局限性,图层人头也可以替换成动物,由创造者自己选择制作。本次演示就将CryptoPunks的图层提取出来自己动手制作一下来解析背后的原理。
首先建立画板,画板尺寸为24×24、48×48、1920×1080,任意尺寸都可以达成目的,只是风格不同。这里为演示CryptoPunks分解,采用24×24。
绘制基本的人头图层,如下图:
随后制作五官、发型、肤色后第一个“山寨版”的CryptoPunks诞生了:

画的有点像豆芽了,不过没关系,除人头也就是轮廓图层以外每个图层新加一个图层,并且加上装饰物如眼镜、烟斗等与背景颜色。

本次实验共画了两个装饰图层,两个五官图层,两个发型图层,三个肤色图层,一个人头轮廓图层,共合成得到24张图像。其中一张如下图:
将生成的全部图像合成到一个图像上,如图1.3 CryptoPunks中punks.png所示,将该图进行哈希SHA256运算后,将哈希值放到CryptoPunksMarket.sol中“string public imageHash"中来保证token持有者的正版所有权。

五、总结与展望

本次解析了CryptoPunks的设计流程,并逐步实现了 CryptoPunks在测试环境Ganache中的部署,实现合约中基本功能。并通过24×24的像素画板模仿CryptoPunks图像制作流程并制作新的图像,可以得出生成的图像数量等于各图层相乘:X×Y×Z×…×A=生成图像数量。
本次设计流程并未结束,接下来将从CryptoPunks入手对以太坊,区块链,nft等各项目进行解析与实现部署。我会不断更新文章内容与项目内容,请关注github与本CSDN账号,谢谢。

六、感谢

感谢truffle与nodejs的版本问题、Truffle搭建环境migrate步骤错误解决方案、truffle的webpack案例测试,webpack项目中的报错、解决Node.js项目报错SyntaxError: Unexpected token、开发一个简单的智能合约给予的帮助,谢谢,欢迎与我讨论。

NFT老炮CryptoPunks解析与实现相关推荐

  1. 我的AI人生:12岁少女变身极客,摇滚老炮当上AI个体户

    点击左上方蓝字关注我们 计算机科学奠基人艾伦·佩利堪称AI的虔诚信徒,"在人工智能里呆上一年,足以让人相信上帝的存在". 酷炫的人工智能,似乎只是科幻电影里的异次元场景,或者高科技 ...

  2. 一位20多年SE“骨灰级老炮”,告诉你售前工程师能力分级的真相

    在五六年之前,售前工程师SE(Sale Engineer)被业内分为三级,即:技术支持级.资深工程师级.行业顾问级. 然而,随着云计算.大数据等新兴技术的蓬勃发展,影响着百行百业的数字化进行,因而,对 ...

  3. BOSS直聘上市背后:招聘新人与行业老炮的终极对决

    BOSS直聘上市背后:招聘新人与行业老炮的终极对决 出品 l 观点财经 作者 l 亚瑟 "找工作,我要跟老板谈!" 当"神奇女侠"盖尔·加朵用并不标准的普通话自 ...

  4. 看江湖老炮用尽洪荒之力解读网络协议(下)

    作者言:老炮总结的有些协议比喻也不是很恰当,毕竟网络协议是一门科学,而江湖规矩是口口相传的道义:如果把此文当成一份凉菜,"老炮如是说"的话语只能做为一点调味,具体调的好不好,老炮也 ...

  5. 《后会无期》为何将电影老炮甩身后

    韩寒的<后会无期>票房还是让很多人大跌眼镜.一个文艺片竟然3天2.2亿票房,首周票房报收2.84亿. 我的微信在几天前做过一个调查:请问在以下中国新锐导演中你最看好谁?韩寒.徐峥.邓超.郭 ...

  6. 三星手机换新卡显示无服务器,玩机“老炮”告诉你三星手机新玩法

    原标题:玩机"老炮"告诉你三星手机新玩法 目前的整个手机市场都已趋于瓶颈,如何在瓶颈中寻找突破,每个厂商都有自己的方向. 就拿当前来说许多厂商一个劲儿的推新配置.新产品,恨不得互联 ...

  7. 搞砸了瑞幸咖啡,资本老炮陆正耀会把小面毁了吗?

    文/琥珀消研社 作者/白露 听说了吗?割美国韭菜请中国人喝咖啡的"带慈善家"陆正耀,老陆,又撸起袖子准备进军餐饮业了. 曾经的资本眼泪.正义铁锤.华尔街收割机,这回,估计暴打不了资 ...

  8. 华为OD机试(21-40)老题库解析Java源码系列连载ing

    华为OD机试算法题新老题库练习及源码 老题库 21.字符串序列判定 22.最长的指定瑕疵度的元音子串 23.处理器问题 24.单向链表中间节点 25.字符串重新排列.字符串重新排序 26.完美走位 2 ...

  9. 存储圈老炮大战小鲜肉

    一.历史 2008年的时候,冬瓜哥在NetApp大连全球技术支持中心担任二线技术支持工程师,当时可以说是国内最懂NetApp底层技术实现的人之一.当然,7年过去了,冬瓜哥对NetApp方面的知识掌握可 ...

最新文章

  1. 利用脚本生成GUID
  2. Windows环境下学习Lisp和Scheme的两大利器
  3. 深入浅出Yolo系列之Yolov3Yolov4Yolov5核心基础知识完整讲解
  4. stdmove 跟 左键引用 区别_JavaScript 基本数据类型和引用类型的区别详解
  5. 初阶和高阶产品之间的核心差距
  6. 用fuser或者lsof解决无法umount问题(device is busy)
  7. 用libtommath实现RSA算法
  8. 又遇到jqGrid在chrome下宽度不正常有滚动条
  9. Zabbix钉钉机器人报警
  10. 自学python能做erp吗_erp系统可以自学吗?应该怎么学习?
  11. azure微软文字转语音工具​AzureTools​使用
  12. NCM格式如何转换为Mp3(简单快速)
  13. 按蚂蚁金服面试不过,就因为不会RPC服务超时排查思路?
  14. 游戏本自动掉帧_实用 | 大夏天,如何解决卡顿掉帧?
  15. git 设置代理的方法
  16. 华为交换机初始化_我想问华为 s5700交换机 1。请问怎么恢复出厂设置? 2。如果没有密码又怎么恢复出厂设置? 谢谢了...
  17. windows 10 笔记本无法连接无线网,显示已关闭
  18. J9数字论:DAO与Web3的联系
  19. JZOJ 7066. 【2021.4.24 NOI模拟】ehzeux与圆周(DP)
  20. easyExcell

热门文章

  1. 获得反向链接的正确方法
  2. 自动生成卡密SQL脚本(转载)
  3. 瑞康医药的上云之旅:企业数字化转型首先要选好平台
  4. 女王大学计算机科学,女王大学计算机科学专业本科课件.pdf
  5. python爬取王者_爬虫 抓取王者荣耀所有英雄皮肤高清壁纸+超强注释
  6. 标品怎样开直通车?标品开直通车的步骤是什么?标品怎样开直通车能获得高转化?
  7. 大意是没有经历过贫穷的人,很难成为优秀的人才。
  8. windows2007 iis安装
  9. 专家解读:读研到底值不值(转自中华英才网)
  10. echarts如何在json地图上设置多种颜色的点位和自定义背景弹出框