|
|
|
|
移动端

区块链难理解?200行代码教你写一个自己的区块链!

“区块链”三个字,无疑是近一年来最火的投资概念。随着比特币等区块链资产价格的大幅飙升,普通投资者对区块链,以及数字货币投资的兴趣越来越大,突然间,似乎全世界都在谈论区块链、比特币。

作者:佚名来源:51CTO技术栈|2018-02-06 10:04

技术沙龙 | 邀您于8月25日与国美/AWS/转转三位专家共同探讨小程序电商实战

“区块链”三个字,无疑是近一年来最火的投资概念。随着比特币等区块链资产价格的大幅飙升,普通投资者对区块链,以及数字货币投资的兴趣越来越大,突然间,似乎全世界都在谈论区块链、比特币。

区块链就像一次对全人类经济层面的入侵,这种入侵,比互联网的入侵,可能还要彻底,人们开始转移的,并不是简单的信息以及消费习惯,而是对资产的重新认识和选择。

区块链难理解?这里有一篇初学者指南

我并不明白为什么人们会觉得要理解区块链会有点难,而我也想知道为什么自己并没有。

那是在 2013 年,我第一次听说有比特币这个东西(是的,知道的太晚啦)。我穷得连一个都买不起,看到这儿你也许已经对此文无爱了。

后来,我想要去了解一下它所依赖的底层技术,也就是区块链。 不过因为太忙了,所以迟迟没有开始(那就并不存在什么开始不开始了)。

“区块链”到底是什么?

区块链其实是两个东西:一个是区块,一个是链。说玄虚一点,就是一链子的区块。

因为它是存在于计算机中的东西,所以我们可以厘定它的一个物理形态是啥样子的,就是数字信息被分成一个一个区块然后把这些区块链接起来。

举个例子,下图中的方块,每一个都表示一个国家,而且每一个都包含了对应国家的城市名称。

等等,其实还有更多东西来着。这里的每一个方块都是一个叫做哈希的东西。一个哈希就是一串字符 (比如 “1hi515AHA5H” )。

哈希是根据方块里面所包含的信息来得到的。U.S.A 的方块拥有 New York, Los Angeles,还有 Chicago 这几个城市,所以它的哈希就是像 “NYLAC” 的东西了 (技术上其实远非如此,但你理会精要就行了)。

每一个接续的方块都会包含前一个方块的哈希,所以这个就是(强制性的)将它们绑到一起的纽带。

如果有人擅自篡改了第一个方块,加入了城市 Boston,那么新的哈希就会是 “NYLACB”。

然而后面接续的 India 这个方块已经存着的哈希还是 “NYLAC”,这种不匹配就会把链条打断。所以哈希的目的就是确保没有人可以篡改区块。

那如果有人修改了一个方块的内容,然后把后面的接续方块的哈希也一并更新会如何呢?

这也是有可能的,不过有一件事情我还没有告诉你。区块链的数据并不只是存在于仅仅一台计算机里面。一台计算机里面的区块链数据并不能骗到人,因为它会被复制到网络中每一个用户的计算机里面去。

如果你加入了一个区块链网络,那么你的计算机就会去下载这些区块数据,如果有人篡改了他拥有的版本,整个网络也会考虑占多数的人的计算机上所拥有的版本才是正确的。

还有一件事,在一个区块链网络中,不仅是数据,就连整个系统的程序都被复制到了所有的电脑中。

大多数互联网应用都是集中化的,比如 Facebook,她的数据和程序都被放在了她的服务器上,你的计算机会从 Facebook 的服务器上获取到你一个人需要知道的信息。

但在区块链的世界理,就没有存在于中心的东西,它依赖的是用户的计算机来容纳自己的程序。这就意味着,如果整个区块链网络中的每一台电脑都关机了,那么这个区块链系统就死翘翘了。

公共区块链

这是不是就意味着区块链系统其实就是由一群心怀善意自愿让他们的计算机保持运行的人来组成的呢? 还有这些防篡改的区块是用来干嘛的呢?

区块链网络的功效不胜枚举。比特币是一种数据货币和一个支付系统。它所有的防篡改区块中所保存的就是全部交易的分类账。那些贡献了他们自己的计算机的人被称为矿工。系统会给他们提供比特币作为奖励。

Ethereum 有一项附加功能。它可以承载你的代码,从头开始发展出一个区块链系统。

而要构建一个属于你自己的系统也许会非常地困难(记住这得看有人为你牺牲他们的计算机运行能力才行哦)。Ethereum 就维护着这些耗损巨大的运算能力,而你则需要为这些计算消耗买单。

区块链应用并不非得是支付系统或者加密货币。它可以是任何东西,像是一个社交网络,一个像 LiveEdu 这样的学习平台,等等。

私有区块链

Bitcoin,Ethereum 等等这些都是公共区块链的例子,任何人都可以成为其中的一分子。

那如果我们想要有一个私有的区块链网络该如何呢?有些人想要一个私有的区块链是想干嘛呢?那就来瞧瞧下面的故事吧。

Mark 和 Sara

Mark 已经五个月没交房租了,当 Sara 找他要的时候,他就说晚点会给她。她付不起律师费,而法院强制执行诉讼就需要 8 个月甚至一年,所以唯一的选择就是去说服 Mark。

Joe 的生意

Joe 是一个商人,他经常要跟不同的公司做生意。几个月之前他和一家零售商签了一份合同,尽管合同条款都已经履约了,可零售商却拒绝付款。

这帮人利用法律制度中的漏洞来游说 Joe,想以此达到少付钱的目的。Joe 在这以前就是有这方面经验的,在某些情况下,他会找法院求助,但这样做所耗费的时间和金钱却要损失他自己的利润。

我们该如何帮助 Sara 和 Joe 呢?

我们是不是能在其他地方解决这个问题呢? 在 Sara 遇到的这种情况中,我们需要让 Mark  按月支付房租,这其实就是一个基于时间的触发机制。你的日历程序使用这样的触发器来给你提供预设事件的通知。

在 Joe 遇到的场景中,一旦合约中的条款都满足了,当事人就得付款,这其实就是一个基于条件的触发机制。你想想上次从 Amazon 买电子书的时候,是不是得先确认付款了,Amazon 才会把电子书发给你?

重点是,计算机程序会始终如一的执行诸如此类的指令。当你点击着这篇文章,向下滚动,诸如这类的操作,它也会照着执行不误。为了能帮助到 Sara ,我们需要将合同的条款转变成代码。

Sara 和 Mark 之间所订立的智能合同的伪代码

  • If today’s date is 30th and rent is not paid then
  • Transfer $500 from Mark’s account to Sara’s account

可是我们在哪儿部署这些代码呢? 它就应该被部署到所有参与者的计算机上。Sara 的还有 Mark 的银行都会是这一个私有区块链网络的一部分。

Joe 和 Sara 会签署一份编码的协议(也就是智能合同),然后这份协议会被分发到网络中去,Mark 的和 Sara 的银行都会有一份拷贝。

在每个月的 30 号,当时钟跳动到 12 点整,协议好的金额就会从 Mark 的账户转移到 Sara 的账户上去。Joe 也开始使用智能合同来强制让他的客户支付协议好的货款。

  • Sara 高兴了,因为她再也不用去烦心 Mark 会不会如约付房租了。Joe 也高兴,因为他也不用找法院要说法了,省下这些精力,他可以继续发展自己的生意了。

私有区块链只限于业务中涉及到的相关各方,因此 Joe 不会是 Sara 和 Mark 所属区块链网络的一部分。

前行之路

现在你对此是不是已经有点概念了?如果还是不理解,看看小编之前发布的文章技术人再不懂区块链,你就OUT了?不过下面这篇文章也能让你秒懂区块链。

区块链与裸照:一个去中心化的色情网站是什么样的?

“区块链”概念已火,虽然大部分人对“区块链”好奇,甚至眼馋,但不少还处于不求甚解的懵逼阶段.....正好最近我一直在研究区块链,同时也见了几个圈内人深聊了下,就想为大家写一个“入门级”的区块链介绍文章。

为了通俗易懂,我决定不惜自毁清誉,用充满荷尔蒙的比喻。因为技浪潮每次确实都性感得让人荷尔蒙爆炸啊,性(huang)感(bao)内容开始。

以前,大家想看陈老师的裸照,都要去一个叫 1025 的网站,这就是中心化。

后来,1025 网站被和谐掉了,大部分猥琐男们傻逼了没有网站看片了,因为他们太相信中心化组织了,还天天被 1025 弹窗“皇家澳门赌场”的小广告真是活该啊。

不过没关系,陈老师的 2100 张裸照,幸存在 100 万个猥琐男的电脑里,除非地球毁灭,不然陈老师的裸照不可能绝迹。这就是去中心化,数据分布式存储。

后来,有个叫“中本粗”的超级猥琐男,是陈老师 2100 张裸照的超级发骚友。为了2100张照片永远不消失,为世人所享用,他做了一个互联网共享文件夹“陈老师plus”。

如果猥琐男们想获取“陈老师plus”2100 张的观看权,就必须加入一个电子协议中:不得复制、修改、P 任何“陈老师plus”中的照片,用户在“陈老师plus”发生的任何行为,都会按时间戳记录!

例如,“小张在 2018 年 1 月 9 日中午 12:00,查看了编号为 103 的照片,并在 13:00 删除了编号 1-100 的 100 张照片.....”

小张的行为被记录并广播给其他 100 万个猥琐男,“陈老师plus”的 2100 张照片会得到保护,小张电脑中“陈老师plus”会按时间戳中最新记录,同步其他 100 万个猥琐男的电脑里的数据,复原小张电脑中的数据...

小张永远别想对“陈老师plus”搞修改破坏,且所有行为都同步记录在其他猥琐男的电脑里。

这就是区块链,数据分散存储,去中心化,按时间戳广播记录所有行为,无法修改、破坏数据源或造假,除非同一时刻炸掉 100 万个猥琐男的电脑,或互联网消失,或世界毁灭.....

当然,也有唯一一种特殊情况,可以增加“陈老师plus”文件夹中的照片,这种情况叫做“区块链共识层”,顾名思义,这是 100 万猥琐男达成的增加照片共识,你不能瞎增加的。

中本粗最初设定协议时,把“猥琐男们可以用 X 相机,在每年 XX 时间,拍陈老师的裸照,前 100 张可以添加进入‘陈老师plus’中增加作为文件夹照片”,那么,“陈老师plus”每年就可以增长 100 张照片了。

当然,你还可以给照片估价嘛,发行“陈老师plus”币。因为,“陈老师plus”中每张照片都是不可造假破坏的,所以具有唯一性,还有单独编号,我们就给每一张照片估价,它不就值钱了吗?就像现实世界中无法复制的名画一样啊!

怎么估值?就进行所谓的 ICO(Initial Coin Offering)啊,就是我和李哭来老师成立一个基金,举行一张发布会,就说我们给这 2100 张照片估值个 1.05 亿!每张照片 5 万!

我们先丢 5050 万进去认购前 1100 张,其他猥琐男可以众筹 5000 万买剩下的 1000 张照片,不想要了?卖给我和李哭来基金就行,我们认它值钱啊,我们这么牛逼不会骗你的。

自从有了区块链——我们再也不怕 1025 们作恶,给我们弹窗小广告,给我们下病毒了;再也不怕陈老师的照片丢失被破坏了,1984 老大哥复活都做不到.....

当然,也有烦恼,就是有炒名画的现在来炒“陈老师plus”照片了,把价格搞得很高。

还有的更猥琐,为了赚钱,自己拍了一堆裸照按照这个模式弄了个“某某老师plus”东施效颦收割韭菜,这就叫山寨币,现在大概快一百种了吧。

看到这里,你肯定能明白区块链了......最后,教大家怎么用 200 行 Go 代码写一个自己的区块链!

只用 200 行 Go 代码写一个自己的区块链

这篇文章就是帮助你使用 Go 语言来实现一个简单的区块链,用不到 200 行代码来揭示区块链的原理!

“用不到 200 行 Go 代码就能实现一个自己的区块链!” 听起来有意思吗?有什么能比开发一个自己的区块链更好的学习实践方法呢?那我们就一起来实践下!

因为我们是一家从事医疗健康领域的科技公司,所以我们采用人类平静时的心跳数据(BPM 心率)作为这篇文章中的示例数据。

让我们先来统计一下你一分钟内的心跳数,然后记下来,这个数字可能会在接下来的内容中用到。

通过本文,你将可以做到:

  • 创建自己的区块链
  • 理解 hash 函数是如何保持区块链的完整性
  • 如何创造并添加新的块
  • 多个节点如何竞争生成块
  • 通过浏览器来查看整个链
  • 所有其他关于区块链的基础知识

但是,对于比如工作量证明算法(PoW)以及权益证明算法(PoS)这类的共识算法文章中将不会涉及。

同时为了让你更清楚得查看区块链以及块的添加,我们将网络交互的过程简化了,关于 P2P 网络比如“全网广播”这个过程等内容将在下一篇文章中补上。让我们开始吧!

设置

我们假设你已经具备一点 Go 语言的开发经验。在安装和配置 Go 开发环境后之后,我们还要获取以下一些依赖:

  1. go get github.com/davecgh/go-spew/spew 

spew 可以帮助我们在 console 中直接查看 struct 和 slice 这两种数据结构。

  1. go get github.com/gorilla/mux 

Gorilla 的 mux 包非常流行, 我们用它来写 Web handler。

  1. go get github.com/joho/godotenv 

godotenv 可以帮助我们读取项目根目录中的 .env 配置文件,这样我们就不用将 http port 之类的配置硬编码进代码中了。比如像这样:

  1. ADDR=8080 

接下来,我们创建一个 main.go 文件。之后我们的大部分工作都围绕这个文件,让我开始编码吧!

导入依赖

我们将所有的依赖包以声明的方式导入进去:

  1. package main 
  2.  
  3. import ( 
  4.     "crypto/sha256" 
  5.     "encoding/hex" 
  6.     "encoding/json" 
  7.     "io" 
  8.     "log" 
  9.     "net/http" 
  10.     "os" 
  11.     "time" 
  12.  
  13.     "github.com/davecgh/go-spew/spew" 
  14.     "github.com/gorilla/mux" 
  15.     "github.com/joho/godotenv" 

数据模型

接着我们来定义一个结构体,它代表组成区块链的每一个块的数据模型:

  1. type Block struct { 
  2.     Index     int 
  3.     Timestamp string 
  4.     BPM       int 
  5.     Hash      string 
  6.     PrevHash  string 
  • Index 是这个块在整个链中的位置。
  • Timestamp 显而易见就是块生成时的时间戳。
  • Hash 是这个块通过 SHA256 算法生成的散列值。
  • PrevHash 代表前一个块的 SHA256 散列值。
  • BPM 每分钟心跳数,也就是心率。还记得文章开头说到的吗?

接着,我们再定义一个结构表示整个链,最简单的表示形式就是一个 Block 的 slice:

  1. var Blockchain []Block 

我们使用散列算法(SHA256)来确定和维护链中块和块正确的顺序,确保每一个块的 PrevHash 值等于前一个块中的 Hash 值,这样就以正确的块顺序构建出链:

散列和生成块

我们为什么需要散列?主要是两个原因:

  • 在节省空间的前提下去唯一标识数据。散列是用整个块的数据计算得出,在我们的例子中,将整个块的数据通过 SHA256 计算成一个定长不可伪造的字符串。
  • 维持链的完整性。通过存储前一个块的散列值,我们就能够确保每个块在链中的正确顺序。任何对数据的篡改都将改变散列值,同时也就破坏了链。

以我们从事的医疗健康领域为例,比如有一个恶意的第三方为了调整“人寿险”的价格,而修改了一个或若干个块中的代表不健康的 BPM 值,那么整个链都变得不可信了。

我们接着写一个函数,用来计算给定的数据的 SHA256 散列值:

  1. func calculateHash(block Block) string { 
  2.     record := string(block.Index) + block.Timestamp + string(block.BPM) + block.PrevHash 
  3.     h := sha256.New() 
  4.     h.Write([]byte(record)) 
  5.     hashed := h.Sum(nil) 
  6.     return hex.EncodeToString(hashed) 

这个 calculateHash 函数接受一个块,通过块中的 Index,Timestamp,BPM,以及 PrevHash 值来计算出 SHA256 散列值。

接下来我们就能便携一个生成块的函数:

  1. func generateBlock(oldBlock Block, BPM int) (Block, error) { 
  2.     var newBlock Block 
  3.  
  4.     t := time.Now() 
  5.     newBlock.Index = oldBlock.Index + 1 
  6.     newBlock.Timestamp = t.String() 
  7.     newBlock.BPM = BPM 
  8.     newBlock.PrevHash = oldBlock.Hash 
  9.     newBlock.Hash = calculateHash(newBlock) 
  10.  
  11.     return newBlock, nil 

其中,Index 是从给定的前一块的 Index 递增得出,时间戳是直接通过 time.Now() 函数来获得的,Hash 值通过前面的 calculateHash 函数计算得出,PrevHash 则是给定的前一个块的 Hash 值。

校验块

搞定了块的生成,接下来我们需要有函数帮我们判断一个块是否有被篡改。检查 Index 来看这个块是否正确得递增,检查 PrevHash 与前一个块的 Hash 是否一致,再来通过 calculateHash 检查当前块的 Hash 值是否正确。

通过这几步我们就能写出一个校验函数:

  1. func isBlockValid(newBlock, oldBlock Block) bool { 
  2.     if oldBlock.Index+1 != newBlock.Index { 
  3.         return false 
  4.     } 
  5.     if oldBlock.Hash != newBlock.PrevHash { 
  6.         return false 
  7.     } 
  8.     if calculateHash(newBlock) != newBlock.Hash { 
  9.         return false 
  10.     } 
  11.     return true 

除了校验块以外,我们还会遇到一个问题:两个节点都生成块并添加到各自的链上,那我们应该以谁为准?这里的细节我们留到下一篇文章,这里先让我们记住一个原则:始终选择最长的链。

通常来说,更长的链表示它的数据(状态)是更新的,所以我们需要一个函数能帮我们将本地的过期的链切换成最新的链:

  1. func replaceChain(newBlocks []Block) { 
  2.     if len(newBlocks) > len(Blockchain) { 
  3.         Blockchain = newBlocks 
  4.     } 

到这一步,我们基本就把所有重要的函数完成了。接下来,我们需要一个方便直观的方式来查看我们的链,包括数据及状态。通过浏览器查看 Web 页面可能是最合适的方式!

Web 服务

我猜你一定对传统的 Web 服务及开发非常熟悉,所以这部分你肯定一看就会。

借助 Gorilla/mux 包,我们先写一个函数来初始化我们的 Web 服务:

  1. func run() error { 
  2.     mux := makeMuxRouter() 
  3.     httpAddr := os.Getenv("ADDR"
  4.     log.Println("Listening on ", os.Getenv("ADDR")) 
  5.     s := &http.Server{ 
  6.         Addr:           ":" + httpAddr, 
  7.         Handler:        mux, 
  8.         ReadTimeout:    10 * time.Second
  9.         WriteTimeout:   10 * time.Second
  10.         MaxHeaderBytes: 1 << 20, 
  11.     } 
  12.  
  13.     if err := s.ListenAndServe(); err != nil { 
  14.         return err 
  15.     } 
  16.  
  17.     return nil 

其中的端口号是通过前面提到的 .env 来获得,再添加一些基本的配置参数,这个 web 服务就已经可以 listen and serve 了!

接下来我们再来定义不同 endpoint 以及对应的 handler。例如,对“/”的 GET 请求我们可以查看整个链,“/”的 POST 请求可以创建块。

  1. func makeMuxRouter() http.Handler { 
  2.     muxRouter := mux.NewRouter() 
  3.     muxRouter.HandleFunc("/", handleGetBlockchain).Methods("GET"
  4.     muxRouter.HandleFunc("/", handleWriteBlock).Methods("POST"
  5.     return muxRouter 

GET 请求的 handler:

  1. func handleGetBlockchain(w http.ResponseWriter, r *http.Request) { 
  2.     bytes, err := json.MarshalIndent(Blockchain, """  "
  3.     if err != nil { 
  4.         http.Error(w, err.Error(), http.StatusInternalServerError) 
  5.         return 
  6.     } 
  7.     io.WriteString(w, string(bytes)) 

为了简化,我们直接以 JSON 格式返回整个链,你可以在浏览器中访问 localhost:8080 或者 127.0.0.1:8080 来查看(这里的 8080 就是你在 .env 中定义的端口号 ADDR)。

POST 请求的 handler 稍微有些复杂,我们先来定义一下 POST 请求的 payload:

  1. type Message struct { 
  2.     BPM int 

再看看 handler 的实现:

  1. func handleWriteBlock(w http.ResponseWriter, r *http.Request) { 
  2.     var m Message 
  3.  
  4.     decoder := json.NewDecoder(r.Body) 
  5.     if err := decoder.Decode(&m); err != nil { 
  6.         respondWithJSON(w, r, http.StatusBadRequest, r.Body) 
  7.         return 
  8.     } 
  9.     defer r.Body.Close() 
  10.  
  11.     newBlock, err := generateBlock(Blockchain[len(Blockchain)-1], m.BPM) 
  12.     if err != nil { 
  13.         respondWithJSON(w, r, http.StatusInternalServerError, m) 
  14.         return 
  15.     } 
  16.     if isBlockValid(newBlock, Blockchain[len(Blockchain)-1]) { 
  17.         newBlockchain := append(Blockchain, newBlock) 
  18.         replaceChain(newBlockchain) 
  19.         spew.Dump(Blockchain) 
  20.     } 
  21.  
  22.     respondWithJSON(w, r, http.StatusCreated, newBlock) 
  23.  

我们的 POST 请求体中可以使用上面定义的 payload,比如:

  1. {"BPM":75} 

还记得前面我们写的 generateBlock 这个函数吗?它接受一个“前一个块”参数,和一个 BPM 值。

POST handler 接受请求后就能获得请求体中的 BPM 值,接着借助生成块的函数以及校验块的函数就能生成一个新的块了!

除此之外,你也可以:

  • 使用 spew.Dump 这个函数可以以非常美观和方便阅读的方式将 struct、slice 等数据打印在控制台里,方便我们调试。
  • 测试 POST 请求时,可以使用 POSTMAN 这个 chrome 插件,相比 curl它更直观和方便。

POST 请求处理完之后,无论创建块成功与否,我们需要返回客户端一个响应:

  1. func respondWithJSON(w http.ResponseWriter, r *http.Request, code int, payload interface{}) { 
  2.     response, err := json.MarshalIndent(payload, """  "
  3.     if err != nil { 
  4.         w.WriteHeader(http.StatusInternalServerError) 
  5.         w.Write([]byte("HTTP 500: Internal Server Error")) 
  6.         return 
  7.     } 
  8.     w.WriteHeader(code) 
  9.     w.Write(response) 

快要大功告成了

接下来,我们把这些关于区块链的函数,Web 服务的函数“组装”起来:

  1. func main() { 
  2.     err := godotenv.Load() 
  3.     if err != nil { 
  4.         log.Fatal(err) 
  5.     } 
  6.  
  7.     go func() { 
  8.         t := time.Now() 
  9.         genesisBlock := Block{0, t.String(), 0, """"
  10.         spew.Dump(genesisBlock) 
  11.         Blockchain = append(Blockchain, genesisBlock) 
  12.     }() 
  13.     log.Fatal(run()) 
  14.  

这里的 genesisBlock (创世块)是 main 函数中最重要的部分,通过它来初始化区块链,毕竟第一个块的 PrevHash 是空的。

哦耶!完成了

你们可以从这里获得完整的代码:Github repo[1]

让我们来启动它:

  1. go run main.go 

在终端中,我们可以看到 web 服务器启动的日志信息,并且打印出了创世块的信息:

接着我们打开浏览器,访问 localhost:8080 这个地址,我们可以看到页面中展示了当前整个区块链的信息(当然,目前只有一个创世块):

接着,我们再通过 POSTMAN 来发送一些 POST 请求:

刷新刚才的页面,现在的链中多了一些块,正是我们刚才生成的,同时你们可以看到,块的顺序和散列值都正确。

下一步

刚刚我们完成了一个自己的区块链,虽然很简单(陋),但它具备块生成、散列计算、块校验等基本能力。

接下来你就可以继续深入的学习区块链的其他重要知识,比如工作量证明、权益证明这样的共识算法,或者是智能合约、Dapp、侧链等等。

目前这个实现中不包括任何 P2P 网络的内容,我们会在下一篇文章中补充这部分内容,当然,我们鼓励你在这个基础上自己实践一遍!

【编辑推荐】

  1. 厉害了!神经网络替你写前端代码
  2. 不需要任何代码知识的开发工具—GrapesJS
  3. 区块链输不起,智能造车等不起,2018年没有哪个领域让大佬如此焦虑
  4. 码农福音:微软车库项目 Ink to Code 将 UI 草图转换成代码
  5. 区块链投资十大风险点,不了解这些就不要投资了
【责任编辑:武晓燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

读 书 +更多

.NET for Flash动态网站开发手札

本书深入浅出地说明了如何利用.NET、Flash及XML来辅助Flash富媒体应用程序的开发。 本书首先介绍了Flash影片应用程序与.NET应用程序结合的...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊