Java程序員必須知道的常用序列化技術及選型,Protobuf 原理詳解

基于 socket 進行對象傳輸
先舉個簡單的例子,基于我們前面幾次課程的只是,寫一個 socket 通信的代碼

User.java

public class User {
  private String name;
  public String getName() {
  return name;
}
public void setName(String name) {
  this.name = name;
}
}

SocketServerProvider.java

public class SocketServerProvider {
    public static void main(String[] args) throws
            IOException {
        ServerSocket serverSocket = null;
        BufferedReader in = null;
        try {
            serverSocket = new ServerSocket(8080);
            Socket socket = serverSocket.accept();
            ObjectInputStream objectInputStream =
                    new ObjectInputStream(socket.getInputStream());
            User user=(User)objectInputStream.readObject();
            System.out.println(user);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (serverSocket != null) {
                serverSocket.close();
            }
        }
    }
}

SocketClientConsumer.java

public class SocketClientConsumer {
    public static void main(String[] args) {
        Socket socket = null;
        ObjectOutputStream out = null;
        try {
            socket = new Socket("127.0.0.1", 8080);
            User user = new User();
            out = new
                    ObjectOutputStream(socket.getOutputStream());
            out.writeObject(user);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

運行結果

這段代碼運行以后瘸爽,能夠實現(xiàn) Java 對象的正常傳輸嗎? 很顯然铅忿,會報錯


如何解決報錯的問題呢剪决?
對 User 這個對象實現(xiàn)一個 Serializable 接口,再次運行就可以看到對象能夠正常傳輸了

public class User implements Serializable {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

了解序列化的意義

我們發(fā)現(xiàn)對 User 這個類增加一個 Serializable檀训,就可以解決 Java 對象的網絡傳輸問題柑潦。這就是今天想給大家講解的序列化這塊的意義。

Java 平臺允許我們在內存中創(chuàng)建可復用的 Java 對象峻凫,但一般情況下渗鬼,只有當 JVM 處于運行時,這些對象才可能存在荧琼,即譬胎,這些對象的生命周期不會比 JVM 的生命周期更長。但在現(xiàn)實應用中命锄,就可能要求在 JVM 停止運行之后能夠保存(持久化)指定的對象堰乔,并在將來重新讀取被保存的對象。 Java 對象序列化就能夠幫助我們實現(xiàn)該功能脐恩。

簡單來說

序列化是把對象的狀態(tài)信息轉化為可存儲或傳輸的形式過程镐侯,也就是把對象轉化為字節(jié)序列的過程稱為對象的序列化。

反序列化是序列化的逆向過程驶冒,把字節(jié)數組反序列化為對象苟翻,把字節(jié)序列恢復為對象的過程成為對象的反序列化

序列化的高階認識

簡單認識一下 Java 原生序列化

前面的代碼中演示了,如何通過 JDK 提供了 Java 對象的序列化方式實現(xiàn)對象序列化傳輸骗污,主要通過輸出流java.io.ObjectOutputStream和對象輸入流java.io.ObjectInputStream來實現(xiàn)袜瞬。

java.io.ObjectOutputStream:表示對象輸出流 , 它的 writeObject(Object obj)方法可以對參數指定的 obj 對象進行序列化,把得到的字節(jié)序列寫到一個目標輸出流中身堡。

java.io.ObjectInputStream:表示對象輸入流 ,它的 readObject()方法源輸入流中讀取字節(jié)序列,再把它們反序列化成為一個對象拍鲤,并將其返回贴谎。

需要注意的是,被序列化的對象需要實現(xiàn) java.io.Serializable 接口

  • serialVersionUID 的作用
    在 IDEA 中通過如下設置可以生成 serializeid


字面意思上是序列化的版本號季稳,凡是實現(xiàn) Serializable 接口的類都有一個表示序列化版本標識
符的靜態(tài)變量

演示步驟

  1. 先將 user 對象序列化到文件中
  2. 然后修改 user 對象擅这,增加 serialVersionUID 字段
  3. 然后通過反序列化來把對象提取出來
  4. 演示預期結果:提示無法反序列化

結論
Java 的序列化機制是通過判斷類的 serialVersionUID 來驗證版本一致性的。在進行反序列化時景鼠, JVM 會把傳來的字節(jié)流中的 serialVersionUID 與本地相應實體類的 serialVersionUID 進行比較仲翎,如果相同就認為是一致的痹扇,可以進行反序列化,否則就會出現(xiàn)序列化版本不一致的異常溯香,即是 InvalidCastException鲫构。

從結果可以看出,文件流中的 class 和 classpath 中的 class玫坛,也就是修改過后的 class结笨,不兼容了,處于安全機制考慮湿镀,程序拋出了錯誤炕吸,并且拒絕載入。從錯誤結果來看勉痴,如果沒有為指定的 class 配置 serialVersionUID赫模,那么 java 編譯器會自動給這個 class 進行一個摘要算法,類似于指紋算法蒸矛,只要這個文件有任何改動瀑罗,得到的 UID 就會截然不同的,可以保證在這么多類中莉钙,這個編號是唯一的廓脆。所以,由于沒有顯指定 serialVersionUID磁玉,編譯器又為我們生成了一個 UID停忿,當然和前面保存在文件中的那個不會一樣了,于是就出現(xiàn)了 2 個序列化版本號不一致的錯誤蚊伞。因此席赂,只要我們自己指定了 serialVersionUID,就可以在序列化后时迫,去添加一個字段颅停,或者方法,而不會影響到后期的還原掠拳,還原后的對象照樣可以使用癞揉,而且還多了方法或者屬性可以用。

tips: serialVersionUID 有兩種顯示的生成方式:
一是默認的 1L溺欧,比如: private static final long serialVersionUID = 1L;
二是根據類名喊熟、接口名、成員方法及屬性等來生成一個 64 位的哈希字段,當實現(xiàn) java.io.Serializable 接口的類沒有顯式地定義一個 serialVersionUID 變量時候姐刁, Java 序列化機制會根據編譯的 Class 自動生成一個 serialVersionUID 作序列化版本比較用芥牌,這種情況下,如果 Class 文件(類名聂使,方法明等)沒有發(fā)生變化(增加空格壁拉,換行谬俄,增加注釋等等),就算再編譯多次弃理, serialVersionUID 也不會變化的溃论。

  • Transient 關鍵字
    Transient 關鍵字的作用是控制變量的序列化,在變量聲明前加上該關鍵字案铺,可以阻止該變量被序列化到文件中蔬芥,在被反序列化后, transient 變量的值被設為初始值控汉,如 int 型的是0笔诵,對象型的是 null。

  • writeObject 和 readObject 原理
    writeObject 和 readObject 是兩個私有的方法姑子,他們是什么時候被調用的呢乎婿?從運行結果來看,它確實被調用街佑。而且他們并不存在于 Java.lang.Object谢翎,也沒有在 Serializable 中去聲明。
    我們唯一的猜想應該還是和 ObjectInputStream 和 ObjectOutputStream 有關系沐旨,所以基于這個入口去看看在哪個地方有調用



    從源碼層面來分析可以看到森逮, readObject 是通過反射來調用的。
    其實我們可以在很多地方看到 readObject 和 writeObject 的使用磁携,比如 HashMap褒侧。

Java 序列化的一些簡單總結

  1. Java 序列化只是針對對象的狀態(tài)進行保存,至于對象中的方法谊迄,序列化不關心
  2. 當一個父類實現(xiàn)了序列化闷供,那么子類會自動實現(xiàn)序列化,不需要顯示實現(xiàn)序列化接口
  3. 當一個對象的實例變量引用了其他對象统诺,序列化這個對象的時候會自動把引用的對象也進
    行序列化(實現(xiàn)深度克峦嵩唷)
  4. 當某個字段被申明為 transient 后,默認的序列化機制會忽略這個字段
  5. 被申明為 transient 的字段粮呢,如果需要序列化婿失,可以添加兩個私有方法: writeObject 和
    readObject

分布式架構下常見序列化技術

初步了解了 Java 序列化的知識以后,我們又得回到分布式架構中啄寡,了解序列化的發(fā)展過程豪硅。

隨著分布式架構、微服務架構的普及这难。服務與服務之間的通信成了最基本的需求。這個時候葡秒,我們不僅需要考慮通信的性能姻乓,也需要考慮到語言多元化問題嵌溢。所以,對于序列化來說蹋岩,如何去提升序列化性能以及解決跨語言問題赖草,就成了一個重點考慮的問題。

由于 Java 本身提供的序列化機制存在兩個問題

  1. 序列化的數據比較大剪个,傳輸效率低
  2. 其他語言無法識別和對接

以至于在后來的很長一段時間秧骑,基于 XML 格式編碼的對象序列化機制成為了主流,一方面解決了多語言兼容問題扣囊,另一方面比二進制的序列化方式更容易理解乎折。以至于基于 XML的 SOAP協(xié)議及對應的 WebService 框架在很長一段時間內成為各個主流開發(fā)語言的必備的技術。

再到后來侵歇,基于 JSON 的簡單文本格式編碼的 HTTP REST 接口又基本上取代了復雜的 Web Service 接口骂澄,成為分布式架構中遠程通信的首要選擇。但是 JSON 序列化存儲占用的空間大惕虑、性能低等問題坟冲,同時移動客戶端應用需要更高效的傳輸數據來提升用戶體驗。在這種情況下與語言無關并且高效的二進制編碼協(xié)議就成為了大家追求的熱點技術之一溃蔫。首先誕生的一個開源的二進制序列化框架-MessagePack健提。它比 google 的 Protocol Buffers 出現(xiàn)得還要早。

簡單了解各種序列化技術

  • XML 序列化框架介紹
    XML 序列化的好處在于可讀性好伟叛,方便閱讀和調試私痹。但是序列化以后的字節(jié)碼文件比較大,而且效率不高痪伦,適用于對性能不高侄榴,而且 QPS 較低的企業(yè)級內部系統(tǒng)之間的數據交換的場景,同時 XML 又具有語言無關性网沾,所以還可以用于異構系統(tǒng)之間的數據交換和協(xié)議癞蚕。比如我們熟知的Webservice,就是采用 XML 格式對數據進行序列化的辉哥。 XML 序列化/反序列化的實現(xiàn)方式有很多桦山,熟知的方式有 XStream 和 Java 自帶的 XML 序列化和反序列化兩種。

  • JSON 序列化框架
    JSON(JavaScript Object Notation)是一種輕量級的數據交換格式醋旦,相對于 XML 來說恒水, JSON的字節(jié)流更小,而且可讀性也非常好∷瞧耄現(xiàn)在 JSON 數據格式在企業(yè)運用是最普遍的钉凌。
    JSON 序列化常用的開源工具有很多:

  1. Jackson (https://github.com/FasterXML/jackson
  2. 阿里開源的 FastJson (https://github.com/alibaba/fastjon
  3. Google 的 GSON (https://github.com/google/gson)

這幾種 json 序列化工具中, Jackson 與 fastjson 要比 GSON 的性能要好捂人,但是 Jackson御雕、GSON 的穩(wěn)定性要比 Fastjson 好矢沿。而 fastjson 的優(yōu)勢在于提供的 api 非常容易使用。

  • Hessian 序列化框架
    Hessian 是一個支持跨語言傳輸的二進制序列化協(xié)議酸纲,相對于 Java 默認的序列化機制來說捣鲸,Hessian 具有更好的性能和易用性,而且支持多種不同的語言闽坡。
    實際上 Dubbo 采用的就是 Hessian 序列化來實現(xiàn)栽惶,只不過 Dubbo 對 Hessian 進行了重構,性能更高疾嗅。

  • Avro 序列化
    Avro 是一個數據序列化系統(tǒng)外厂,設計用于支持大批量數據交換的應用。它的主要特點有:支持二進制序列化方式宪迟,可以便捷酣衷,快速地處理大量數據;動態(tài)語言友好次泽, Avro 提供的機制使動態(tài)語言可以方便地處理 Avro 數據穿仪。

  • kyro 序列化框架
    Kryo 是一種非常成熟的序列化實現(xiàn),已經在 Hive意荤、 Storm)中使用得比較廣泛啊片,不過它不能跨語言. 目前 dubbo 已經在 2.6 版本支持 kyro 的序列化機制。它的性能要優(yōu)于之前的hessian2

  • Protobuf 序列化框架

Protobuf 是 Google 的一種數據交換格式玖像,它獨立于語言紫谷、獨立于平臺。 Google 提供了多種語言來實現(xiàn)捐寥,比如 Java笤昨、 C、 Go握恳、 Python瞒窒,每一種實現(xiàn)都包含了相應語言的編譯器和庫文件,Protobuf 是一個純粹的表示層協(xié)議乡洼,可以和各種傳輸層協(xié)議一起使用崇裁。

Protobuf 使用比較廣泛,主要是空間開銷小和性能比較好束昵,非常適合用于公司內部對性能要求高的 RPC 調用拔稳。 另外由于解析性能比較高,序列化以后數據量相對較少锹雏,所以也可以應用在對象的持久化場景中巴比。

但是要使用 Protobuf 會相對來說麻煩些,因為他有自己的語法,有自己的編譯器轻绞,如果需要用到的話必須要去投入成本在這個技術的學習中腰耙。

protobuf 有個缺點就是要傳輸的每一個類的結構都要生成對應的 proto 文件,如果某個類發(fā)生修改铲球,還得重新生成該類對應的 proto 文件。

Protobuf 序列化的原理
那么接下來著重分析一下 protobuf 的序列化原理晰赞,前面說過它的優(yōu)勢是空間開銷小稼病,性能也相對較好。它里面用到的一些算法還是值得我們去學習的掖鱼。

protobuf 的基本應用

使用 protobuf 開發(fā)的一般步驟是

  1. 配置開發(fā)環(huán)境然走,安裝 protocol compiler 代碼編譯器
  2. 編寫.proto 文件,定義序列化對象的數據結構
  3. 基于編寫的.proto 文件戏挡,使用 protocol compiler 編譯器生成對應的序列化/反序列化工具類
  4. 基于自動生成的代碼芍瑞,編寫自己的序列化應用

Protobuf 案例演示

下載 protobuf 工具,https://github.com/google/protobuf/releases褐墅, 找到 protoc-3.5.1-win32.zip拆檬。

編寫 proto 文件

syntax="proto2";
package com.gupaoedu.serial;
option java_package =
"com.gupaoedu.serial";
option java_outer_classname="UserProtos";
message User {
  required string name=1;
  required int32 age=2;
}

數據類型:string / bytes / bool / int32(4 個字節(jié))/ int64 / float / double / enum 枚舉類 / message 自定義類
修飾符:required 表示必填字段;optional 表示可選字段妥凳;repeated 可重復竟贯,表示集合。
1逝钥, 2屑那, 3, 4 需要在當前范圍內是唯一的艘款,表示順序持际。

生成實體類
【.\protoc.exe --java_out=./ ./user.proto】

實現(xiàn)序列化

<dependency>
   <groupId>com.google.protobuf</groupId>
   <artifactId>protobuf-java</artifactId>
   <version>RELEASE</version>
 </dependency>
UserProtos.User user=UserProtos.User.newBuilder()
  .setName("Mic")
  .setAge(18).build();
ByteString bytes=user.toByteString();
System.out.println(bytes);
UserProtos.User nUser=UserProtos.User.parseFrom(bytes);
System.out.println(nUser);

protobuf 序列化原理
我們可以把序列化以后的數據打印出來看看結果


我們可以看到,序列化出來的數字基本看不懂哗咆,但是序列化以后的數據確實很小蜘欲,那我們接下來帶大家去了解一下底層的原理。

正常來說岳枷,要達到最小的序列化結果芒填,一定會用到壓縮的技術,而 protobuf 里面用到了兩種壓縮算法空繁,一種是 varint殿衰,另一種是 zigzag。

varint
先說第一種盛泡,我們先來看 age=300 這個數字是如何被壓縮的



這兩個字節(jié)字節(jié)分別的結果是: -84 闷祥、 2
-84 怎么計算來的呢? 我們知道在二進制中表示負數的方法,高位設置為 1凯砍, 并且是對應數字的二進制取反以后再計算補碼表示(補碼是反碼+1)
所以如果要反過來計算

  1. 【補碼】 10101100 -1 得到 10101011
  2. 【反碼】 01010100 得到的結果為 84. 由于高位是 1箱硕,表示負數所以結果為-84

字符如何轉化為編碼
“Mic”這個字符,需要根據 ASCII 對照表轉化為數字悟衩。
M =77剧罩、 i=105、 c=99
所以結果為 77 105 99
大家肯定有個疑問座泳,這里的結果為什么直接就是 ASCII 編碼的值呢惠昔?怎么沒有做壓縮呢?有沒有同學能夠回答出來挑势。
原因是镇防, varint 是對字節(jié)碼做壓縮,但是如果這個數字的二進制只需要一個字節(jié)表示的時候潮饱,其實最終編碼出來的結果是不會變化的来氧。

還有兩個數字, 3 和 16 代表什么呢香拉?那就要了解 protobuf 的存儲格式了

存儲格式
protobuf 采用 T-L-V 作為存儲方式


tag 的計算方式是 field_number(當前字段的編號) << 3 | wire_type
比如 Mic 的字段編號是 1 啦扬,類型 wire_type 的值為 2 所以 : 1 <<3 | 2 =10
age=300 的字段編號是 2,類型 wire_type 的值是 0凫碌, 所以 : 2<<3|0 =16

第一個數字 10考传,代表的是 key,剩下的都是 value证鸥。

負數的存儲
在計算機中僚楞,負數會被表示為很大的整數,因為計算機定義負數符號位為數字的最高位枉层,所以如果采用 varint 編碼表示一個負數泉褐,那么一定需要 5 個比特位。所以在 protobuf 中通過sint32/sint64 類型來表示負數鸟蜡,負數的處理形式是先采用 zigzag 編碼(把符號數轉化為無符號數)膜赃,在采用 varint 編碼。
sint32: (n << 1) ^ (n >> 31)
sint64: (n << 1) ^ (n >> 63)
比如存儲一個(-300)的值
-300
原碼: 0001 0010 1100
取反: 1110 1101 0011
加 1 : 1110 1101 0100
n<<1: 整體左移一位揉忘,右邊補 0 -> 1101 1010 1000
n>>31: 整體右移 31 位跳座,左邊補 1 -> 1111 1111 1111
n<<1 ^ n >>31
1101 1010 1000 ^ 1111 1111 1111 = 0010 0101 0111
十進制: 0010 0101 0111 = 599
varint 算法: 從右往做,選取 7 位泣矛,高位補 1/0(取決于字節(jié)數)
得到兩個字節(jié)
1101 0111 0000 0100
-41 疲眷、 4

總結

Protocol Buffer 的性能好,主要體現(xiàn)在 序列化后的數據體積小 & 序列化速度快您朽,最終使得
傳輸效率高狂丝,其原因如下:
序列化速度快的原因:
a. 編碼 / 解碼 方式簡單(只需要簡單的數學運算 = 位移等等)
b. 采用 Protocol Buffer 自身的框架代碼 和 編譯器 共同完成
序列化后的數據量體積小(即數據壓縮效果好)的原因:
a. 采用了獨特的編碼方式,如 Varint几颜、 Zigzag 編碼方式等等
b. 采用 T - L - V 的數據存儲方式:減少了分隔符的使用 & 數據存儲得緊湊

序列化技術的選型

  • 技術層面
  1. 序列化空間開銷倍试,也就是序列化產生的結果大小,這個影響到傳輸的性能
  2. 序列化過程中消耗的時長蛋哭,序列化消耗時間過長影響到業(yè)務的響應時間
  3. 序列化協(xié)議是否支持跨平臺县习,跨語言。因為現(xiàn)在的架構更加靈活谆趾,如果存在異構系統(tǒng)通信需求准颓,那么這個是必須要考慮的
  4. 可擴展性/兼容性,在實際業(yè)務開發(fā)中棺妓,系統(tǒng)往往需要隨著需求的快速迭代來實現(xiàn)快速更新,這就要求我們采用的序列化協(xié)議基于良好的可擴展性/兼容性炮赦,比如在現(xiàn)有的序列化數據結構中新增一個業(yè)務字段怜跑,不會影響到現(xiàn)有的服務
  5. 技術的流行程度,越流行的技術意味著使用的公司多吠勘,那么很多坑都已經淌過并且得到了解決性芬,技術解決方案也相對成熟
  6. 學習難度和易用性
  • 選型建議
  1. 對性能要求不高的場景,可以采用基于 XML 的 SOAP 協(xié)議剧防;
  2. 對性能和間接性有比較高要求的場景植锉,那么 Hessian、 Protobuf峭拘、 Thrift、 Avro 都可以;
  3. 基于前后端分離庭呜,或者獨立的對外的 api 服務懈贺,選用 JSON 是比較好的,對于調試拣展、可讀性都很不錯彭沼;
  4. Avro 設計理念偏于動態(tài)類型語言,那么這類的場景使用 Avro 是可以的备埃。

各個序列化技術的性能比較
這 個 地 址 有 針 對 不 同 序 列 化 技 術 進 行 性 能 比 較 :https://github.com/eishay/jvmserializers/wiki

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末姓惑,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子按脚,更是在濱河造成了極大的恐慌于毙,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辅搬,死亡現(xiàn)場離奇詭異望众,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門烂翰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來夯缺,“玉大人,你說我怎么就攤上這事甘耿∮欢担” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵佳恬,是天一觀的道長捏境。 經常有香客問我,道長毁葱,這世上最難降的妖魔是什么垫言? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮倾剿,結果婚禮上筷频,老公的妹妹穿的比我還像新娘。我一直安慰自己前痘,他們只是感情好凛捏,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著芹缔,像睡著了一般坯癣。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上最欠,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天示罗,我揣著相機與錄音,去河邊找鬼芝硬。 笑死鹉勒,一個胖子當著我的面吹牛,可吹牛的內容都是我干的吵取。 我是一名探鬼主播禽额,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼皮官!你這毒婦竟也來了脯倒?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤捺氢,失蹤者是張志新(化名)和其女友劉穎藻丢,沒想到半個月后,有當地人在樹林里發(fā)現(xiàn)了一具尸體摄乒,經...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡悠反,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年残黑,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片斋否。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡梨水,死狀恐怖,靈堂內的尸體忽然破棺而出茵臭,到底是詐尸還是另有隱情疫诽,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布旦委,位于F島的核電站奇徒,受9級特大地震影響,放射性物質發(fā)生泄漏缨硝。R本人自食惡果不足惜摩钙,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望查辩。 院中可真熱鬧胖笛,春花似錦、人聲如沸宜肉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谬返。三九已至,卻和暖如春日杈,著一層夾襖步出監(jiān)牢的瞬間遣铝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工莉擒, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留酿炸,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓涨冀,卻偏偏與公主長得像填硕,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子鹿鳖,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345