以太坊DApp开发初探

文|李伟志

       关于“以太猫”的流行,相信不少人都有所耳闻,甚至入手养过几只。从游戏性来说,其本质就是一个简单的收集交换类游戏,然鹅,是区块链赋予了它魅力,让用户每一只猫永远不会消失、不被篡改,更重要的是可以炒(滑稽脸),于是今天借此机会一探以太坊应用DApp的开发过程以及开发中遇到的坑。

        以太坊DApp介绍

        以太坊是一个区块链公有链平台,和比特币类似,以太坊也有其代币--以太币,可在挖矿、交易中获得,然而,说到以太坊和比特币的区别就是其支持智能合约,一个智能合约由代码和数据组成,和其他编程语言中的类类似,一个以太坊分布式应用DApp由众多智能合约组成,每个智能合约都有其独特的地址,可以看做以太坊上的一个账户,可以存取以太币,作用就像一个裁判、中间人。一个简单但不是很恰当的例子就是赌博,我和小明打赌明天会下雨,输的人给赢的人一百块,这种情况我们在现实中一般会以下面两种方法实现:

        依靠朋友间的信任。等明天到了,根据下雨与否进行交易。但这种方法一般不可行,因为毕竟是朋友,输的人会自动把昨天的打赌作为玩笑话,而赢的人也碍于面子不好意思要钱,所以交易无法达成。

        依靠公证的第三方。OK,我们这次认真点,找一个彼此都认识的朋友小方作为公证,把我和小明的赌注一百块都先存着,等明天到了再给赢的人两百块。这种方法确实比第一种要好,但还是害怕就是第三方拿着两个人的赌注夹带私逃了,这对交易双方的损失更大。

       OK,智能合约就是为了解决以上的信任问题而诞生的,由于智能合约存放于区块链,而区块链具有的不可抵赖和不可篡改性,使得智能合约比现实中任意一个机构的公信力都强。其实,区块链去中心化思想最大的优势就是解决了信任问题,而现实中最常见需要解决信任问题的场景莫过于涉及货币交易,从以太坊的众多DApp列表https://www.stateofthedapps.com/中看到,大多数都是关于交易、赌博性质的应用,可以说“以太猫”的横空出世刷新了人们对于区块链应用的固有认知。

开发准备

开发以太坊DApp需要安装以下环境或工具,以Mac OS X为例

$ brew install node以太坊DApp其他开发工具都是通过npm安装的,node.js大法好,mac用户可通过homebrew安装。

$ npm install ethereumjs-testrpc以太坊提供的区块链测试环境,所有节点都是虚拟的存在内存中,启动后默认创建10个账户。读者也可以选择安装geth搭建私有链,使用真实节点存储。

$ npm install web3以太坊提供读写区块链数据的JavaScript接口,源码地址:https://github.com/ethereum/web3.js/,通过web3.js我们可以访问各个账户、部署智能合约、调用合约方法、发起交易等等。

$ npm install truffle第三方提供的开源以太坊DApp集成工具,源码地址:https://github.com/trufflesuite/truffle,truffle工具会帮助我们编译、测试、打包和部署DApp项目中的所有合约,类似的还有Meteor(官方推荐工具,但实用下来感觉没有truffle方便,而且文档也较少)。

以下是非必需工具

$ npm install truffle-contract基于web3.js封装的JavaScript与智能合约交互接口,通过链式调用将对合约的各个操作串联在一起,具体API参考源码地址:https://github.com/trufflesuite/truffle-contract

$ npm install expressnode.js社区中基于connect流行的服务器开发框架,本文使用该框架搭建后台服务器,读者可自行选择其他框架。

编程语言

编写一个DApp可以说是包括两部分,合约部分和业务逻辑部分。

智能合约

Solidity,类JavaScript,这是以太坊推荐的旗舰语言,也是最流行的智能合约语言,具体用法参考http://solidity.readthedocs.io/en/latest/,本文所有合约都使用该语言编写,另外测试、调试Solidity有一个非常好的在线IDE--Remixhttps://remix.ethereum.org/,由以太坊团队推出的。

Serpent,类Python。

LLL,类Lisp。

业务逻辑

业务逻辑部分即提供客户端与智能合约交互的接口,相当于目前BS结构中的后台逻辑,因此业务逻辑部分可部署在中心服务器中,而且在以太坊中每个智能合约函数的每一行代码都有固定的gas费用以及延时的,一些简单的逻辑应该交由业务逻辑处理,编写业务逻辑目前提供有以下几种语言:

JavaScript,主要是基于Web3.js这个库调用智能合约,本文例子也是使用JavaScript编写的。

Go,上述提到的以太坊私链搭建工具geth就是使用Go编写的。

Python

Java

Ruby

Haskell

Rust

DApp实践

废话不多说,下面我们通过一个DApp例子来窥探一下区块链智能合约的魅力,demo源码地址:https://github.com/Dave1991/QzoneBlockPet

Demo功能介绍

该demo是一个卡片收集类游戏,业务场景为每个用户都拥有一只随机的宠物,用户通过收集卡片作用于宠物身上进行装扮,而卡片的收集来源分三种:

系统定期为随机用户生成卡片

与其他用户交换卡片

在卡片商城中购买卡片

Demo目录结构

我们通过$ truffle init命令创建一个DApp项目,truffle会帮我们组织好一个DApp的目录结构,如下所示,其中app目录为笔者添加的,用于存放业务逻辑代码。

app

业务逻辑代码,后面再展开讨论

varMigrations=artifacts.require("./Migrations.sol");varPetCard=artifacts.require("./PetCard.sol");varUserCenter=artifacts.require("./UserCenter.sol");module.exports=function(deployer){deployer.deploy(Migrations);deployer.deploy(PetCard);deployer.deploy(UserCenter);};

build

合约编译生成目录,不要手动修改

contracts

合约目录,后面展开讨论

migrationstruffle部署配置文件,新的合约需要部署需要修改里面的配置文件1_initial_migration.js,该demo包含两个合约,加上truffle部署时需要使用的合约,一共三个合约,代码如下所示,当添加一个合约时需要在该文件中添加合约变量而且需要通过deployer部署到区块链,需要注意的是这里当前目录是contracts目录。

test

合约的测试文件,我们可以在该目录中存放各个合约的测试代码,类似于其他编程语言中的单元测试,该文章不展开讨论。

module.exports={networks:{development:{host:"localhost",port:8545,network\_id:"\*"// Match any network id}}};

truffle.js

区块链网络配置文件,在truffle部署合约时会使用该文件定义的地址,目前配的是testrpc默认测试环境,如下所示:

Demo运行方式

安装上述提到的依赖(包括非必需)

$ testrpc启动区块链测试环境,可以看到testrpc在内存中为我们创建了10个虚拟账户以及对应的私钥。

image.png

$ truffle compile编译智能合约,底层调用的是solc编译器,该编译方式是增量的,如果要全量编译,可加上--all参数。

image.png

$ truffle migrate --reset部署所有智能合约,部署的环境由truffle.js定义,和compile类似,migrate也是增量部署,如果要重新部署所有合约,可加上--reset参数。

image.png

$ cd app

$ npm start启动服务器

浏览器访问localhost:8080,目前提供的接口详见INTERFACE.md文件,下面展示其中两个接口。

生成卡片

接口名称方法路由参数

createRandomCardGET无

例子返回

/createRandomCard{"cardId":"2","code":"0x616161666","owner":"0x5727b589bca4500e896ffc82e3fedf56cae7017f","value":"52"}

获取用户所有卡片

接口名称方法路由参数

getAllCardsForUserGET/:address

例子返回

/getAllCardsForUser/0xc3d9b7ea1e42b04dddf3475b464bb1abd5f8451f{"cardId":"0","code":"0x616161666","value":"4"}

需要注意的是上面两个方法调用前都需要设置gas(以太坊交易手续费),不过由于demo运行在testrpc中所有账户的balance都是虚拟的,业务逻辑直接从接口调用方账户扣除了gas,对其屏蔽了该过程,但如果正式部署到生产环境我们需要先询问用户是否愿意付该笔gas然后再真正调用合约接口,因此,以太坊的web3.js提供了estimateGas方法来预估合约函数执行所需的gas。

编写智能合约

智能合约使用Solidity语言编写,语法有点类似于JavaScript,文件名以.sol结尾,通常来说一个.sol文件定义一个合约,相当于Java中一个文件定义一个public class。一个合约通常包含两部分,成员变量和成员函数。

进入本demo的contracts目录,可以看见里面包含了以下文件:

Migrations.sol:truffle创建目录时创建的合约,用于部署DApp

PetCard.sol:本demo核心合约,定义了宠物卡片合约

strings.sol:第三方定义的字符串类库,本demo主要使用了其分割字符串的函数

UserCenter.sol:用户中心合约,用于注册用户和查询用户

下面展示的是宠物卡片合约的部分代码。

pragma solidity^0.4.17;contract PetCard{struct Card{bytes32 code;//卡片代码,决定卡片的功能uint256 value;address owner;bool isSelling;uint sellingPrice;uint cardId;}enumErrorCode{ERROR_NO_ERROR,ERROR_INDEX_OUT_OF_RANGE,ERROR_WRONG_OWNER,ERROR_CARD_IS_SELLING,ERROR_CARD_IS_NOT_SELLING,ERROR_PRICE_NOT_ENOUGH}Card[]cards;address CEO;functionPetCard()publicpayable{CEO=msg.sender;}// 匿名函数,当外部调用找不到时调用该函数eventFallbackTrigged(bytes data);function()publicpayable{FallbackTrigged(msg.data);}eventBuyCardEvent(uint cardId,bool isSuccess,ErrorCode errorCode);// 从卡片商城中购买卡片functionbuyCard(uint cardId)publicpayable{address buyer=msg.sender;// 判断card下标是否合法,不合法时退款给买家if(cardId>=cards.length||cardId<0){buyer.transfer(msg.value);BuyCardEvent(cardId,false,ErrorCode.ERROR_INDEX_OUT_OF_RANGE);return;}Card storage card=cards[cardId];// 判断消费金额是否小于card价格if(msg.value=card.sellingPrice){card.owner.transfer(card.sellingPrice);}card.owner=buyer;card.isSelling=false;card.sellingPrice=0;BuyCardEvent(cardId,true,ErrorCode.ERROR_NO_ERROR);}// 获取用户所有卡片functiongetAllCardsForUser()publicconstantreturns(uint[]cardIds,bytes32[]codes,uint[]values,uint len){cardIds=newuint[](cards.length);codes=newbytes32[](cards.length);values=newuint[](cards.length);// codes = new string[](cards.length);len=0;for(uint i=0;i

定义卡片结构与成员变量

合约内部可以定义多个结构体,关键字为struct,结构体内部也可定义成员变量,允许的类型和合约一样。此外,合约支持数据类型包括以下几种:

整型,uintx / intx,其中x代表整型所占用的位数,从8到256,步长为8,如果我们直接使用uint / int,则与uint256 / int256等价。

布尔型,bool,有true/false两个值。

浮点型,fixedMxN / ufixedMxN,浮点数在Solidity中支持得不是很好,它与其他语言中的浮点数并不一样,Solidity中浮点数在声明时就必须确定长度,而其他语言是可变的,M代表的是浮点数占用的总位数,从8到256,步长为8,N代表小数部分的长度,范围是0-80。

定长字节型,bytesx,其中x代表变量所占字节长度,范围是1-32,当变量打印出来时,显示的是十六进制。

变长字节型,bytes或string,两者区别在于bytes使用十六进制标识,string是用UTF-8表示。

地址,address, 等价于bytes20,而且Solidity为地址变量预设了几个方法,例如,balance方法获取地址对应账户的余额,transfer方法转账以太币到地址对应的账户中,转账者为调用者,收款者为address,另一个方法send类似于transfer也是转账,但值得注意的是,当transfer失败时,会回滚交易并抛出异常,而send方法则不会。

枚举,enum,和其他语言一样,Solidity也支持枚举值,语法也类似,可参考代码中错误码枚举值的定义。

根据上述的数据类型,我们定义卡片的结构体,包括卡片代码、卡片价值、卡片拥有者、卡片是否正在出售、卡片出售价格以及卡片id。然后,定义了函数执行可能会发生的错误码,还有一个卡片的集合以及合约的创建者CEO。

struct Card{bytes32 code;//卡片代码,决定卡片的功能uint256 value;address owner;bool isSelling;uint sellingPrice;uint cardId;}enumErrorCode{ERROR_NO_ERROR,ERROR_INDEX_OUT_OF_RANGE,ERROR_WRONG_OWNER,ERROR_CARD_IS_SELLING,ERROR_CARD_IS_NOT_SELLING,ERROR_PRICE_NOT_ENOUGH}Card[]cards;address CEO;

函数

在Solidity中函数的定义语法是

function函数名(参数列表) 修饰符returns(返回值列表)

这里值得注意的是,在函数生命中返回值列表我们可以声明返回值的名字,类似于形参,当在函数体中给返回值变量赋值后,我们可以不用写return,但如果写了还是以return为主,同时,一个函数返回值支持多个,调用者拿到的将是一个返回值数组,和python有点像。

另外,EVM会给每个合约的函数传入一个名为msg的对象,该对象包含几个属性,如sender是调用者账户地址、value是调用者执行该函数支付的以太币(单位是wei)、data是函数调用的描述。除了data外,其他属性的值是由调用者传入,详见业务逻辑代码的介绍。

构建函数和匿名函数

和大部分语言一样,Solidity中每个合约也有构建函数,在构建函数中我们可以做一些初始化的操作,在下面的代码中我们注意到函数后有两个修饰符,分别是public和payable,其中public说明该函数外部合约也可见,对应的还有external,private,internal,要说到这四者的区别,需要查看函数的调用方式和可见性,本文就不展开了。然后payable说明该函数会涉及货币交易,同时当我们在一个合约的其他函数中调用了转账操作,那么构建函数必须也得声明为payable。

匿名函数,也就是没有名字的函数,每个合约中最多可定义一个,当其他地方调用该合约不存在的函数或者出现异常时,EVM(以太坊智能合约执行虚拟机)会自动调用合约的匿名函数,同样地,当合约内其他函数有转账操作时匿名函数也需要加上payable修饰。

functionPetCard()publicpayable{CEO=msg.sender;}// 匿名函数,当外部调用找不到时调用该函数eventFallbackTrigged(bytes data);function()publicpayable{FallbackTrigged(msg.data);}

事件

代码中我们定义了多个event,每个event只需要定义其名字和参数列表即可以,其作用相当于其他语言中的log,在函数中传入实参即可记录,虽说event的作用和log一样,但在Solidity中作用却非同小可,因为当一个函数是以transaction的形式被调用,调用者是无法拿到返回值的,因为transaction的调用是异步的,EVM无法立刻执行给出返回值,所以调用者只能通过event的记录取得函数执行后的数据,具体操作流程见业务逻辑代码的介绍。

购买卡片

定义购买卡片的函数,函数一开始我们写了三个是否合法的判断,这里可以使用require关键字对这些条件进行限定,但由于笔者希望调用者可以接收到错误信息,这里就使用了四个if判断,并且使用了事件通知调用者,同时当条件不满足时我们需要做一些回滚操作,例如将金额退还给调用者账户。而当条件满足后,我们将卡片定价转给卖家,转移卡片拥有者。

eventBuyCardEvent(uint cardId,bool isSuccess,ErrorCode errorCode);// 从卡片商城中购买卡片functionbuyCard(uint cardId)publicpayable{address buyer=msg.sender;// 判断card下标是否合法,不合法时退款给买家if(cardId>=cards.length||cardId<0){buyer.transfer(msg.value);BuyCardEvent(cardId,false,ErrorCode.ERROR_INDEX_OUT_OF_RANGE);return;}Card storage card=cards[cardId];// 判断消费金额是否小于card价格if(msg.value=card.sellingPrice){card.owner.transfer(card.sellingPrice);}card.owner=buyer;card.isSelling=false;card.sellingPrice=0;BuyCardEvent(cardId,true,ErrorCode.ERROR_NO_ERROR);}

遍历卡片

该函数的作用是获取所有属于调用者账户的卡片,值得注意的是,该函数在EVM中是一个昂贵的操作,首先我们声明了三个定长数组(定长是和临时变量存储的地方有关),每个长度都等于所有卡片数组的大小,因此每个数组都已经开销了不少gas,然后遍历又是一个耗时操作,又需要花费gas,而且函数在编译时并不知道cards的长度,所以即使调用者使用estimategas函数预估该函数所需gas也是不准确的,这对于调用者是危险的,随时都可能因为gas不够而执行失败。

functiongetAllCardsForUser()publicconstantreturns(uint[]cardIds,bytes32[]codes,uint[]values,uint len){cardIds=newuint[](cards.length);codes=newbytes32[](cards.length);//这里不能用string,solidity不支持定长的变长数组values=newuint[](cards.length);// codes = new string[](cards.length);len=0;for(uint i=0;i

生成卡片

这里生成卡片的逻辑交给业务层,合约只负责根据参数创建一个新的卡片,最后通知调用者即业务层。

eventCreateNewCardEvent(uint cardId,bytes32 code,address owner,uint value);// 给用户掉落新卡片functioncreateNewCardForUser(bytes32 code,uint value)public{Card memory card=Card({code:code,value:value,owner:msg.sender,isSelling:false,cardId:cards.length,sellingPrice:0});cards.push(card);CreateNewCardEvent(card.cardId,card.code,card.owner,card.value);}

编写业务逻辑

合约编写完成后,可先到Remix上测试,测试通过后再使用truffle编译和部署到区块链上。之后,便是业务逻辑的编写了。

由于truffle,web3等都是依赖于node.js,为了一致性与方便性,本demo也是使用node.js构建业务服务器,主要依赖的模块是express和truffle-contract,前者用于更方便的业务路由和模块化,后者用于更方便调用合约。

打开app目录,我们会看到一下的文件结构:

PetCard.js:宠物卡片业务路由处理以及合约交互

UserCenter.js:用户中心,负责用户注册和获取所有用户的上层调用

UserCenterCore.js:用户中心核心,负责业务层与合约层交互

Web3Provider.js:定义Web3连接的是区块链地址

package.json:定义npm运行所需要的命令和依赖

server.js:业务层总入口,负责默认页面、404页面处理,以及各业务模块的中转路由,还有定义服务器绑定的端口

下面我们主要看PetCard.js中业务层是如何与合约层进行交互的。

获取合约示例

这一步我们首先获取宠物卡片合约和用户中心合约的实例,便于下面调用合约,这里我们需要依赖truffle-contract还有本地的Web3Provider模块。而truffle-contract的用法都是链式调用,通过then函数连接起来。

contract=require('truffle-contract');provider=require('./Web3Provider.js');express=require("express");constPetCard=contract(require('../../build/contracts/PetCard.json'));PetCard.setProvider(provider);varpetCard;PetCard.deployed().then(function(instance){petCard=instance;});varuserCenter;require('./UserCenterCore.js').then(function(instance){userCenter=instance;});varapp=module.exports=express();

购买卡片

从下面代码中可以看到,业务层接受客户端传递的路由参数,再传入合约层,这里合约层函数的参数分两种,一种是自定义参数,另一种就是EVM预设参数,而预设参数是一个对象,需要在最后传入,正如上面Solidity函数介绍,预设参数对象需要包括from为调用者地址,value为传入合约的以太币。最后,由于这是直接通过合约实例调用函数,是一个transaction操作,因此如上面Solidity事件介绍,我们需要从返回值的日志中获取合约执行后的数据。由于日志拿到的事件参数是一个对象,所以我们直接以json形式返回给客户端即可,例如下面的返回就表示卡片购买失败,原因是卡片当前不在销售:{"cardId":"1","isSuccess":false,"errorCode":"4"}。

app.get('/buyCard/:address/:cardId/:price',function(req,res){petCard.buyCard(req.params.cardId,{from:req.params.address,value:req.params.price}).then(function(result){if(result.logs.length>0){vareventObj=result.logs[0].args;res.send(JSON.stringify(eventObj));}});});

遍历所有卡片

遍历卡片的操作并不涉及永久写入合约数据的操作,因此遍历卡片这里我们不使用transaction,而使用call的形式,因此我们可以直接拿到函数的返回值,然后由于函数返回多个值,因此result是一个数组。这里需要注意的是,上面我们说到遍历卡片时合约需要创建三个未知长度的数组,而且遍历的次数也是未知的,因此,estimategas函数预估的gas会不准确,我们这里直接给一个比较大的gas值。该接口返回的例子如:{"cardId":"0","code":"0x616161666","value":"4"}。

app.get('/getAllCardsForUser/:address',function(req,res){// 因为这需要创建未知长度数组,estimate 估计的gas会不准确,该方法慎调petCard.getAllCardsForUser.call({from:req.params.address,gas:3000000}).then(function(result){if(result.length>=4){varcardIds=result[0],codes=result[1],values=result[2];varlen=result[3];varcards=[];for(vari=0;i

生成卡片

生成卡片的逻辑是在所有用户随机挑选一个用户作为卡片的拥有者,然后卡片的code这里先简单地写死了一串,后续可以想更好玩的code生成逻辑,接着就是调用estimateGas函数估计所需的gas,最后才是真正调用合约函数,传入预估的gas,其实比较好的交互应该像以太猫那样,在进行真正的调用之前告知用户交易所需的gas,并可以让用户调整,用户确认后再执行合约函数。下面是生成卡片调用后返回的一个例子:{"cardId":"2","code":"0x616161666","owner":"0x5727b589bca4500e896ffc82e3fedf56cae7017f","value":"52"}。

app.get('/createRandomCard',function(req,res){varallUsers,randomUser;userCenter.showAllPlayers.call().then(function(result){allUsers=result;randomIdx=Math.floor(Math.random()*allUsers.length);randomUser=allUsers[randomIdx];if(randomUser!=undefined){varcardCode="aaaforestlinbbb";varcardValue=Math.floor(Math.random()*100+1);petCard.createNewCardForUser.estimateGas(cardCode,cardValue).then(function(esti_gas){returnpetCard.createNewCardForUser(cardCode,cardValue,{from:randomUser,gas:esti_gas});}).then(function(rest){if(rest.logs.length>0){vareventObj=rest.logs[0].args;res.send(JSON.stringify(eventObj));}});}else{res.send("random user is undefined");}});});

总结DApp开发中遇到的坑

一个DApp开发流程介绍到此结束,下面总结一下开发中值得注意的地方:

Solidity这个语言目前还不是很完善,版本还是0.4.x,而且文档相对其他语言较少,这里除了官网,还推荐两个论坛区块链技术博客以太坊爱好者供大家参考。

合约函数中慎用未知长度的数组以及遍历操作,比较耗费gas,而且对于调用者极不友好,无法预估gas。

对于不需要写操作的函数,我们可以加上constant修饰符或者调用时使用call的方法而非直接调用,不产生transaction,也就不需要写入区块链。

对于不需要的数组我们可以使用delete操作删除整个数组或者某个元素,可以归还一些gas,但是最好复用,使用指示器标记当前使用的长度,因为delete操作本身也是需要耗费gas的。

合约内不适合做业务过重的操作,如上面的生成卡片操作,应该将逻辑放在业务层,毕竟在EVM中没执行一行代码都是需要gas的,合约应该只有读写区块链的操作。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容