ProtocolBuffer for Swift

参考:http://www.jianshu.com/p/8c6c009bc500

http://blog.csdn.net/kmyhy/article/details/70243065

第一步、配置环境(跟着github 上面配置就行了)

第二步、把项目从gitHub拉下来(以下都是根据这个github的内容配置的):https://github.com/alexeyxo/protobuf-swift (使用终端操作)

以下两个方法供使用:

$ git clone git@github.com:alexeyxo/protobuf-swift.git

$ git clone https://github.com/alexeyxo/protobuf-swift.git

第三步、执行 ./scripts/build.sh 文件  (使用终端操作)

使用终端cd到protobuf-swift目录下,然后直接在终端输入以下命令执行(貌似这是一个脚本,反正正常执行会生成一大坨文件)

$ ./scripts/build.sh

第四步、集成protobuf-swift  (手动拖进项目, 或者pod管理)

1.直接拖拽进你的工程中,然后添加编译文件:target--> build phases  -> Link binary with libraries  (注意: 拖进去的/ProtocolBuffers.xcodeproj文件, 在你的工程里不会存在真实的文件夹, 这里拖进去只是一个引用)

2.然后记得 pod 一下 或者把Source文件夹拖进工程:protobuf-swift/Source    (和 使用第三方框架一样原理)

$ pod 'ProtocolBuffers-Swift'

$ pod install

第五步、用你自己定义的 .proto 文件生成一个 .swift 文件,.proto 文件使用来写protobuf代码的 (使用终端操作)

1.创建(touch)一个 .proto 文件 ,然后 protobuf 代码就全部写在这个 .ptoto 文件里面

$ touch  Test.proto

例如代码:

syntax = "proto3";

message Test {

string sessionId = 1;

string name = 2;

string authorites = 3;

}

2.cd到你的 .proto 文件位置, 然后 使用终端 编译成 swift文件

$ protoc  Test.proto --swift_out="./"

3. 生成的 swift文件就可以直接拖进你的项目工程中了

第六步、祝你好运。






对于大部分需要后台支持的 App 来说,转化和存储数据是非常重要的工作。和 web service 交互时,程序员常常需要发送或接收 JSON/XML 数据,创建数据结构并传递它们。

尽管已经有许多序列化/反序列化框架和 APIs,但这里有一个维护性的问题,比如版本管理以及当后台模型改变后需要对对象解析器进行修改。

如果你想创建全新的、健壮的后端和前端服务,请尝试 protocol buffers,它是一个和语言无关的序列化结构数据的方法,由 Google 所开发。许多时候,它都要比别的常用方法比如 JSON、XML 要更灵活高效。

它有一个重要的特点,只需要定义一次数据结构,编译器就能够生成多种语言的代码——包括Swift! 它所产生的类文件能够毫不费力地对对象进行读和谐。

在本教程中,你将启动一个Python服务器,并集成一个已有iOSAPP 中的数据。然后,我们将介绍如何使用 protocol buffer,如何配置环境,如何用 protocol buffers 传递数据。

你是不是仍然觉得没有必要使用 protocol buffers?请看下面。

本教程假设你具备一定的 iOS 和 Swift 基本技能,了解基本的服务端编程,知道怎么使用终端。

同时,确认你使用的是最新版的 Xcode 8.2。

开始

RWCards 是一个 app,允许你查看自己的门票以及活动发言者清单。

首先下载开始项目,打开目录下面的 Starter。

请先熟悉一下这三部分内容:

客户端

在 Starter/RWCards 目录,打开 RWCards.xcworkspace 浏览一下主要的项目文件,包括:

SpeakersListViewController.swift 负责列出发言者名单。这个控制器是一个模板,因为我们还没有创建模型对象。

SpeakersViewModel.swift 充当 SpeakersListViewController 的数据源。它包含了发言者的列表。

CardViewController.swift 负责显示一个出席者的图标以及他们的社交信息。

RWService.swift 负责集成客户端和后台。我们将使用 Alamofire 来调用后台服务。

Main.storyboard 包含了整个 App 用到的所有 scene。

这个项目通过 CocoaPods 集成了这两个框架:

Swift Protobuf 允许你在你的 Xcode 项目中使用 protocol buffers。

Almofire 是一个 HTTP 网络库,我们用它来访问服务器。

注意:本教程中我们将使用 Swift Protobuf 0.9.24 以及 Google 的 Protoc Compiler 3.1.0。它们都已经内置在开始项目中了,因此你不需要做什么。

如何使用 Protocol Buffers?

要使用 protocol buffers,首先必须定义一个 .proto 文件。在这个文件中,你需要定义一个消息类型,用于定义你的架构或数据结构。这是一个 .proto 文件的例子:

syntax ="proto3";message Contact {  enum ContactType {    SPEAKER =0;    ATTENDANT =1;    VOLUNTEER =2;  }stringfirst_name =1;stringlast_name =2;stringtwitter_name =3;stringemail =4;stringgithub_link =5;  ContactTypetype=6;stringimageName =7;};

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

这里指定了一个 Contact 消息以及它的属性。

定义好 .proto 文件后,你只需要将文件传递给 protocol buffer 编译器,它将根据你选择的语言生成数据访问类(在 Swift 中是结构)。然后你就可以通过这个类/结构了。简单吧!

https://koenig-media.raywenderlich.com/uploads/2016/12/pb3.png’ width=’600’/>

编译器对消息进行翻译,将它的值类型映射为指定的语言,并生成相应的模型对象文件。后面会详细介绍如何定义消息。

在准备使用 Protocol buffers 之前,你首先应当明白为什么要在你的项目中使用它。

好处

JSON 和 XML 是程序员存储和数据转换的标准方式,但 protocol buffers 具有这些优势:

更快,更小:根据 Google 所说,protocol buffers 比起 XML 数据量小 3-10 倍,速度快 20-100 倍。请阅读 Damien Bod 的这篇文章,其中对不同主流数据格式的读写时间进行了比较。

类型安全:protocol buffers 和 Swift 一样是类型安全的。通过 protocol buffer 语言,你必须为每个属性指定属性。

自动反序列化:你不再需要编写千篇一律的解析代码。只需要修改你的 proto 文件,重新生成数据访问类。

面向分享:可以通过指定的语言,跨平台分享模型,这意味着进行跨平台时能够减少工作。

局限

Protocol buffers 有优点,也有缺点:

时间和工作量:将已有项目中切换到 protocol buffers 上时,可能会增加一些成本,因为这种转换需要成本。此外,它需要学习一种新语法。

人类不可读:XML 和 JSON 更加具有描述性,更容易阅读。Protocol buffer 的原始格式不是自描述的。如果没有 .proto 文件,你根本无法看懂数据。

它不是全能的:如果我们想使用样式表(比如 XSLT),那么最好用 XML。protocol buffers 并不适合于这种目的。

语言不支持:编译器可能不支持你想使用的那种语言。

它并不是什么情况下都能够使用,因此对于 protocol buffers 仍然有很多争论!

编译运行 App,看看它是什么样子。

https://koenig-media.raywenderlich.com/uploads/2016/12/pb2-6.gif’ width=’400’/>

不幸的是,你不能看到任何数据,因为数据源还没有准备好。你的任务是调用后台服务,用发言者列表和参与者角标来刷新 UI。首先,我们来看一下开始项目中提供的这两部分。

Protocol Buffer 的 Schema

回到 Finder,进入 Starter/ProtoSchema 目录,你会看到如下文件:

contact.proto 通过 protocol buffer 语法描述了一个 contact 的结构。后面会详细介绍。

protoScript.sh 是一个 bash 脚本,将通过 protocol buffer 编译器生成 contact.proto 所定义的 Swift 结构和 Python 类。

后台

在 folder Starter/Server 下面有如下文件:

RWServer.py 是一个 Python 服务器,基于 Flask 构建。它有两个 GET 请求:

/currentUser 返回当前参会者的信息。

/speakers 返回发言者的列表。

RWDict.py 包含了 RWServer 会读取的一个发言者的 dictionary。

然后是配置 protocol buffer 运行环境。在后面,你将安装 Google protocol buffer 编译器的运行环境、Swift Protobuf 插件、以及运行 Python 服务器所需要的 Flask。

安装环境

要使用 protocol buffers,我们需要安装一些工具和库。开始项目中已经包含了一个 protoInstallation.sh 脚本,它会为你完成所有的工作。而且更好的是,它还会在安装某个库之前检查其它库是否安装了。

这个脚本的执行会花一点时间,尤其是安装 Google 的 protocol buffer 库的时候。打开你的终端,切到开始项目目录,执行命令:

$ ./protoInstallation.sh

注意:执行这个脚本,可能需要你输入管理员密码。

当脚本执行完成,你可以再次执行它,并确认你看到如下输出:

https://koenig-media.raywenderlich.com/uploads/2016/12/Screen-Shot-2016-12-10-at-2.51.38-PM.png’ width=’550’/>

如果你看到上图的样子,说明脚本执行成功。如果脚本没有执行成功,可能你的管理员密码输错了。如果是这样,请重新运行脚本,对于已经执行成功的内容,它不会重复执行的。

这段脚本进行了如下工作:

安装 Flask,以便能够在本机运行 Python 服务器。

从 Starter/protobuf-3.1.0 目录编译 Google protocol buffer 编译器。

安装 Python 的 protocol buffer 模块,以便服务器能够调用 protobuf 库。

将 Swift Protobuf 插件拷贝到 /usr/local/bin。这使得 Protobuf 编译器能够生成 Swift 结构。

注意:要具体了解这个脚本是什么意思,请用文本编辑器打开 protoInstallation.sh 查看详细命令。这需要你具备一定的 bash 知识。

万事俱备,让我们开始使用 protocol buffers 吧!

定义一个 .proto 文件

.proto 文件用于定义 protocol buffer 消息,所谓消息描述了数据的结构。将它传递给 protocol buffer 编译器之后,就会生成数据访问器结构。

注意:在本教程中,我们将使用 proto3,这是最新的 protocol buffer 语言版本。要深入了解这种语法以及如何定义 proto3 文件,请看 Google 的官方指南

用任意文本编辑器打开 ProtoSchema/contact.proto。这里,我们使用现成的 .proto 文件,它已经为我们定义好了 Contact 消息和 Speakers 消息:

syntax ="proto3";message Contact {// 1enumContactType {// 2SPEAKER =0;    ATTENDANT =1;    VOLUNTEER =2;  }stringfirst_name =1;//3stringlast_name =2;stringtwitter_name =3;stringemail =4;stringgithub_link =5;  ContactType type =6;stringimageName =7;};message Speakers {// 4repeated Contact contacts =1;};

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

这个定义包含了如下内容:

Contact 模型描述了某个人的联系信息。在 App 中,这些信息会显示在这个人的徽章下面。

每个 Contact 都有一个类别,以便区分这个人是发言者还是听众。

proto 文件中的 message 和 enum 定义中的每个字段都必须赋一个递增的、唯一的 tag 值。这个 tag 值会被 message 二进制格式中用作唯一标识,以便保持它们的顺序。更多关于 tag 值和字段管理的内容,请参考 google 文档中的reserved fields

Speakers 模型是一个包含了 Contact 对象的集合。repeated 表示这是一个对象的数组。

生成 Swift 结构

当我们把 contact.proto 传递给 protoc 程序时,proto 文件就生成了 Swift 结构。这些结构将继承 ProtobufMessage 协议。protoc 会为每个 Swift 字段、初始化方法、以及序列化/反序列化方法提供属性。

注意:关于 Swfit protobuf API 的更多内容,请参考Apple 的 Protobuf API 文档

打开终端,切到 Starter/ProtoSchema 目录。在文本编辑器中打开 protoScript.sh :

#!/bin/bashecho'Running ProtoBuf Compiler to convert .proto schema to Swift'protoc --swift_out=. contact.proto //1echo'Running Protobuf Compiler to convert .proto schema to Python'protoc -I=. --python_out=. ./contact.proto //2

1

2

3

4

5

1

2

3

4

5

这个脚本会调用 protoc 两次——一次生成了 Swift 源文件,一次生成了 Python 文件。

回到终端,执行这个脚本:

$ ./protoScript.sh

你会看到:

Running ProtoBuf Compilertoconvert.proto schematoSwiftprotoc-gen-swift: Generating Swiftforcontact.protoRunning Protobuf Compilertoconvert.proto schematoPython

1

2

3

1

2

3

这样我们就根据 contact.proto 文件生成了 Swift/Python 源文件。

在 ProtoSchema 目录,你会看到两个文件,一个 Swift 的,一个是 Python 的。注意每个新生成的文件都会以 .pb.swift 或 .ph.py 文件为后缀名。pb 后缀表示它是 protocol buffer 生成的。

https://koenig-media.raywenderlich.com/uploads/2016/12/generatedFiles-650x71.png’ width=’700’/>

将 contact.pb.swift 拖到 Xcode 的项目导航器中,并放到 Protocol Buffer Objects 文件夹下。注意勾选 “Copy items if needed” 选项。通过 Finder 或终端,将 contact_pb2.py 拷贝到 Starter/Server 文件夹。

大概看一下 contact.pb.swift 和 contact_pb2.py 的内容,看看 proto 的消息是如何映射成两种语言的结构的。

我们已经有了模型对象,是时候来使用他了!

运行本地服务器

示例项目包含了一个内置的 Python 服务器。这个服务器提供两个 GET 接口:一个返回听众的徽章信息,一个返回发言者列表。

本教程不涉及服务端代码。但是需要注意 contact_pb2.py 模型文件。另外如果你有兴趣,可以看一下 RWServer.py。当然对于本教程而言,这不是必须的。

要开启服务器,请打开终端,切换到 Starter/Server 目录。 执行命令:

$ python RWServer.py

你会看到:

https://koenig-media.raywenderlich.com/uploads/2016/12/startServer.png’ width=’600’/>

测试 GET 请求

用浏览器进行 HTTP 请求,就可以看见 protocol buffer 的原始数据格式。

访问http://127.0.0.1:5000/currentUser,你会看到:

https://koenig-media.raywenderlich.com/uploads/2016/12/Screen-Shot-2016-12-10-at-11.41.52-AM-650x61.png’ width=’600’/>

试一下另外一个接口,http://127.0.0.1:5000/speakers:

https://koenig-media.raywenderlich.com/uploads/2016/12/Screen-Shot-2016-12-19-at-10.14.00-PM-650x70.png’ width=’600’/>

注意:你可以保持本地服务器的运行,也可以停止服务器,然后在需要测试RWCards App 时再启动它。

我们启动了一个简单服务器,这个服务器使用了 proto 文件中的消息作为我们的模型。太好了!

调用服务

让本地服务器启动并保持运行,让我们在 App 中调用它的服务。在 RWService.swift 中,将 RWService 类替换为:

class RWService {staticletshared = RWService()// 1leturl ="http://127.0.0.1:5000"privateinit() { }  func getCurrentUser(_ completion: @escaping (Contact?) -> ()) {// 2letpath ="/currentUser"Alamofire.request("\(url)\(path)").responseData { responseinifletdata = response.result.value{// 3letcontact =try? Contact(protobuf: data)// 4completion(contact)      }      completion(nil)    }  }}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

这个类负责和我们的Python 服务器进行通讯。我们用它实现了 currentUser 接口调用。代码说明如下:

shared 是一个单实例,用于访问网络接口。

getCurrentUser(_:) 方法用于请求 /currentUser 接口,以获取当前用户数据。这个用户在后台是以硬编码的形式定义的。

通过 if let 语句对响应值进行解包。

data 对象中是 protocol buffer 的二进制形式。Contact 构造函数把它作为参数,然后对收到的消息进行解码。

将 protocol buffer 转换成对象非常简单,只需调用这个对象的构造函数并传入 data。不需要你手动解析数据。Swift Protobuf 库自动为你完成一切。

获得接口数据之后,我们要把它显示出来。

显示听众徽章

打开 CardViewController.swift,在 viewWillAppear(_:) 方法后面添加方法:

func fetchCurrentUser() { //1RWService.shared.getCurrentUser{ contactinif let contact = contact {      self.configure(contact)    }  }}func configure(_ contact: Contact) { //2self.attendeeNameLabel.attributedText= NSAttributedString.attributedString(for: contact.firstName,and: contact.lastName)  self.twitterLabel.text= contact.twitterNameself.emailLabel.text= contact.emailself.githubLabel.text= contact.githubLinkself.profileImageView.image= UIImage(named: contact.imageName)}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

这两个方法用于从服务器抓取数据并显示用户徽章。代码解释如下:

fetchCurrentUser() 负责请求服务器,抓取当前用户信息,用 contact 对象刷新 CardViewController。

configure(_:) 需要一个 Contact 参数,并对控制器中的 UI 控件进行赋值。

稍后我们再调用这两个方法,现在我们需要从 ContactType 枚举派生出一个可读的听众类型。

自定义 Protocol Buffer 类

我们需要用一个方法将枚举类型转换成字符串类型,这样发言者的徽章会显示成 SPEAKER 而不是 0。

这里有一个问题。因为每当修改消息之后都需要重新生成 .proto 文件,那么我们如何在模型中加入自己的方法呢?

Swift 的扩展能够解决这个问题。通过扩展,我们可以向某个类中添加方法,而不需要修改它用来的代码。

创建一个 contact+extension.swift 文件,将它加到 Protocol Buffer Objects 文件夹中。这个文件的内容编辑如下:

extension Contact {  func contactTypeToString()->String {switchtype {case.speaker:return"SPEAKER"case.attendant:return"ATTENDEE"case.volunteer:return"VOLUNTEER"default:return"UNKNOWN"}  }}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

1

2

3

4

5

6

7

8

9

10

11

12

13

14

contactTypeToString() 方法将 ContactType 类型转换为可显示的字符串。

打开 CardViewController.swift,在 configure(_:) 中加入:

self.attendeeTypeLabel.text= contact.contactTypeToString()

1

1

这句代码将 attendeeTypeLabel 的文本显示为由 contact 类型转换来的字符串表示。

最后,在 ViewWillAppear 方法的 applyBusinessCardAppearance() 一句后面加入:

ifisCurrentUser {  fetchCurrentUser()}else{// TODO: handle speaker}

1

2

3

4

5

1

2

3

4

5

isCurrentUser 当前被硬编码为 true,当我们需要支持发言者类型时我们再来修改它。当 isCurrentUser 为 true 时,调用 fetchCurrentUser(),这会抓取当前用户信息并显示到卡片中。

运行程序,查看听众的徽章。

https://koenig-media.raywenderlich.com/uploads/2016/12/Simulator-Screen-Shot-Dec-10-2016-11.00.10-AM.png’ width=’160’/>

显示发言者列表

在 My Badge 页完成之后,我们需要来完成 Spearkers 页面。

打开 RWService.swift,添加方法:

func getSpeakers(_ completion:@escaping(Speakers?) -> ()){ // 1letpath= "/speakers"Alamofire.request("\(url)\(path)").responseData{responseinifletdata=response.result.value{ // 2letspeakers=try?Speakers(protobuf: data)// 3completion(speakers)}  }completion(nil)}

1

2

3

4

5

6

7

8

9

10

1

2

3

4

5

6

7

8

9

10

看起来很熟悉吧?这和 getCurrentUser(_:) 其实是一样的,不过它获取的是发言者数据。发言者是一个 Contact 对象数组,表示所有会议发言者。

打开 SpeakersViewModel.swift ,替换文件内容为:

classSpeakersViewModel{varspeakers: Speakers!varselectedSpeaker: Contact?  init(speakers: Speakers) {    self.speakers = speakers  }  func numberOfRows()->Int {returnspeakers.contacts.count  }  func numberOfSections()->Int {return1}  func getSpeaker(forindexPath: IndexPath)->Contact {returnspeakers.contacts[indexPath.item]  }  func selectSpeaker(forindexPath: IndexPath) {    selectedSpeaker = getSpeaker(for: indexPath)  }}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

这个类用作 SpeakersListViewController 的数据源,用于显示一个会议发言者的列表。speakers 是一个 Contacts 数组,用 /speakers 接口返回的数据填充。这个数据源为表格的每一行提供一个 Contact 对象。

view model 准备好之后,我们就可以来配置单元格了。打开 SpeakerCell.swift 添加方法:

func configure(with contact: Contact) {  profileImageView.image= UIImage(named: contact.imageName)  nameLabel.attributedText= NSAttributedString.attributedString(for: contact.firstName,and: contact.lastName)}

1

2

3

4

1

2

3

4

这个方法使用一个 Contact 参数,用它来对 cell 的 UIImage 和 UILabel 进行赋值。每个 cell 会包含一张发言者的图片,以及姓名。

然后,打开 SpeakersListViewController.swift,在 viewWillAppear(_:) 的父类方法调用之后添加:

RWService.shared.getSpeakers{ [unowned self] speakersinif let speakers = speakers {    self.speakersModel= SpeakersViewModel(speakers: speakers)    self.tableView.reloadData()  }}

1

2

3

4

5

6

1

2

3

4

5

6

getSpeakers(_:) 方法负责请求并返回一个发言者列表。然后用返回的发言者列表,初始化 SpeakersViewModel 对象。然后用抓取到的数据刷新表格。

然后需要为表格中的每一行分配一个发言者以便显示。将 tableView(_:cellForRowAt:) 方法代码替换为:

letcell = tableView.dequeueReusableCell(withIdentifier:"SpeakerCell",for: indexPath)as! SpeakerCellifletspeaker = speakersModel?.getSpeaker(for: indexPath) {  cell.configure(with: speaker)}returncell

1

2

3

4

5

1

2

3

4

5

getSpeaker(for:) 方法返回指定 indexPath 所对应的 contact 对象。configure(with:) 方法是 SpeakCell 中定义的,作用是设置单元格的发言者图片和姓名。

当发言者列表中的 cell 被点击,我们要用 CardViewController 显示所选发言者信息。打开CardViewController.swift ,新增如下属性:

var speaker: Contact?

我们最终会将所选的发言者传递个这个属性。然后,我们需要显示发言者。将 // TODO: handle speaker 一行替换为:

ifletspeaker = speaker {  configure(speaker)}

1

2

3

1

2

3

这里检查了 speaker 是否不为空,如果不为空,调用 configure() 方法,这个方法将用指定发言者信息刷新卡片。

回到 SpeakersListViewController.swift,传入选定的发言者。首先在 tableView(_:didSelectRowAt:) 方法中,在 performSegue(withIdentifier:sender:) 之前加入:

speakersModel?.selectSpeaker(for: indexPath)

这句会在 speakersModel 中记录用户所选择的发言者。

然后,在 prepare(for:sender:) 方法中,在 vc.isCurrentUser = false 一句后面添加:

vc.speaker = speakersModel?.selectedSpeaker

这句将 selectedSpeaker 传递给 CardViewController ,以便显示它。

看一下你的本地服务器是否仍然运行,然后编译运行 Xcode。现在,你会发现 App 已经能够显示用户徽章和发言者列表了。

https://koenig-media.raywenderlich.com/uploads/2016/12/pb4-1.gif” width=”300”/>

我们用一个 Python 服务器和一个 Swift 客户端打造了一个端到端应用。它们共用由同一个 proto 文件所生成的模型。如果你想修改模型,只需要运行一下编译器再次生成模型即可,这样你就可以同时在服务端和客户端使用了!

结束

你可以在这里下载最终完成项目。

在本教程中,我们学习了基本的 protocol buffers 用法,如何定义 .proto 文件、用编译器生成 Swift 代码。我们也学习了如何启动一个简单的 Flask 本地服务器,通过这个服务器我们创建了一个服务用于发送 protocol buffer 二进制到客户端,用 protocol buffers 解析这些数据是非常简单的!

关于 protocol buffers 还有很多内容,比如定义消息映射以及向后兼容。如果你对这些感兴趣,请参考Google 文档

还有一件有意思的事情就是 protocol buffers 可以用在远程过程调用。请参考GRPC

有任何问题和建议,请在下面留言。

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

推荐阅读更多精彩内容