Golang gob与rpc简介

本文参考
Gob的数据
手机网络游戏应用协议设计(一)

关于protobuf基础知识,可以参考Cocos Creator Protobuf的js版本使用
关于varint,参考数值压缩存储方法Varint图解Protobuf编码

为了让某个数据结构能够在网络上传输或能够保存至文件,它必须被编码然后再解码。当然,已经有许多可用的编码方式了:JSON,XML,Google 的 protocol buffers,等等。而现在,又多了一种,由 Go 的 gob 包提供的方式。

为什么定义新的编码?这要做许多繁重的工作。为什么不使用某个现成的格式?呃,无论如何,我们这样做了!Go 已经有刚才提到的所有编码方式的包(protocol buffer 包在另外一个代码库中,但它是下载得最多的包之一)。并且在许多情况下,包括同其他语言编写的工具和系统通讯,这些都是正确的选择。

但是在特定的 Go 环境中,例如在两个 Go 编写的服务之间通讯,这需要某些东西使得其更加容易使用,并且可能更加有效率。Gobs 协同 Go 语言的工作方式,对于那些外部定义的、同语言无关的编码方式来说无法做到。同时,从现有的系统中也吸取了很多教训。

一、目标

gob 包有在设计时有许多目标。

首先,也是最显然的,它被设计成为非常容易使用的。一方面,由于 Go 有反射(reflection),就没有必要弄一个单独的接口定义语言或“协议编译器”。数据结构本身提供了编码和解码所需要的全部信息。 另一方面,这种方法也意味着 gob 永远无法良好的同其他语言协同工作,但这没问题:gob 是厚颜无耻的以 Go 为中心(译注:呃,以XXX为中心,坚决贯彻XXX的领导……)。

效率也是非常重要的。基于文本形式的,如 XML 和 JSON ,应用于高效通讯网络会太慢了。二进制编码是必须的(译注:二进制神马的,是必须的!回音:必须的……)!

Gob 流必须可以自解释。每个 gob 流,从开始读取,整个流将由包含足够的信息,以便在终端对其内容毫不知情的前提下对整个流加以解析。这一特性意味,即便你忘记了保存在文件中的 gob 流表示什么,也总是可以对其解码。

同样,这里有一些从 Google protocol buffers 获得的经验。

二、Protocol buffer 的硬伤

Protocol buffers 对 gob 的设计产生了主要的影响,但是有三个特性被谨慎的避开了。(暂且不说 protocol buffer 不是自解释的:如果你不知道 protocol buffer 编码时的数据的定义,你就无法解析它。)

首先,protocol buffer 仅工作于 Go 的 struct 数据类型。不能在最顶级编码一个整数或者数组,只可以将其至于 struct 中作为一个字段。 至少在 Go 中,这个限制似乎没有什么意义。如果你希望传输的仅仅是一个数组或者整数,为什么你要先将其放到 struct 中?

其次,可能 protocol buffer 的定义指定字段 T.x 和 T.y 需要解析,无论是在编码还是解码类型 T 的值。虽然,这样的必须字段看起来是个好主意,但是由于编解码器中必须含有用于编码和解码的特定的数据结构,用于报告必须字段是否丢失,实现的开销是大的。这同样也产生了问题。过一段时间后,某人可能希望修改数据定义,移除了必须的字段,但这导致现有接收数据的客户端崩溃。最好是在编码时就根本没有这些字段。(Protocol buffer 也有可选字段。但是,如果我们没有必须字段,所有的字段就是可选的。等一下还会针对可选字段进行一些讨论。)

第三个 protocol buffer 的硬伤是默认值。当 protocol buffer 在某个“默认”字段上设置了默认值,而解码后的结构就像那个字段被设置了某个值一样。这个想法在有 getter 和 setter 控制字段的访问的时候非常棒,但是当容器是一个原始结构的时候就很难控制其保持清晰了。必须的字段也存在同样的麻烦:在哪定义默认值,它们的类型是什么(是UTF-8文本?无符号字节串?在浮点型中有几位?)尽管有许多看起来很简单,protocol buffer 的设计和实现还是有许多伴随的问题。我们决定让这些都远离 gob,并且回到我们的 Go 旅程中,一个很有效率的默认规则:除非你设置了一些内容,否则就是那个类型的“零值”,而这个不需要被传输。

所以 gob 最终看起来是个更加通用、简单的 protocol buffer。它又是如何工作的呢?

三、gob 的值

编码后的 gob 数据不是 int8 或者 uint16 的串。作为代替,其看起来更象是 Go 的常量,不论是有符号的还是无符号的整数值是虚拟的、无大小定义的数字。当你编码一个 int8 的时候,其值被转换为一个无大小定义的变长整数。当你对 int64 编码时,其值也是转换为一个无大小定义的变长整数。(有符号和无符号是相同处理的,无大小定义也适用于无符号值。)如果都是值 7,在线传输的位是一致的。当接收者解码其值,它将其放入接收者变量中,可能是任意的一个整数类型。因此,编码方发送了一个来自 int8 的 7,而接收方可能将其保存在 int64 中。这没有问题:这个值永远匹配于一个整数。(如果不匹配,会产生错误。)在变量的大小上解偶,为编码提供了一些灵活性:我们可以随着软件演化扩展整数类型,但是仍然可以解码旧的数据。

这种灵活性对于指针同样有效。在传输前,所有指针都进行整理。int8、int8、*int8、****int8等等的值,被传输为可能被存储于任何大小的 int,或者 *int,或者 ******int等等的整数值。这同样是一种灵活性。

同样的原因,在解码一个 struct,当其字段由编码方发送,存储于目标方的时候,也体现出这种灵活性。给出这样一个值:type T struct { X, Y, Z int } // 只有导出字段(exported fields)被编码和解码。 var t = T{X: 7, Y: 0, Z: 8}编码后仅发送 7 和 8。由于为零,Y 不会被发送;没有必要发送一个零值。

接收方可能用下面的结构解码:type U struct { X, Y *int8 } // 注意:int8 的指针 var u U而获得的 u 的值只有 X (值为 7 的 int8 变量的地址);Z 字段被忽略了——你应将其放到哪里呢?当解码一个 struct 的时候,字段会匹配其名字和类型,只有双方都有的字段会生效。这个简单的办法巧妙处理了“可选字段”问题:类型 T 添加了字段,过期的接收者仍然能处理它们知道的那部分。因此 gob 在可选字段上提供了重要的特性——无须任何额外的机制或标识。

从整数串可以构造其他类型:字节数组、字符串、数组、内存片段、Map,甚至浮点数组。IEEE 754 浮点位定义描述了浮点值存储为整数,在你知道其类型的时候,这会工作得很好,我们总是知道类型的吧。另外,这里的整数使用字节翻转的顺序发送,因为一般的浮点数字,就像是小整数数组,在低位上有许多个零是不用传递的。

gob 还有一个非常棒的特性是 Go 使得通过 GobEncoder 和 GobDecoder 接口使得自定义类型的编码成为可能,从某个意义上说类似于 JSON 包的 Marshaler 和 Unmarshaler,以及 fmt 包的 String 化接口。这个技巧使一些特殊功能成为可能,强制使用常量,或者传输数据的时候隐藏信息。

四、gob 类型的传输

在第一次传输给定类型的时候,gob 包中包含了这个类型的描述。实际上,是这样的,编码器编码的是gob标准格式,而内部的 struct 则带有类型描述并给其标识一个唯一编号。(基本类型、类型描述结构的层级,在软件启动时已经定义好了。)在类型被描述后,它可以通过编号来引用。

因此,当我们发送类型 T 时,gob 编码器发送 T 的描述,并对其编号,例如 127。包括第一个数据包在内的所有的数据,都使用这个编号,所以 T 值的数据流看起来是这样:("define type id" 127, definition of type T)(127, T value)(127, T value), ...

类型编号使得描述递归类型,以及发送这些类型的数据成为可能。因此,gob 可以对树状类型做编码:type Node struct { Value int Left, Right *Node }

带有了类型信息,gob 流就完全自说明了。除了那些初始类型,它们已经在开始的时候就定义好了。

五、gob 编译机

在第一次传输给定类型的时候,gob 包会构造一个针对这个类型的小翻译机。在这个类型上使用了反射来构造这个翻译机,但是一旦翻译机构建完成,它就不再依赖反射。这个翻译机使用了 unsafe 和其他一些巧妙的机制来高速的将数据转化为编码后的字节流。也可以使用反射来避免 unsafe,但是会明显变慢。(受到 gob 实现的影响,Go 的 protocol buffer 使用了类似的机制提高速度。)而后的同样类型的值使用已经编译好的翻译机,这样就可以总是有一致的编码。

解码类似,但是略微复杂。当你解码一个数据,gob 包用一个字节片保存编码后的类型的值用于来解码,再加上得到解码的 Go 的值。gob 包构造一个翻译机用于这个过程:gob 类型在线传输用于 Go 类型的解码。一旦解码翻译机构造,一个没有反射的使用 unsafe 方法的引擎能提供最快的速度。

六、gob 例子

参考 golang - gob与rpc

例1:数据结构与bytes.Buffer之间的转换(编码成字节切片)

 1package main
 2
 3import (
 4    "bytes"
 5    "fmt"
 6    "encoding/gob"
 7    "io"
 8)
 9
10//准备编码的数据
11type P struct {
12    X, Y, Z int
13    Name    string
14}
15
16//接收解码结果的结构
17type Q struct {
18    X, Y *int32
19    Name string
20}
21
22func main() {
23    //初始化一个数据
24    data := P{3, 4, 5, "CloudGeek"}
25    //编码后得到buf字节切片
26    buf := encode(data)
27    //用于接收解码数据
28    var q *Q
29    //解码操作
30    q = decode(buf)
31    //"CloudGeek": {3,4}
32    fmt.Printf("%q: {%d,%d}\n", q.Name, *q.X, *q.Y)
33
34}
35
36func encode(data interface{}) *bytes.Buffer {
37    //Buffer类型实现了io.Writer接口
38    var buf bytes.Buffer
39    //得到编码器
40    enc := gob.NewEncoder(&buf)
41    //调用编码器的Encode方法来编码数据data
42    enc.Encode(data)
43    //编码后的结果放在buf中
44    return &buf
45}
46
47func decode(data interface{}) *Q {
48    d := data.(io.Reader)
49    //获取一个解码器,参数需要实现io.Reader接口
50    dec := gob.NewDecoder(d)
51    var q Q
52    //调用解码器的Decode方法将数据解码,用Q类型的q来接收
53    dec.Decode(&q)
54    return &q
55}

例2:数据结构到文件的序列化和反序列化

 1package main
 2
 3import (
 4    "encoding/gob"
 5    "os"
 6    "fmt"
 7)
 8
 9//试验用的数据类型
10type Address struct {
11    City    string
12    Country string
13}
14
15//序列化后数据存放的路径
16var filePath string
17
18func main() {
19    filePath = "./address.gob"
20    encode()
21    pa := decode()
22    fmt.Println(*pa) //{Chengdu China}
23}
24
25//将数据序列号后写到文件中
26func encode() {
27    pa := &Address{"Chengdu", "China"}
28    //打开文件,不存在的时候新建
29    file, _ := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY, 0666)
30    defer file.Close()
31
32    //encode后写到这个文件中
33    enc := gob.NewEncoder(file)
34    enc.Encode(pa)
35}
36
37//从文件中读取数据并反序列化
38func decode() *Address {
39    file, _ := os.Open(filePath)
40    defer file.Close()
41
42    var pa Address
43    //decode操作
44    dec := gob.NewDecoder(file)
45    dec.Decode(&pa)
46    return &pa
47}
七、golang中的rpc

参考
如何给老婆解释什么是RPC
golang - gob与rpc

1.rpc服务端

 1package main
 2
 3import (
 4    "net"
 5    "net/rpc"
 6    "net/http"
 7)
 8
 9type Args struct {
10    A, B int
11}
12
13//定义一个算术类型,其实就是int
14type Arith int
15
16//实现乘法的方法绑定到Arith类型,先不管为什么是这样的形式
17func (t *Arith) Multiply(args *Args, reply *int) error {
18    *reply = args.A * args.B
19    return nil
20}
21
22func main() {
23    //得到一个Arith类型的指针实例
24    arith := new(Arith)
25    //注册到rpc服务
26    rpc.Register(arith)
27    //挂到http服务上
28    rpc.HandleHTTP()
29    //开始监听
30    l, _ := net.Listen("tcp", ":1234")
31    http.Serve(l, nil)
32}

2.rpc客户端

 1package main
 2
 3import (
 4    "net/rpc"
 5    "fmt"
 6)
 7
 8type Args struct {
 9    A, B int
10}
11
12func main() {
13    //连接服务器端,创建一个client
14    client, _ := rpc.DialHTTP("tcp", "127.0.0.1:1234")
15    args := &Args{7, 8}
16    var reply int
17    //通过Call方法调用Arith类型的Multiply方法,注意形参
18    client.Call("Arith.Multiply", args, &reply)
19    //得到调用结果,输出Arith: 7*8=56
20    fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
21}

下面我们再来看一些rpc相关的细节,首先能够被rpc调用的方法应该看起来像这样:func (t *T) MethodName(argType T1, replyType *T2) error,大概解释一下:

  • 函数必须是可导出的(首字母大写)
  • 必须有两个导出类型的参数,第一个参数用来接收参数,第二个参数是返回给客户端的结果参数,第二个参数必须是指针类型的
  • 函数还要有一个返回值error
  • T1、T2能够被encoding/gob编码

看到这里你应该对于rpc的作用有了一定的认识,go中rpc包的用法简单来看就是准备一个类型,绑定一堆符合规范的方法,然后注册给rpc服务,监听客户端连接,客户端通过rpc包提供的Call方法可以调用到server注册好的方法。

八、GOLANG实现RPC的几种方式
1.net/rpc库

在golang中实现RPC非常简单,有封装好的官方库和一些第三方库提供支持。Go RPC可以利用tcp或http来传递数据,可以对要传递的数据使用多种类型的编解码方式。golang官方的net/rpc库使用encoding/gob进行编解码,支持tcp或http数据传输方式,由于其他语言不支持gob编解码方式,所以使用net/rpc库实现的RPC方法没办法进行跨语言调用

在上一节中已经简单介绍过:

客户端通过rpc包提供的Call方法可以调用到server注册好的方法

$GOPATH/src/test/rpc/rpc_server.go

package main

import (
    "errors"
    "fmt"
    "log"
    "net"
    "net/http"
    "net/rpc"
    "os"
)

// 算数运算结构体
type Arith struct {
}

// 算数运算请求结构体
type ArithRequest struct {
    A int
    B int
}

// 算数运算响应结构体
type ArithResponse struct {
    Pro int // 乘积
    Quo int // 商
    Rem int // 余数
}

// 乘法运算方法
func (this *Arith) Multiply(req ArithRequest, res *ArithResponse) error {
    res.Pro = req.A * req.B
    return nil
}

// 除法运算方法
func (this *Arith) Divide(req ArithRequest, res *ArithResponse) error {
    if req.B == 0 {
        return errors.New("divide by zero")
    }
    res.Quo = req.A / req.B
    res.Rem = req.A % req.B
    return nil
}

func main() {
    rpc.Register(new(Arith)) // 注册rpc服务
    rpc.HandleHTTP()         // 采用http协议作为rpc载体

    lis, err := net.Listen("tcp", "127.0.0.1:8095")
    if err != nil {
        log.Fatalln("fatal error: ", err)
    }

    fmt.Fprintf(os.Stdout, "%s", "start connection")

    http.Serve(lis, nil)
}

$GOPATH/src/test/rpc/rpc_client.go

package main

import (
    "fmt"
    "log"
    "net/rpc"
)

// 算数运算请求结构体
type ArithRequest struct {
    A int
    B int
}

// 算数运算响应结构体
type ArithResponse struct {
    Pro int // 乘积
    Quo int // 商
    Rem int // 余数
}

func main() {
    conn, err := rpc.DialHTTP("tcp", "127.0.0.1:8095")
    if err != nil {
        log.Fatalln("dailing error: ", err)
    }

    req := ArithRequest{9, 2}
    var res ArithResponse

    err = conn.Call("Arith.Multiply", req, &res) // 乘法运算
    if err != nil {
        log.Fatalln("arith error: ", err)
    }
    fmt.Printf("%d * %d = %d\n", req.A, req.B, res.Pro)

    err = conn.Call("Arith.Divide", req, &res)
    if err != nil {
        log.Fatalln("arith error: ", err)
    }
    fmt.Printf("%d / %d, quo is %d, rem is %d\n", req.A, req.B, res.Quo, res.Rem)
}
2.net/rpc/jsonrpc库

golang官方还提供了net/rpc/jsonrpc库实现RPC方法,JSON RPC采用JSON进行数据编解码,因而支持跨语言调用。但目前的jsonrpc库是基于tcp协议实现的,暂时不支持使用http进行数据传输,数据编解码性能不高。

$GOPATH/src/test/rpc/jsonrpc_server.go

package main

import (
    "errors"
    "fmt"
    "log"
    "net"
    "net/rpc"
    "net/rpc/jsonrpc"
    "os"
)

// 算数运算结构体
type Arith struct {
}

// 算数运算请求结构体
type ArithRequest struct {
    A int
    B int
}

// 算数运算响应结构体
type ArithResponse struct {
    Pro int // 乘积
    Quo int // 商
    Rem int // 余数
}

// 乘法运算方法
func (this *Arith) Multiply(req ArithRequest, res *ArithResponse) error {
    res.Pro = req.A * req.B
    return nil
}

// 除法运算方法
func (this *Arith) Divide(req ArithRequest, res *ArithResponse) error {
    if req.B == 0 {
        return errors.New("divide by zero")
    }
    res.Quo = req.A / req.B
    res.Rem = req.A % req.B
    return nil
}

func main() {
    rpc.Register(new(Arith)) // 注册rpc服务

    lis, err := net.Listen("tcp", "127.0.0.1:8096")
    if err != nil {
        log.Fatalln("fatal error: ", err)
    }

    fmt.Fprintf(os.Stdout, "%s", "start connection")

    for {
        conn, err := lis.Accept() // 接收客户端连接请求
        if err != nil {
            continue
        }

        go func(conn net.Conn) { // 并发处理客户端请求
            fmt.Fprintf(os.Stdout, "%s", "new client in coming\n")
            jsonrpc.ServeConn(conn)
        }(conn)
    }
}

$GOPATH/src/test/rpc/jsonrpc_client.go

package main

import (
    "fmt"
    "log"
    "net/rpc/jsonrpc"
)

// 算数运算请求结构体
type ArithRequest struct {
    A int
    B int
}

// 算数运算响应结构体
type ArithResponse struct {
    Pro int // 乘积
    Quo int // 商
    Rem int // 余数
}

func main() {
    conn, err := jsonrpc.Dial("tcp", "127.0.0.1:8096")
    if err != nil {
        log.Fatalln("dailing error: ", err)
    }

    req := ArithRequest{9, 2}
    var res ArithResponse

    err = conn.Call("Arith.Multiply", req, &res) // 乘法运算
    if err != nil {
        log.Fatalln("arith error: ", err)
    }
    fmt.Printf("%d * %d = %d\n", req.A, req.B, res.Pro)

    err = conn.Call("Arith.Divide", req, &res)
    if err != nil {
        log.Fatalln("arith error: ", err)
    }
    fmt.Printf("%d / %d, quo is %d, rem is %d\n", req.A, req.B, res.Quo, res.Rem)
}
3.protorpc库

为了实现跨语言调用,在golang中实现RPC方法的时候我们应该选择一种跨语言的数据编解码方式,比如JSON,上述的jsonrpc可以满足此要求,但是也存在一些缺点,比如不支持http传输,数据编解码性能不高等。于是呢,一些第三方rpc库都选择采用protobuf进行数据编解码,并提供一些服务注册代码自动生成功能。下面的例子我们使用protobuf来定义RPC方法及其请求响应参数,并使用第三方的protorpc库来生成RPC服务注册代码。

$GOPATH/src/test/rpc/pb/arith.proto

syntax = "proto3";
package pb;

// 算术运算请求结构
message ArithRequest {
    int32 a = 1;
    int32 b = 2;
}

// 算术运算响应结构
message ArithResponse {
    int32 pro = 1;  // 乘积
    int32 quo = 2;  // 商
    int32 rem = 3;  // 余数
}

// rpc方法
service ArithService {
    rpc multiply (ArithRequest) returns (ArithResponse);    // 乘法运算方法
    rpc divide (ArithRequest) returns (ArithResponse);      // 除法运算方法
}
4.其他RPC库

除了上面提到的三种在golang实现RPC的方式外,还有一些其他的rpc库提供了类似的功能,比较出名的有google开源的grpc,但是grpc的初次安装比较麻烦,这里就不做进一步介绍了,有兴趣的可以自己了解。
参考https://www.grpc.io/https://github.com/grpc/grpc-go
在gRPC客户端可以直接调用不同服务器上的远程程序,就想调用本地程序一样,很容易构建分布式应用和服务。和很多RPC系统一样,服务负责实现定义好的接口并处理客户端请求,客户端根据接口描述直接调用需要的服务。客户端和服务器可以分别使用gRPC支持的不同语言实现。
gRPC在Go中的使用(一)Protocol Buffers语法与相关使用
gRPC在Go中的使用(二)gRPC实现简单通讯
gRPC在Go中的使用(三)gRPC实现TLS加密通信与流模式

Dive into gRPC(1):gRPC简介
Dive into gRPC(2):实现一个服务
Dive into gRPC(3):安全通信
Dive into gRPC(4):Streaming
Dive into gRPC(5):验证客户端
Dive into gRPC(6):metadata

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

推荐阅读更多精彩内容

  • http://mp.weixin.qq.com/s/ZgWzkBl_x_YvsRkPWnV6Yg
    爱我之域阅读 44评论 0 0
  • 学思践悟101——包容心 包容是中华民族的传统美德,也是一个人应该具备的宝贵品质,怀有...
    若水ct十陈涛阅读 679评论 1 1
  • 在下一个拐角处,有昏暗的路灯,一个等待的人。在到达那里的时候,我们不知放弃了什么,拥有了什么,但有信念告诉我们要不...
    面句阅读 308评论 0 1