Gin 源码学习(二)丨请求体中的参数是如何解析的?

上一篇文章Gin 源码学习(一)丨请求中 URL 的参数是如何解析的?
对 Gin 请求中 URL 的参数解析进行了讲解,其中主要是存在于 URL 中的参数,这篇文章将讲解 Gin 是如何解析请求体中的参数的。

主要包括请求头中 Content-Typeapplication/x-www-form-urlencoded 的 POST 表单以及 Content-Typeapplication/json 的 JSON 格式数据请求。

下面开始 Gin 源码学习的第二篇:请求体中的参数是如何解析的?

Go 版本:1.14

Gin 版本:v1.5.0

目录

  • URL 编码的表单参数解析
  • JSON 格式数据的参数解析
  • 小结

URL 编码的表单参数解析

func main() {
    router := gin.Default()

    router.POST("/form_post", func(c *gin.Context) {
        message := c.PostForm("message")
        name := c.DefaultPostForm("name", "Cole")
        m := c.PostFormMap("map")

        c.JSON(200, gin.H{
            "message": message,
            "name":    name,
            "map": m,
        })
    })
    router.Run(":8000")
}

引用 Gin 官方文档中的一个例子,我们先把关注点放在 c.PostForm(key)c.DefaultPostForm(key, defaultValue)上面。相信有些人已经猜到了,以 Default 开头的函数,一般只是对取不到值的情况进行了处理。

c.JSON(code, obj) 函数中的参数 200 为 HTTP 请求中的一种响应码,表示一切正常,而 gin.H 则是 Gin 对 map[string]interface{} 的一种简写。

发起一个对该接口的请求,请求内容和结果如下图所示:

1.jpg

在后续文章中会对 Gin 内部是如何处理响应的进行详细讲解,这里就不对响应的内容进行讲解。

我们一起来看一下,Gin 是如何获取到请求中的表单数据的,先看一下 c.PostForm(key)c.DefaultPostForm(key, defaultValue) 这两个函数的源代码:

// PostForm returns the specified key from a POST urlencoded form or multipart form
// when it exists, otherwise it returns an empty string `("")`.
func (c *Context) PostForm(key string) string {
    value, _ := c.GetPostForm(key)
    return value
}

// DefaultPostForm returns the specified key from a POST urlencoded form or multipart form
// when it exists, otherwise it returns the specified defaultValue string.
// See: PostForm() and GetPostForm() for further information.
func (c *Context) DefaultPostForm(key, defaultValue string) string {
    if value, ok := c.GetPostForm(key); ok {
        return value
    }
    return defaultValue
}

通过注释,我们可以知道这两个函数都用于从 POST 请求的 URL 编码表单或者 multipart 表单(主要用于携带二进制流数据时使用,如文件上传)中返回指定 Key 对应的 Value,区别就是当指定 Key 不存在时,DefaultPostForm(key, defaultValue) 函数将参数 defaultValue 作为返回值返回。

可以看到这两个函数都是从 c.GetPostForm(key) 函数中获取的值,我们来跟踪一下这个函数:

// GetPostForm is like PostForm(key). It returns the specified key from a POST urlencoded
// form or multipart form when it exists `(value, true)` (even when the value is an empty string),
// otherwise it returns ("", false).
// For example, during a PATCH request to update the user's email:
//     email=mail@example.com  -->  ("mail@example.com", true) := GetPostForm("email") // set email to "mail@example.com"
//     email=                  -->  ("", true) := GetPostForm("email") // set email to ""
//                             -->  ("", false) := GetPostForm("email") // do nothing with email
func (c *Context) GetPostForm(key string) (string, bool) {
    if values, ok := c.GetPostFormArray(key); ok {
        return values[0], ok
    }
    return "", false
}

// GetPostFormArray returns a slice of strings for a given form key, plus
// a boolean value whether at least one value exists for the given key.
func (c *Context) GetPostFormArray(key string) ([]string, bool) {
    c.getFormCache()
    if values := c.formCache[key]; len(values) > 0 {
        return values, true
    }
    return []string{}, false
}

首先是 c.GetPostForm(key) 函数,在其内部调用了 c.GetPostFormArray(key) 函数,其返回类型为 ([]string, bool)

然后是 c.GetPostFormArray(key) 函数,在该函数内的第一行,调用了 c.getFormCache() 函数,看到这里,大家是不是有种似曾相似的感觉?没错,这和上一篇文章中的查询字符串的参数解析是类似的,在 gin.Context 类型中,有着两个 url.Values 类型的属性,分别是 queryCacheformCache,而 url.Values 则是位于 net/url 包中的自定义类型,是一种键为字符串,值为字符串切片的 map,这在上一篇文章中有讲过,就当做是复习。

在 Gin 内部,就是把 POST 请求中的表单数据解析并保存至 formCache 中,用于多次获取而无需重复解析。

接下来我们来看一下,c.getFormCache() 函数是如何解析表单中的数据的:

func (c *Context) getFormCache() {
    if c.formCache == nil {
        c.formCache = make(url.Values)
        req := c.Request
        if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
            if err != http.ErrNotMultipart {
                debugPrint("error on parse multipart form array: %v", err)
            }
        }
        c.formCache = req.PostForm
    }
}

首先是对 c.formCache 进行初始化,然后把 c.Request 赋值给 req,再通过调用 req.ParseMultipartForm(maxMemory) 进行参数解析,最后把解析后的数据 req.PostForm 赋值给 c.formCache

问题来了,这里为什么要把 c.Request 赋值给一个临时变量 req 呢?为什么不直接使用 c.Request.ParseMultipartForm(maxMemory) 呢?这样的话连 c.formCache 都可以省了。这个问题我也思考了一会,毕竟我也不是这块功能的设计者,所以,我的个人理解如下(不一定正确):

第一个问题,想必是由于 req.ParseMultipartForm(maxMemory) 的调用,会将表单数据解析至该 Request 对象中,从而增加该对象的内存占用空间,而且在解析过程中不仅只会对 req.PostForm 进行赋值,而对于该 Request 对象中其他属性进行赋值并不是 Gin 所需要的;

第二个问题,同样地,由于第一个问题造成的影响,导致如果直接使用 c.Request 对象来进行参数解析的话,会添加额外的不必要的内存开销,这是阻碍到 Gin 高效的原因之一,其次,假如使用 c.Request.PostForm 的话,那么对参数的获取操作,则操作的对象为 Request 对象,而 Request 对象位于 Go 内置函数库中的 net/http 库,毕竟不属于 Gin 的内部库,如果这样做的话,多多少少会增加两者之间的耦合度,但是如果操作对象是 gin.Context.formCache 的话,那么 Gin 只需把关注点放在自己身上就够了。

以上是笔者对这两个疑问的一些看法,如果有其他看法,欢迎留言讨论!

差点走远了~

好了,回归正题,终于到了我们要找的地方,就是这个属于 Go 自带函数库 net/http 库中的函数 req.ParseMultipartForm(maxMemory)

通过上面的分析,我们知道了在这个函数内部,会对表单数据进行解析,并且将解析后的参数赋值在该 Request 对象的 PostForm 属性上,下面,我们来看一下该函数的源代码(省略无关源代码):

// ParseMultipartForm parses a request body as multipart/form-data.
// The whole request body is parsed and up to a total of maxMemory bytes of
// its file parts are stored in memory, with the remainder stored on
// disk in temporary files.
// ParseMultipartForm calls ParseForm if necessary.
// After one call to ParseMultipartForm, subsequent calls have no effect.
func (r *Request) ParseMultipartForm(maxMemory int64) error {
    // 判断该multipart表单是否已解析过
    if r.MultipartForm == multipartByReader {
        return errors.New("http: multipart handled by MultipartReader")
    }
    if r.Form == nil {
        // 解析请求体中的参数
        err := r.ParseForm()
        if err != nil {
            return err
        }
    }
    if r.MultipartForm != nil {
        return nil
    }

    // 判断该表单的Content-Type是否为multipart/form-data
    // 并解析分隔符
    mr, err := r.multipartReader(false)
    if err != nil {
        // Content-Type不为multipart/form-data
        // Urlencoded表单解析完毕
        return err
    }

    // 省略
    r.MultipartForm = f

    return nil
}

通过函数上方的注释,我们可以知道该函数会将请求体中的数据解析为 multipart/form-data,并且请求体中数据最大为 maxMemory 个字节,还说文件部分存储于内存中,另外部分存储于磁盘中,不过这些都与我们的 URL 编码表单无关,唯一有关的,只是简单的一句,如果需要会调用 ParseForm

通过添加的注释,我们可以知道,URL 编码的表单在调用 r.multipartReader(allowMixed) 函数之后,直接 return,其中参数 allowMixed 表示是否允许 multipart/mixed 类型的 Content-Type,至于 multipart 表单数据的解析这里不做过多说明,其数据以 --分隔符-- 进行分隔,由于使用二进制编码,因此适合用于文件上传等。

下面是对 URL 编码的表单数据进行解析的函数:

func (r *Request) ParseForm() error {
    var err error
    if r.PostForm == nil {
        if r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH" {
            r.PostForm, err = parsePostForm(r)
        }
        if r.PostForm == nil {
            r.PostForm = make(url.Values)
        }
    }
    if r.Form == nil {
        // 省略
    }
    return err
}

func parsePostForm(r *Request) (vs url.Values, err error) {
    if r.Body == nil {
        err = errors.New("missing form body")
        return
    }
    ct := r.Header.Get("Content-Type")
    // RFC 7231, section 3.1.1.5 - empty type
    //   MAY be treated as application/octet-stream
    if ct == "" {
        ct = "application/octet-stream"
    }
    ct, _, err = mime.ParseMediaType(ct)
    switch {
    case ct == "application/x-www-form-urlencoded":
        var reader io.Reader = r.Body
        maxFormSize := int64(1<<63 - 1)
        // 断言r.Body是否为*maxBytesReader(用于限制请求体的大小)
        if _, ok := r.Body.(*maxBytesReader); !ok {
            // 设置请求体最大为10M
            maxFormSize = int64(10 << 20) // 10 MB is a lot of text.
            // 创建从r.Body读取的Reader
            // 但是当读取maxFormSize+1个字节后会以EOF停止
            reader = io.LimitReader(r.Body, maxFormSize+1)
        }
        // 将请求体内容以字节方式读取
        b, e := ioutil.ReadAll(reader)
        if e != nil {
            if err == nil {
                err = e
            }
            break
        }
        if int64(len(b)) > maxFormSize {
            err = errors.New("http: POST too large")
            return
        }
        vs, e = url.ParseQuery(string(b))
        if err == nil {
            err = e
        }
    case ct == "multipart/form-data":
        // 无具体实现
    }
    return
}

首先看一下 r.ParseForm() 函数,通过源代码可以发现,其支持的请求方式有 POST, PATCHPUT,如果请求类型符合条件,那么就调用 parsePostForm(r) 函数为 r.PostForm 赋值,然后下面是判断 r.Form 属性是否为空,这里面的源代码省略了,因为没涉及到对 Gin 所需要的 r.PostForm 属性进行赋值操作。

然后是 parsePostForm(r) 函数,通过源代码,我们可以发现,除了 Content-Typeapplication/x-www-form-urlencoded 的表单请求外,其余类型的在此函数中都不做处理,源代码逻辑以在注释中给出,需要注意的是 url.ParseQuery(string(b)) 函数的调用,该函数用于解析表单中的数据,而表单中的数据存储方式,与上一篇文章中讲的 URL 中的查询字符串的存储方式一致,因此,表单内数据解析的方式与 URL 中的查询字符串的解析也是一样的。

最后再看一下 Gin 获取请求中 map 类型参数的实现源代码:

// PostFormMap returns a map for a given form key.
func (c *Context) PostFormMap(key string) map[string]string {
    dicts, _ := c.GetPostFormMap(key)
    return dicts
}

// GetPostFormMap returns a map for a given form key, plus a boolean value
// whether at least one value exists for the given key.
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
    c.getFormCache()
    return c.get(c.formCache, key)
}

// get is an internal method and returns a map which satisfy conditions.
func (c *Context) get(m map[string][]string, key string) (map[string]string, bool) {
    dicts := make(map[string]string)
    exist := false
    for k, v := range m {
        if i := strings.IndexByte(k, '['); i >= 1 && k[0:i] == key {
            if j := strings.IndexByte(k[i+1:], ']'); j >= 1 {
                exist = true
                dicts[k[i+1:][:j]] = v[0]
            }
        }
    }
    return dicts, exist
}

c.PostFormMap(key) 函数中直接调用 c.GetPostFormMap(key) 函数获取请求中的 map 类型参数,在 c.GetPostFormMap(key) 函数中,同样先是调用 c.getFormCache() 判断请求中的数据是否已缓存处理,然后以存储表单缓存数据的参数 c.formCache 和要获取参数的 key 作为 c.get(m, key) 的参数,调用该函数,获取该 map 类型数据。

我们来看一下最后的这个 c.get(m, key) 函数,首先声明了一个类型为 map[string]string 的变量 dicts 用于结果集,声明了一个值为 false 的变量 exist 用于标记是否存在符合的 map。

随后是对 c.formCache 的遍历,以最初的请求为例,则该 c.formCache 的值为:

{"message": ["name"], "name": ["Les An"], "map[a]": ["A"], "map[b]": ["B"]}

由于 m 的类型为 map[string][]string,因此 kv 分别为 m 的字符串类型的键和字符串切片类型的值,在遍历过程中判断 m 中的每个 k 是否存在 [,若存在则判断位于 [ 前面的所有内容是否与传入的参数 key 相同,若相同则判断 [ 中是否存有间隔大于等于 1 的 ],若存在则将 [] 之间的字符串作为要返回的 map 中的其中一个键,将该 k 对应的 v 字符串切片的第一个元素作为该键的值,以此循环。

JSON 格式数据的参数解析

Gin 提供了四种可直接将请求体中的 JSON 数据解析并绑定至相应类型的函数,分别是:BindJSON, Bind, ShouldBindJSON, ShouldBind。下面讲解的不会太涉及具体的 JSON 解析算法,而是更偏向于 Gin 内部的实现逻辑。

其中,可为它们分为两类,一类为 Must Bind,另一类为 Should Bind,前缀为 Should 的皆属于 Should Bind,而以 Bind 为前缀的,则属于 Must Bind。正如其名,Must Bind 一类在对请求进行解析时,若出现错误,会通过 c.AbortWithError(400, err).SetType(ErrorTypeBind) 终止请求,这会把响应码设置为 400,Content-Type 设置为 text/plain; charset=utf-8,在此之后,若尝试重新设置响应码,则会出现警告,如将响应码设置为 200:[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 200;而 Should Bind 一类在对请求进行解析时,若出现错误,只会将错误返回,而不会主动进行响应,所以,在使过程中,如果对产生解析错误的行为有更好的控制,最好使用 Should Bind 一类,自行对错误行为进行处理。

先来看一下下面这段代码,以及发起一个请求体为 JSON 格式数据的请求后得到的响应内容(解析正常的情况下 /must/should 响应内容一致):

func main() {
    router := gin.Default()

    router.POST("/must", func(c *gin.Context) {
        var json map[string]interface{}
        if err := c.BindJSON(&json); err == nil {
            c.JSON(http.StatusOK, gin.H{"msg": fmt.Sprintf("username is %s", json["username"])})
        }
    })

    router.POST("/should", func(c *gin.Context) {
        var json map[string]interface{}
        if err := c.ShouldBindJSON(&json); err != nil {
            c.JSON(http.StatusOK, gin.H{"msg": err.Error()})
            return
        }
        c.JSON(http.StatusOK, gin.H{"msg": fmt.Sprintf("username is %s", json["username"])})
    })

    router.Run(":8000")
}
2.jpg

我们先来看一下开始提到的 BindBindJSON 这两个函数的实现源代码:

// Bind checks the Content-Type to select a binding engine automatically,
// Depending the "Content-Type" header different bindings are used:
//     "application/json" --> JSON binding
//     "application/xml"  --> XML binding
// 省略部分注释
func (c *Context) Bind(obj interface{}) error {
    b := binding.Default(c.Request.Method, c.ContentType())
    return c.MustBindWith(obj, b)
}

// BindJSON is a shortcut for c.MustBindWith(obj, binding.JSON).
func (c *Context) BindJSON(obj interface{}) error {
    return c.MustBindWith(obj, binding.JSON)
}

// MustBindWith binds the passed struct pointer using the specified binding engine.
// It will abort the request with HTTP 400 if any error occurs.
// See the binding package.
func (c *Context) MustBindWith(obj interface{}, b binding.Binding) error {
    if err := c.ShouldBindWith(obj, b); err != nil {
        c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // nolint: errcheck
        return err
    }
    return nil
}

首先是 c.Bind(obj) 函数,其与 c.BindJSON(obj) 的唯一区别就是,它会自动检查 Content-Type 来选择绑定引擎,例如 application/json,则使用 JSON 绑定,application/xml 则选择 XML 绑定,省略的注释内容笔者本人在阅读时,感觉写得不是很正确,可能是在更新过程中没有修改该注释的原因,因此将其省略。

在该函数中,通过调用 binding.Default(method, contentType) 函数,根据请求方法的类型以及 Content-Type 来获取相应的绑定引擎,首先是判断请求的方法类型,如果为 GET,则直接返回 Form 绑定引擎,否则使用 switch 根据 Content-Type 选择合适的绑定引擎。

接下来则是使用传递进来的 obj 指针结构和相应的绑定引擎来调用 c.MustBindWith(obj, b) 函数,该函数将会使用该绑定引擎将请求体中的数据绑定至该 obj 指针结构上。

c.MustBindWith(obj, b) 函数内部,实际上调用的是 c.ShouldBindWith(obj, b) 函数,看到这里,大家应该也懂了,没错,Must Bind 一类内部的实现其实也是调用 Should Bind 一类,只不过 Must Bind 一类主动对错误进行了处理并进行响应,也就是一开始提到的,设置响应码为 400 以及 Content-Typetext/plain; charset=utf-8 同时对请求进行响应并返回错误给调用者,至于 c.AbortWithError(code, err) 函数的具体实现,我们这里不做过多解释,只需理解其作用就行,在本系列的后续文章中,会对其再做详细讲解。

下面,我们先来一起看一下,c.BindJSON(obj) 函数中使用的 binding.JSONc.MustBindWith(obj, b) 函数中的参数 b binding.Binding 有什么关系,其相关源代码如下:

// Binding describes the interface which needs to be implemented for binding the
// data present in the request such as JSON request body, query parameters or
// the form POST.
type Binding interface {
    Name() string
    Bind(*http.Request, interface{}) error
}

// These implement the Binding interface and can be used to bind the data
// present in the request to struct instances.
var (
    JSON          = jsonBinding{}
    XML           = xmlBinding{}
    Form          = formBinding{}
    Query         = queryBinding{}
    FormPost      = formPostBinding{}
    FormMultipart = formMultipartBinding{}
    ProtoBuf      = protobufBinding{}
    MsgPack       = msgpackBinding{}
    YAML          = yamlBinding{}
    Uri           = uriBinding{}
    Header        = headerBinding{}
)

type jsonBinding struct{}

func (jsonBinding) Name() string {
    return "json"
}

func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
    if req == nil || req.Body == nil {
        return fmt.Errorf("invalid request")
    }
    return decodeJSON(req.Body, obj)
}

从源代码中,可以看出 binding.Binding 是一个接口类型,其有两个可实现方法,Name()Bind(*http.Request, interface{}),然后 binding.JSON 是一个 jsonBinding 类型的对象,类似 Gin 还提供了 binding.XML, binding.Form 等全局变量,分别表示不同类型的绑定引擎,jsonBinding 是 Gin 内部定义的一个自定义类型,其实现了 binding.Binding 接口,Name() 函数返回绑定引擎的名称,Bind(req, obj) 函数用于对请求体中的数据进行解析并绑定至传递进来的指针变量 obj 上。

Gin 默认使用的是 Go 自带函数库中的 encoding/json 库来进行 JSON 解析,但是由于 encoding/json 库提供的 JSON 解析性能不算特别快,因此还提供了一个 JSON 解析库 json-iterator,使用的方式也很简单,只需在运行或构建时加上 -tags=jsoniter 选项即可,例如:go run main.go -tags=jsoniter,这是 Go 提供的一种条件编译方式,通过添加标签的方式来实现条件编译,在 Gin 的 internal/json 库中,有着 json.gojsoniter.go 两个源文件,内容分别如下:

// json.go
// +build !jsoniter

package json

import "encoding/json"

var (
    // Marshal is exported by gin/json package.
    Marshal = json.Marshal
    // Unmarshal is exported by gin/json package.
    Unmarshal = json.Unmarshal
    // MarshalIndent is exported by gin/json package.
    MarshalIndent = json.MarshalIndent
    // NewDecoder is exported by gin/json package.
    NewDecoder = json.NewDecoder
    // NewEncoder is exported by gin/json package.
    NewEncoder = json.NewEncoder
)
// jsoniter.go
// +build jsoniter

package json

import "github.com/json-iterator/go"

var (
    json = jsoniter.ConfigCompatibleWithStandardLibrary
    // Marshal is exported by gin/json package.
    Marshal = json.Marshal
    // Unmarshal is exported by gin/json package.
    Unmarshal = json.Unmarshal
    // MarshalIndent is exported by gin/json package.
    MarshalIndent = json.MarshalIndent
    // NewDecoder is exported by gin/json package.
    NewDecoder = json.NewDecoder
    // NewEncoder is exported by gin/json package.
    NewEncoder = json.NewEncoder
)

json.go 文件的头部中,带有 // +build !jsoniter,而在 jsoniter.go 文件的头部中,则带有 // +build jsoniter,Go 的条件编译的实现,正是通过 +build-tags 实现的,此处的 // +build !jsoniter 表示,当 tags 不为 jsoniter 时,使用该文件进行编译,而 // +build jsoniter 则与其相反。

通过上面的源码讲解,我们可以从中发现一个问题,在对请求中的数据进行绑定时,仅有在调用 binding.Default(method, contentType) 函数时,才会对请求的 Content-Type 进行判断,以 JSON 格式的请求数据为例,假设请求体中的 JSON 格式数据正确,那么,当调用 c.BindJSON(obj) 或者 c.ShouldBindJSON(obj) 时,即使请求头中的 Content-Type 不为 application/json,对请求体中的 JSON 数据也是能够正常解析并正常响应的,仅有在使用 c.Bind(obj) 或者 c.ShouldBind(obj) 对请求数据进行解析时,才会去判断请求的 Content-Type 类型。

小结

这篇文章讲解了 Gin 是如何对请求体中的数据进行解析的,分别是使用 URL 编码的表单数据和 JSON 格式的数据。

第一部分讲的是 URL 编码的表单参数解析过程,首先是对表单中的数据进行提取,提取之后使用与上一篇文章中 URL 查询字符串参数解析相同的方式进行解析并做缓存处理;同时还涉及到了另外的一种适用于上传二进制数据的 Multipart 表单,其使用切割符对数据进行解析;最后还讲了 Gin 从缓存起来的请求数据中获取 map 类型数据的实现算法。

第二部分讲的是 JSON 格式数据的参数解析过程,在 Gin 内部提供了多种用于解析不同格式参数的绑定引擎,其共同实现了 binding.Binding 接口,所以其他格式数据的请求参数解析也是与此类似的,不同的地方仅仅是将数据从请求体中绑定至指针变量中使用的解析算法不一样而已;然后还讲了 Gin 额外提供了另外一个可供选择的 JSON 解析库 json-iterator,其适用于对性能有较高要求的场景,并且介绍了 Go 提供的条件编译的实现方式。

本系列的下一篇文章将对 Gin 中路由匹配机制的实现进行讲解,至此,Gin 源码学习的第二篇也就到此结束了,感谢大家对本文的阅读~~

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

推荐阅读更多精彩内容