Protobuf学习记录

简介

protoBuf是google 的一种数据交换的格式,它独立于语言,独立于平台。google 提供了多种语言的实现:java、c#、c++、go 和 python,每一种实现都包含了相应语言的编译器以及库文件。由于它是一种二进制的格式,比使用 xml 进行数据交换快许多。

优点

与同类型的数据交换格式相比(诸如json,xml),由于protobuf是基于二进制数据传输格式,因此它具有高效的解析速度和更小的体积,并且由于它是直接基于.proto文件生成对应语言的数据结构,因此它的转换过程更加简单直接。同时因为protobuf实现都包含了相应语言的编译器以及库文件,它将比传统的基于一套规范解析更加精准可控,误解的概率更低。

综上优点 :

  • 高效的解析速度
  • 小巧的传输体积
  • 直接上手,简单易用
  • 解析可控,误解率低

缺点

与传统的数据交换格式相比,由于基于二进制数据传输格式,protobuf可读性为零。同时虽然protobuf强调的是跨平台性,但相较于json,xml来说,protobuf的语言覆盖率偏低,并且手动开发一个自定义protobuf的工作量也偏大。

综上缺点 :

  • 可读性低
  • 语言支持相对少

用途

作为一种效率和兼容性都很优秀的二进制数据传输格式,可以用于分布式应用之间的数据通信或者异构环境[1],并且在传统的cs架构的环境中,protobuf也可以用作客户端服务端公用的数据交换格式。


简单上手

下面介绍在Windows下使用protobuf进行python与java互通的helloword程序

下载protobuf

由于国内google被墙,只能在github上下载下载地址
在windows下载protoc-XXX-win32.zip解压即可

配置一个.proto

//文件名test.proto
//定义包名
package test;
//protobuf的编译器版本
syntax = "proto3";

//java的包名
option java_package = "test.protobuf";
//java中主类的名称(及public修饰的类的名称)
option java_outer_classname = "MProtobuf";


message request{

    //1为msg在request中的field_num,在后面会讲
    string msg = 1;
    string commet = 2;
}

生成对应的Java和Python文件

将刚才写好的test.proto移到解压后文件的bin目录
并在该目录下打开命令窗口。
输入:

    protoc.exe --java_out=./ test.proto

在当前目录生成java文件

输入:

    protoc.exe --python_out=./ test.proto

在当前目录生成python文件

在当前目录下test文件就是我们生成的java文件,点进去可见到MProtobuf.java
这就是我们在:

    option java_outer_classname = "MProtobuf";

定义的outer_classname

分析生成文件结构

MProtobuf由requestOrBuilder和request构成

  • 我们可以通过request中的newBuilder()构建一个requestOrBuilder
    并通过requestOrBuilder构建一个request
  • 在request中有writeTo方法:
public void writeTo(com.google.protobuf.CodedOutputStream output)
                        throws java.io.IOException {
      if (!getMsgBytes().isEmpty()) {
        com.google.protobuf.GeneratedMessageV3.writeString(output, 1, msg_);
      }
      if (id_ != 0) {
        output.writeInt32(2, id_);
      }
      unknownFields.writeTo(output);
    }

能方便将request写进流里

  • 通过request中的parseFrom方法:
public static test.protobuf.HelloWorldProto.HelloWorld parseFrom(
        com.google.protobuf.CodedInputStream input)
        throws java.io.IOException {
      return com.google.protobuf.GeneratedMessageV3
          .parseWithIOException(PARSER, input);
    }

能方便将流转换成request对象

编写Server.java

  • 首先生成的MProtobuf.java是依赖于com.google.protobuf的,所有先下载它的jar包

编写Server.java:

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

import com.google.protobuf.CodedInputStream;

import test.protobuf.MProtobuf.request;

public class Server {
    public static void main(String[] args) throws IOException {
        
        ServerSocket server = new ServerSocket(12345);
        
        for(;;){
            Socket socket = server.accept();
            
            InputStream ism = socket.getInputStream();
            CodedInputStream csm = CodedInputStream.newInstance(ism);
            
            request req = request.parseFrom(csm);
            
            System.out.println(req);
        }
    }
}

编写client.py

同样在python中依赖protobuf

    pip install protobuf

在生成的test_pb2.py的文件下建立client.py

编写client.py:

import test_pb2 as tp
import socket

socket = socket.socket()

socket.connect(("127.0.0.1",12345))

req = tp.request()
req.msg = "helloworld"
req.commet = "power by protobuf"
socket.sendall(req.SerializeToString())

运行

先运行Java,在运行client.py
控制台输出:

msg: "helloworld"
commet: "power by protobuf"

运行成功

深入

字段修饰符[2]

required
: 对于required的字段而言,字段初值是必须要提供的,否则字段的便是未初始化的
: 对于修饰符为required的字段,序列化的时候必须给予初始化,否则程序运行会异常

optional
: 对于optional的字段而言,如果未进行初始化,那么一个默认值将赋予该字段编号
: 也可以指定默认值,如下示例所示.

    optional string name = 1[default = "ssochi"]

repeated
: 对于repeated的字段而言,该字段可以重复多个,即每个编码单元可能有多个该字段
: 在高级语言里面,我们可以通过数组来实现,而在proto定义文件中可以使用repeated来修饰,从而达到相同目的。当然,出现0次也是包含在内的。

字段类型

.proto Type Java Type notes
int32 int 使用变长编码,在值为负数的情况下效率低,应采用sin32代替
int64 long 使用变长编码,在值为负数的情况下效率低,应采用sin64代替
uint32 int 使用变长编码
uint64 long 使用变长编码
sint32 int 使用变长编码,有符号整型,在值为负数的情况下效率高
sint64 long 使用变长编码,有符号整型,在值为负数的情况下效率高
fixed32 int 固定四个字节.如果值经常大于2^28使用fixed32会比使用uint32更高效
fixed64 long 固定八个字节.如果值经常大于2^56使用fixed64会比使用uint64更高效
sfixed32 int 有符号,固定四个字节
sfixed64 long 有符号,固定八个字节
bool boolean
double double
string String string必须一致包含UTF-8编码或者7-bit ASCII字符串
bytes ByteString 可以包含任意字节序列

字段类型对应二进制类型

字段类型 二进制类型 二进制编码值
int32,int64,uint32,uint64,sint32,sint64,bool,enum Varint(可变长度int) 0
fixed64,sfixed64,double double 1
string,bytes,inner messages(内部嵌套),packaed repeated fields(repeated字段) Length-delimited 2
groups(deprecated) Start group 3
groups(deprecated) Endd group 4
fixed32,sfixed32,float 32bit固定长度 5

field_num

每个消息的字段都有一个唯一的数字标签,这些标签用来表示你的字段在二进制消息(message binary format)中处的位置。并且一旦指定标签号,在使用过程中是不可以更改的,标记这些标签号在1-15的范围内每个字段需要使用1个字节用来编码这一个字节包括字段所在的位置和字段的类型。标签号在16-2047需要使用2个字节来编码。所以你最好将1-15的标签号为频繁使用到的字段所保留。如果将来可能会添加一些频繁使用到的元素,记得留下一些1-15标签号。
最小可指定的标签号为1,最大的标签号为2^29 - 1或者536870911。不能使用19000-19999的标签号这些标签号是为protobuf内部实现所保留的,如果你在.proto文件内使用了这些标签号Protobuf编译器将会报错!

结构

消息

protobuf中使用message定义一个消息,在同一个包内能直接引用,在不同包内则需要先import

message request{
    string msg = 1;
    string commet = 2;
}

枚举

enum PhoneType //枚举消息类型
    {
        MOBILE = 0; //proto3版本中,首成员必须为0,成员不应有相同的值
        HOME = 1;
        WORK = 2;
    }

Map

protobuf v2是不支持Map数据结构的,官方给出的不就方法是通过如下代码代替Map:

message MapFieldEntry {
  key_type key = 1;
  value_type value = 2;
}

repeated MapFieldEntry map_field = N;

官方给出的解释是:The map syntax is equivalent to the following on the wire, so protocol buffers implementations that do not support maps can still handle your data

在protobuf v3 的较新版本已经支持Map[3],可以通过如下代码申明Map

 map<string, string> values= 1;

嵌套

  • message定义中可以嵌套定义message,enum
  • 嵌套定义的单元外部可见,引用路径由外到内逐层引用

扩展字段

protobuf通过extension解决数据结构之间的不能派生的问题,以此来达到减少重复工作量和便于维护代码的目的。

message BaseDataType{
     extensions 100 to max;   //标识此字段可扩展,此处可指定扩展字段的ID有效范围,to max表示字段范围至最大值
     optional string field1 = 1;
     optional string field2 = 2;
}
message ExtendDataType{
     extend BaseDataType{
         optional ExtendDataType extendData = 100;     
         optional int32  extendValue = 101;          
     }
     optional string extendField1 = 1;
     optional string extendField2= 2;
} 

编码

Varint编码规则

Varints是将一个整数序列化为一个或多个Bytes的方法,越小的整数,使用的Bytes越少
: 每个byte最高位(msb)是标志位,0表示是最后一个byte,1表示该字段值还有后续byte
: 每个byte低7位存放数值
: Varints使用Little Endian(小端)字节序

编码试例:

举例: 300 的二进制为 10 0101100

第一位:1(有后续) + 0101100
第二位:0(无后续) + 0000010

最终结果: 101011000000010

可见,由于是小端字节序,值越小Varints序列化后使用的Bytes越少。
然而负数在补码的首位是1,则使用Varint序列化后,所有负数都相当于非常大的数,
这造成了负数序列化后使用的bytes增加,为了解决这个问题对于负数采用sint32或sint64。
而sint先采用Zigzag方法避免上述问题再通过Varint序列化。

Zigzag编码规则

对于sint32采用(n<<1)^(n>>31)
对于sint32采用(n<<1)^(n>>63)

其中>>操作当操作数为负数时高位补1正数时高位补1

Zigzag编码将整数重新映射到定义域,使得整数映射后的补码长度和绝对值的大小成正相关:

原始值 编码后的值 编码后的补码
0 0 00000000
-1 1 00000001
1 2 00000010
-2 3 00000011
3 4 00000100
... ... ...

length-delimited编码

string,bytes都属于length-delimited编码,length-delimited(wire_type=2)的编码方式:key+length+content

  • key的编码方式是统一的
  • length采用varints编码方式
  • content就是由length指定的长度的Bytes

消息编码规则

  • message都是以一组或多组key-value对组成,key和value分别采用不同的编码方式
  • 序列化时,将message中所有key-value序列化成二进制字节流。反序列化时,解析出所有key-value对,
    如果遇到无法识别的类型,则直接跳过。这种机制保证了旧有的编/解码在协议添加新的字段时,依旧可以正常工作
  • key由两部分组成,一部分是在定义消息时对字段的编号(field_num[4]),另一部分是字段类型(wire_type,编号最大不超过536870911.
  • key编码方式filed_num<<3|wire_type,编码后的二进制长度是变长的
  • value编码则根据字段类型进行编码.

生成代码分析

接下来我将通过protoc.exe生成的java文件,分析protobuf的编解码过程
首先我们先创建一个包含protobuf所有类型的.proto

//文件名protobufStruct.proto

//protobuf 版本
syntax = "proto3";

//java的包名
option java_package = "struct.protobuf";
//java中主类的名称(及public修饰的类的名称)
option java_outer_classname = "structs";

//基本类型
message baseStruct{

    int32 int32value = 1;
    int64 int64value = 2; 
    uint32 uint32value = 3;
    uint64 uint64value = 4;
    sint32 sint32value = 5;
    sint64 sint64value = 6;
    fixed32 fixed32value = 7;
    fixed64 fixed64value = 8;
    sfixed32 sfixed32value = 9;
    sfixed64 sfixed64value = 10;
    bool boolvalue = 11;
    double doublevalue = 12;
    string stringvalue = 13;
    bytes bytesvalue = 14;
    //枚举
    enum PhoneType{ 
        MOBILE = 0; 
        HOME = 1;
        WORK = 2;
    }

}
//复合类型
message complexStruct{
    //map
    map<int32, string> mapvalues= 1;
    //list
    repeated MapFieldEntry map_field = 2;
}
message MapFieldEntry {
  int32 key = 1;
  string value = 2;
}

通过执行cmd指令生成java文件

    protoc.exe --java_out=./ protobufStruct.proto

我们可以看到baseStructOrBuilder:

 public interface baseStructOrBuilder extends
      // @@protoc_insertion_point(interface_extends:baseStruct)
      com.google.protobuf.MessageOrBuilder {

    /**
     * <code>int32 int32value = 1;</code>
     */
    int getInt32Value();

    /**
     * <code>int64 int64value = 2;</code>
     */
    long getInt64Value();

    /**
     * <code>uint32 uint32value = 3;</code>
     */
    int getUint32Value();

    /**
     * <code>uint64 uint64value = 4;</code>
     */
    long getUint64Value();

    /**
     * <code>sint32 sint32value = 5;</code>
     */
    int getSint32Value();

    /**
     * <code>sint64 sint64value = 6;</code>
     */
    long getSint64Value();

    /**
     * <code>fixed32 fixed32value = 7;</code>
     */
    int getFixed32Value();

    /**
     * <code>fixed64 fixed64value = 8;</code>
     */
    long getFixed64Value();

    /**
     * <code>sfixed32 sfixed32value = 9;</code>
     */
    int getSfixed32Value();

    /**
     * <code>sfixed64 sfixed64value = 10;</code>
     */
    long getSfixed64Value();

    /**
     * <code>bool boolvalue = 11;</code>
     */
    boolean getBoolvalue();

    /**
     * <code>double doublevalue = 12;</code>
     */
    double getDoublevalue();

    /**
     * <code>string stringvalue = 13;</code>
     */
    java.lang.String getStringvalue();
    /**
     * <code>string stringvalue = 13;</code>
     */
    com.google.protobuf.ByteString
        getStringvalueBytes();

    /**
     * <code>bytes bytesvalue = 14;</code>
     */
    com.google.protobuf.ByteString getBytesvalue();

    /**
     * <code>.baseStruct.PhoneType type = 15;</code>
     */
    int getTypeValue();
    /**
     * <code>.baseStruct.PhoneType type = 15;</code>
     */
    struct.protobuf.structs.baseStruct.PhoneType getType();
  }

这里java类型与protobuf的类型关系完全复合上面的字段类型表

将message序列化使用writeTo方法:

public void writeTo(com.google.protobuf.CodedOutputStream output)
                        throws java.io.IOException {
      if (int32Value_ != 0) {
        output.writeInt32(1, int32Value_);
      }
      if (int64Value_ != 0L) {
        output.writeInt64(2, int64Value_);
      }
      if (uint32Value_ != 0) {
        output.writeUInt32(3, uint32Value_);
      }
      if (uint64Value_ != 0L) {
        output.writeUInt64(4, uint64Value_);
      }
      if (sint32Value_ != 0) {
        output.writeSInt32(5, sint32Value_);
      }
      if (sint64Value_ != 0L) {
        output.writeSInt64(6, sint64Value_);
      }
      if (fixed32Value_ != 0) {
        output.writeFixed32(7, fixed32Value_);
      }
      if (fixed64Value_ != 0L) {
        output.writeFixed64(8, fixed64Value_);
      }
      if (sfixed32Value_ != 0) {
        output.writeSFixed32(9, sfixed32Value_);
      }
      if (sfixed64Value_ != 0L) {
        output.writeSFixed64(10, sfixed64Value_);
      }
      if (boolvalue_ != false) {
        output.writeBool(11, boolvalue_);
      }
      if (doublevalue_ != 0D) {
        output.writeDouble(12, doublevalue_);
      }
      if (!getStringvalueBytes().isEmpty()) {
        com.google.protobuf.GeneratedMessageV3.writeString(output, 13, stringvalue_);
      }
      if (!bytesvalue_.isEmpty()) {
        output.writeBytes(14, bytesvalue_);
      }
      if (type_ != struct.protobuf.structs.baseStruct.PhoneType.MOBILE.getNumber()) {
        output.writeEnum(15, type_);
      }
      unknownFields.writeTo(output);
    }

可以看出,当一个值为空,直接跳过这个值。如果这个值存在,则写入它的filed_num和它编码后的值,
前面讲过对于不同类型的值,有不同的编码方式,比如int32使用先Zigzag编码再Varint编码,
而string通过length-delimited编码,等。

再看看complexStruct的builder和WriteTo方法

public interface complexStructOrBuilder extends
      // @@protoc_insertion_point(interface_extends:complexStruct)
      com.google.protobuf.MessageOrBuilder {

    /**
     * <code>map&lt;int32, string&gt; mapvalues = 1;</code>
     */
    int getMapvaluesCount();
    /**
     * <code>map&lt;int32, string&gt; mapvalues = 1;</code>
     */
    boolean containsMapvalues(
        int key);
    /**
     * Use {@link #getMapvaluesMap()} instead.
     */
    @java.lang.Deprecated
    java.util.Map<java.lang.Integer, java.lang.String>
    getMapvalues();
    /**
     * <code>map&lt;int32, string&gt; mapvalues = 1;</code>
     */
    java.util.Map<java.lang.Integer, java.lang.String>
    getMapvaluesMap();
    /**
     * <code>map&lt;int32, string&gt; mapvalues = 1;</code>
     */

    java.lang.String getMapvaluesOrDefault(
        int key,
        java.lang.String defaultValue);
    /**
     * <code>map&lt;int32, string&gt; mapvalues = 1;</code>
     */

    java.lang.String getMapvaluesOrThrow(
        int key);

    /**
     * <code>repeated .MapFieldEntry map_field = 2;</code>
     */
    java.util.List<struct.protobuf.structs.MapFieldEntry> 
        getMapFieldList();
    /**
     * <code>repeated .MapFieldEntry map_field = 2;</code>
     */
    struct.protobuf.structs.MapFieldEntry getMapField(int index);
    /**
     * <code>repeated .MapFieldEntry map_field = 2;</code>
     */
    int getMapFieldCount();
    /**
     * <code>repeated .MapFieldEntry map_field = 2;</code>
     */
    java.util.List<? extends struct.protobuf.structs.MapFieldEntryOrBuilder> 
        getMapFieldOrBuilderList();
    /**
     * <code>repeated .MapFieldEntry map_field = 2;</code>
     */
    struct.protobuf.structs.MapFieldEntryOrBuilder getMapFieldOrBuilder(
        int index);
  }

可以看出protobuf中的repected对应java.util.list
map对应java.util.Map

writeTo方法:

public void writeTo(com.google.protobuf.CodedOutputStream output)
                        throws java.io.IOException {
      com.google.protobuf.GeneratedMessageV3
        .serializeIntegerMapTo(
          output,
          internalGetMapvalues(),
          MapvaluesDefaultEntryHolder.defaultEntry,
          1);
      for (int i = 0; i < mapField_.size(); i++) {
        output.writeMessage(2, mapField_.get(i));
      }
      unknownFields.writeTo(output);
    }

可以看出,被repected修饰的字段,这个字段中每个子项都用相同的field_num。
这样的优点是不用特殊标识就能标识一个集合字段。
但它的缺点很明显,整个protobuf只能标识一种集合类型。
也就是说map肯定转成list然后再编码的

protected static <V> void serializeIntegerMapTo(
      CodedOutputStream out,
      MapField<Integer, V> field,
      MapEntry<Integer, V> defaultEntry,
      int fieldNumber) throws IOException {
    Map<Integer, V> m = field.getMap();
    if (!out.isSerializationDeterministic()) {
      serializeMapTo(out, m, defaultEntry, fieldNumber);
      return;
    }
    // Sorting the unboxed keys and then look up the values during serialziation is 2x faster
    // than sorting map entries with a custom comparator directly.
    int[] keys = new int[m.size()];
    int index = 0;
    for (int k : m.keySet()) {
      keys[index++] = k;
    }
    Arrays.sort(keys);
    for (int key : keys) {
      out.writeMessage(fieldNumber,
          defaultEntry.newBuilderForType()
              .setKey(key)
              .setValue(m.get(key))
              .build());
    }
  }
 /** Serialize the map using the iteration order. */
private static <K, V> void serializeMapTo(
      CodedOutputStream out,
      Map<K, V> m,
      MapEntry<K, V> defaultEntry,
      int fieldNumber)
      throws IOException {
    for (Map.Entry<K, V> entry : m.entrySet()) {
      out.writeMessage(fieldNumber,
          defaultEntry.newBuilderForType()
              .setKey(entry.getKey())
              .setValue(entry.getValue())
              .build());
    }

通过serializeIntegerMapTo和serializeMapTo方法可知:
当Map最终是转换成list来编码的,其中map转换为list有两种顺序,
一种是按照map的迭代顺序存入list
一种是按照key的字典序

综上,我们通过分析代码理解了protobuf的编码过程,当然解码过程其实也就是把编码过程倒过来。
解码需要注意的是zigzag解码方式:
当使用支持无符号移位的语言时可以使用:

    n = (n >>> 1)^(-(n & 1))

当使用像python这类不支持无符号移位的语言可以使用:

    n = (n ^ (-(n & 1)) )>> 1

  1. 异构网络环境(Heterogeneous Network Environments)是指由不同制造商生产的计算机和系统组成的网络环境。这些计算机系统运行不同的操作系统和通信协议,想统一其计算机资源的机构通常会面临集成异种机系统的任务。一般地,每一个部门或分部已经根据操作系统、局域网拓扑结构、通信协议、应用程序、电子函件系统以及其它因素规定了自己的网络需要。企业网的目标就是使这些分散的资源可以进行互连和互操作,以便网络用户可以和其他用户一起共享文件和电子函件,或者访问企业的数据资源。

  2. 为了更好的跨语言性,google已将protobuf v2中的required删去,默认都是optional。并且也不在使用默认值及[default = * ],更多详见https://github.com/google/protobuf/blob/master/CHANGES.txt

  3. 在知乎上看见一条关于protobuf v3中使用Map的警告:protobuf v3用了Emitter,Unity3d的il2cpp是不支持的,用Unity的留心了。

  4. field_num及为string name = 1其中1就是name的field_num。

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