SQLite权威指南(第二版)第七章 The Extension C API

SQLite权威指南(第二版)第七章 The Extension C API

本章介绍有关SQLite的新技术。主要讨论用户自定义函数、聚合、排序规则

  • 用户自定义函数:用户编写的用于特定应用的SQL函数,可以在SQL语句中调用
  • 聚合:聚合函数可以从多个字段进行计算(普通函数对单个记录进行操作),SQLite标准的聚合函数有SUM()/COUNT()/AVG()
  • 排序规则:对文本进行比较和排序的方法。SQLite默认规则BINARY

一、API

在程序中注册过 函数、聚合、排序规则的回调block实现,才可以在SQL中使用他们(排序规则使用单独的注册函数)
扩展API的生命周期是短暂的,他们是基于连接注册的且不存储在数据库中,需要确保应用程序加载了扩展API并在连接中注册

1.注册函数sqlite3_create_function()

int sqlite3_create_function(
    sqlite3 *cnx, /* connection handle */
    const char *zFunctionName, /* function/aggregate name in SQL */
    int nArg, /* number of arguments. -1 = unlimited. */
    int eTextRep, /* encoding (UTF8, 16, etc.) */
    void *pUserData, /* application data, passed to callback */
    void (*xFunc)(sqlite3_context*,int,sqlite3_value**),
    void (*xStep)(sqlite3_context*,int,sqlite3_value**),
    void (*xFinal)(sqlite3_context*)
);
  • 参数定义如下:
    • cnx:数据库连接句柄,函数和聚合的置顶连接。若要使用函数,必须在连接上注册
    • zFunctionName:将在SQL中调用的函数名称,长度在255字节以内
    • nArg:自定义函数的参数数量。SQLite将会强制函数格式保证参数都输入。-1是不限制参数数量
    • eTextRep:首选文本编码。比如:SQLITE_UTF8/SQLITE_UTF16/SQLITE_UTF16BE/SQLITE_UTF16LE/SQLITE_ANY
    • pUserData:应用程序数据。该数据可供在xFunc、xStep和xFinal指定的回调函数中使用。
    • xFunc:回调函数。这是SQL函数的真是实现。用户自定义函数只需要提供该回调,同时让xStep、xFinal函数指针未null。后两个回调函数是专门给用户自定义聚合实现使用的
    • xStep:聚合步骤函数。SQLite每次处理聚合结婚集中的一行都要调用xStep,以便聚合处理该行的相关字段值,并将其包含在聚合计算的结果中
    • xFinal:finalize聚合函数。处理完所有的行后,SQLite调用该函数进行整个聚合汇总处理,该干啥通常是由计算追终止和可选的情况不符组成

2.步骤函数

void fn(sqlite3_context* ctx, int nargs, sqlite3_value** values)

  • ctx 是 函数/聚合 的上下文。它保持特殊函数调用的实例状态,通过它可以活动squlite3_create_function()挺的应用程序数据参数(pUserData).
  • 可以通过sqlite3_user_data()获取用户数据:
void *sqlite3_user_data(sqlite3_context*);
  • 对函数而言,这些数据可以在左右的调用中共享,因此对函数调用的特定实例而言它不是唯一的
  • 同样的pUserData可以在给定函数的所有实例中传递或共享,但是聚合可以通过sqlite3_aggregate_context()为每个特定的实例分配状态:
void *sqlite3_aggregate_context(sqlite3_context*, int nBytes);
  • 第一次调用该函数时执行一个特殊的聚合函数,分配n字节内存,赋值为0,并与上下文关联。
  • 上下文环境汇总后面的调用(同一个聚合的实例)可以返回这些分配的内存
  • 使用该函数是,聚合可以在调用之间存储状态一遍堆积数据
  • 打枊聚合完成final()回调时,由SQLite自动释放内存。

注意: API始终以 void 指针形式使用用户数据。因为:API的很多不法会触发回调函数,在实现所谓的回调函数时,只是作为一种维护状态的便捷方法。

3.返回值

  • 参数values是sqlite3_value结构体数组,是SQLite实际参数值的句柄。这些值的实际数据可以使用sqlite3_value_xxx()系列函数获取:xxx sqlite3_value_xxx(sqlite3_value* value);
  • 比如获取标量值函数
int sqlite3_value_int(sqlite3_value*);
sqlite3_int64 sqlite3_value_int64(sqlite3_value*);
double sqlite3_value_double(sqlite3_value*);
  • 比如获取数组值函数
int sqlite3_value_bytes(sqlite3_value*);
const void *sqlite3_value_blob(sqlite3_value*);
const unsigned char *sqlite3_value_text(sqlite3_value*);
  • 函数 sqlite3_value_bytes()返回blob缓冲去中数据的长度
  • 函数sqlite3_value_blob()返回指向缓冲区的数据的指针
  • 有了空间和指针,就可以复制出数据。eg。:自定义函数的第一个参数是blob,复制数据如下:
int len;
void* data;
len = sqlite3_value_bytes(values[0]);
data = sqlite3_malloc(len);
memcpy(data, sqlite3_value_blob(values[0]), len);
  • 获取值类型sqlite3_value_type()
int sqlite3_value_type(sqlite3_value*);

#define SQLITE_INTEGER 1
#define SQLITE_FLOAT 2
#define SQLITE_TEXT 3
#define SQLITE_BLOB 4
#define SQLITE_NULL 5

二、函数

  • 举个栗子说明实现简单的函数hello_newman(),接收一个参数,访问人名
sqlite > select hello_newman('Jerry') as reply;
reply
------------------
Hello Jerry
sqlite > select hello_newman('Kramer') as reply;
reply
------------------
Hello Kramer
sqlite > select hello_newman('George') as reply;
reply
------------------
Hello George
  • 代码实现如下
int main(int argc, char **argv)
{
    int rc; sqlite3 *db;
    sqlite3_open_v2("test.db", &db);
    sqlite3_create_function( db, "hello_newman", 1, SQLITE_UTF8, NULL,
    hello_newman, NULL, NULL);
    /* Log SQL as it is executed. 执行时记录SQL日志 */
    log_sql(db,1);
    /* Call function with one text argument.传入1个文本参数调用函数 */
    fprintf(stdout, "Calling with one argument.\n");
    print_sql_result(db, "select hello_newman('Jerry')");
    /* Call function with two arguments. 传入两个参数调用函数(会失败,因为注册时只接收1个参数)
    ** It will fail as we registered the function as taking only one argument.*/
    fprintf(stdout, "\nCalling with two arguments.\n");
    print_sql_result(db, "select hello_newman ('Jerry', 'Elaine')");
    /* Call function with no arguments. This will fail too 不传参调用函数也失败 */
    fprintf(stdout, "\nCalling with no arguments.\n");
    print_sql_result(db, "select hello_newman()");
    /* Done */
    sqlite3_close(db);
    return 0;
}

void hello_newman(sqlite3_context* ctx, int nargs, sqlite3_value** values)
{
    const char *msg;
    /* Generate Newman's reply 生成 Newman的 回复 */
    msg = sqlite3_mprintf("Hello %s", sqlite3_value_text(values[0]));
    /* Set the return value. Have sqlite clean up msg w/ sqlite_free().
       设置返回值,用 sqlite3_free清理msg
     */
    sqlite3_result_text(ctx, msg, strlen(msg), sqlite3_free);
}
  • 运行结果:
Calling with one argument.
    TRACE: select hello_newman('Jerry')
hello_newman('Jerry')
---------------------
Hello Jerry
Calling with two arguments.
execute() Error: wrong number of arguments to function hello_newman()
Calling with no arguments.
execute() Error: wrong number of arguments to function hello_newman()
  • log_sql()简单的调用了sqlite3_trace(),在追踪函数中传递哪些带有TRACE的SQL追踪内容。记录SQL的执行
  • print_sql_result()的声明:int print_sql_result(sqlite3 *db, const char* sql, ...),是对sqlite3_prepare_v2()的简单封装,它将执行SQL预计并打印结果

1.返回值

  • sqlite3_result_text()sqlite3_result_xxx()系列之一,该系列函数是用户用来自定义函数和聚合返回值的。标量函数如下:
void sqlite3_result_double(sqlite3_context*, double);
void sqlite3_result_int(sqlite3_context*, int);
void sqlite3_result_int64(sqlite3_context*, long long int);
void sqlite3_result_null(sqlite3_context*);
  • 这些函数在函数名称中接收一个指定类型的参数(第二个参数),将其设置为函数的返回值。数组函数如下:
void sqlite3_result_text(sqlite3_context*, const char*, int n, void(*)(void*));
void sqlite3_result_blob(sqlite3_context*, const void*, int n, void(*)(void*));
  • 这些函数接收数组数据并将该数组设置为函数返回值
void sqlite3_result_xxx(
    sqlite3_context *ctx, /* function context */
    const xxx* value, /* array value */
    int len, /* array length */
    void(*free)(void*)); /* array cleanup function */
  • 这里xxx是特殊的数组类型(void代表blob,char代表text)

2.数组与内存清理器

  • 与在SQL预计中绑定数组只一样,这些函数与sqlite3_bind_xxx()的工作方式相同。他们需要一个指向数组的指针、一个数组的长度、指向清理函数的函数指针。清理函数的指针可以像sqlite3_bind_xxx()中的一样是预定义好的。
#define SQLITE_STATIC ((void(*)(void *))0)
#define SQLITE_TRANSIENT ((void(*)(void *))-1)
  • SQLITE_STATIC意味着数组内存驻留在非托管空间,因此SQLite不需要数据副本,也不会视图清理它。
  • SQLITE_TRANSIENT提示SQLite数组数据有可能改变,因此SQLite需要使用sqlite3_malloc()为自己复制一份数据。当函数返回时,分配的内存将会释放。
  • 第三选项是传递一个指向如下形式的清理函数的指针:void cleanup(void*);
  • 这种情况下,SQLite在洪湖自定义函数完成后,调用清理函数
  • 例子:我们用sqlite3_mprintf()生成前面定义的hello_newman()回复,该回复在堆上分配字符串。我们将sqlite3_free()作为清理函数传递给sqlite3_result_text(),这样当扩展函数完成时,可以释放字符串内存。

3.错误处理

  • 函数遇到错误时应该设置恰当的返回值。sqlite3_result_error()
void sqlite3_result_error(
    sqlite3_context *ctx, /* the function context */
    const char *msg, /* the error message */
    int len); /* length of the error message */
  • 它通知SQLite出现错误,详细信息在msg中。SQLite会终止调用函数并将错误信息包含在msg中

4.返回输入值

  • 场景:将输入的参数原样返回
  • sqlite3_column_xxx()获取值,sqlite3_result_xxx()设置返回值
void sqlite3_result_value(
    sqlite3_context *ctx, /* the function context */
    sqlite3_value* value); /* the argument value */
  • eg。 创建一个可以回显首个参数的echo():
void echo(sqlite3_context* ctx, int nargs, sqlite3_value** values)
{
    sqlite3_result_value(ctx, values[0]);
}

三、聚合

  • 自定义函数实现一个回调,聚合必须实现两个
    • 步骤函数计算进行汇总的值
    • finalize函数完成最终计算和清理工作
st=>start: select sum(id) from foo;
e=>end: sum(id)
op=>operation: sqlite_prepare()
op0=>operation: sqlite_step()
op1=>operation: xStep()
op2=>operation: xFinal()
cond=>condition: SQLITE_ROW

st->op->op0->cond
cond(yes)->op1->op0
cond(no)->op2->e

1.注册函数

int sqlite3_create_function(
    sqlite3 cnx*, /* connection handle */
    const char *zFunctionName, /* function/aggregate name in SQL */
    int nArg, /* number of arguments. -1 = unlimited. */
    int eTextRep, /* encoding (UTF8, 16, etc.) */
    void *pUserData, /* application data, passed to callback */
    void (*xFunc)(sqlite3_context*,int,sqlite3_value**),
    void (*xStep)(sqlite3_context*,int,sqlite3_value**),
    void (*xFinal)(sqlite3_context*)
);

2.实例

int main(int argc, char **argv)
{
    int rc; sqlite3 *db; char *sql;
    sqlite3_open_v2("foods.db", &db);
    /* Register aggregate. */
    fprintf(stdout, "Registering aggregate str_agg()\n");
    /* Turn SQL tracing on. */
    log_sql(db, 1);
    /* Register aggregate. */
    sqlite3_create_function( db, "str_agg", 1, SQLITE_UTF8, db,
    NULL, str_agg_step, str_agg_finalize);
    /* Test. */
    fprintf(stdout, "\nRunning query: \n");
    sql = "select season, str_agg(name, ', ') from episodes group by season";
    print_sql_result(db, sql);
    sqlite3_close(db);
    return 0;
}
  • 注意,步骤函数str_agg_step(),finalize函数str_agg_finalize()
  • 该聚合函数注册成只接收一个参数

3.步骤函数

void str_agg_step(sqlite3_context* ctx, int ncols, sqlite3_value** values)
{
    SAggCtx *p = (SAggCtx *) sqlite3_aggregate_context(ctx, sizeof(*p));
    static const char delim [] = ", ";
    char *txt = sqlite3_value_text(values[0]);
    int len = strlen(txt);
    if (!p->result) {
        p->result = sqlite_malloc(len + 1);
        memcpy(p->result, txt, len + 1);
        p->chrCnt = len;
    } else {
        const int delimLen = sizeof(delim);
        p->result = sqlite_realloc(p->result, p->chrCnt + len + delimLen + 1);
        memcpy(p->result + p->chrCnt, delim, delimLen);
        p->chrCnt += delimLen;
        memcpy(p->result + p->chrCnt, txt, len + 1);
        p->chrCnt += len;
    }
}


// SAggCtx是本例中的结构体
typedef struct SAggCtx SAggCtx;
struct SAggCtx {
    int chrCnt;
    char *result;
};

  • SAggCtx充当聚合迭代(对步骤函数的调用)之间的状态,维持不断增长的拼接文本和字符数

4.聚合上下文

  • 通过sqlite3_aggregate_context()为给定的聚合实例获取结构体
  • sqlite3_aggregate_context()首次调用会为该上下文分配数据,之后调用直接获取
  • 聚合完成后(str_agg_finalize()后),结构体内存自动释放
  • 注意:若系统内存不足,sqlite3_aggregate_context()可能返回null,本例中没处理这种异常,实际开发必须处理
  • 若返回null,需触发sqlite3_resut_error_nomem()
  • str_agg_step()中,可以用sqlite3_value_text()从首个参数获取被聚合的文本值,然后将其谈价到结构体SAggCtx的result成员中,该及饿哦固体中介拼接字符串
  • 注意2:若SQL参数是null,or内存不足,sqlite3_value_text()可能返回null,需要检查处理

5.Finalize函数

  • 每次显示结果的记录都会触发str_agg_step()调用
  • 所有记录完事后,SQLite调用str_agg_finalize()
void str_agg_finalize(sqlite3_context* ctx)
{
    SAggCtx *p = (SAggCtx *) sqlite3_aggregate_context(ctx, sizeof(*p));
    if( p && p->result ) sqlite3_result_text(ctx, p->result, p->chrCnt, sqlite_free);
}
  • str_agg_finalize()像用户滴定仪的回调函数一样工作。它获取及饿哦固体sAggctx,若该结构体存在,通过sqlite3_result_text()将聚合的返回值设置为结构体中存储的成员result
  • 若内存不足,or未调用步骤函数,p->zResult 可能为null,SQLite会返回错误
  • 若内存不足,or未调用步骤函数,sqlite3_result_text()可能返回null,聚合结构体也会是null

6.结果

Registering aggregate str_agg()
Running query:
TRACE: select season, str_agg(name, ', ') from episodes group by season

season str_agg(name, ', ')


0 Good News Bad News
1 Male Unbonding, The Stake Out, The Robbery, The Stock Tip
2 The Ex-Girlfriend, The Pony Remark, The Busboy, The Baby Shower, …
3 The Note, The Truth, The Dog, The Library, The Pen, The Parking Garage …

四、排序规则(Collations)

  • 单词是校对的意思,这里翻译成了排序规则,我挺疑惑的
  • 排序的目的:对事物进行分类比较。
  • SQLite对结果集中的字段进行排序时(SQLite使用比较操作符,如:< or >=, 在字段内对值进行比较)
  • 首先根据存储类对字段值进行排列
  • 在每种存储类中,根据该类指定的方法进行排序
1. NULL values
2. INTEGER and REAL values
3. TEXT values
4. BLOB values
  • SQLite 对每种存储类使用对应的排序算法
  • NULL没有排序
  • 数字按照大小进行排序
  • BLOB按照二进制进行排序
  • 最后,文本是我们要讨论的排序规则的重点

1.排序规则定义(Collation Defined)

  • 排序规则是比较字符串的方法,定义如下
  • 与逐个byte比较数字代码相反,排序规则使用语言敏感的规则对文本进行比较
  • 通常,排序规则必须处理字符串和字符的 排序、比较、排列
  • 排序规则通常使用 排序序列
  • 排序序列是一局拍好像的字符串列表,通常都有数字编码,以便记忆和使用
  • 字符串列表是用来判断字符是如何排序、比较以及分类的
  • 给定俩字符,排序规则必须判断出那个在前or相等
1.1 排序规则原理(How Collation Works)
  • 排序方法对将要比较的两个字符串进行切分,共同管排序序列逐一比较每个字符。从左到右进行比较 eg。 jerry与jello进行比较


    378D1ACC-3582-4C40-AB9F-FD36C989AD6A.png
  • 本例排序规则是ASCII字符集,图中显示了每个字符的ASCII值,使用刚描述的排序规则,字符串比较到第三个字符时,jerry胜出114>108,jerry>jello
  • 若jerry变Jerry,那么jello胜出,因为J是74,74<106
1.2 标准排序规则类型(Standard Collation Types)
  • SQLite中唯一内置的排序规则binary使用标准的C函数库memcmp()比较两个字符串。binary排序规则对于英文文本使用很好。但对于其它语言,可能需要其它的排序规则。SQLite还提供了另一种排序规则nocase,它是大小写不敏感的。
  • 排序规则是通过表字段or索引定义以及在查询中指定的方法和字段关联起来的。例如,在foo中创建大小写不敏感的bar字段,使用如下定义create table foo(bar text collate nocase, baz integer);
  • 从那时起,SQLite处理bar字段时,就用使用nocase排序规则。如不想将排序规则附加到数据库对象上,而是在需要时在查询中指定,可以直接在查询中指定他们,如:select * from foo order by bar collate nocase;
  • SQLite 也可以对cast()表达式的结果应用排序规则,cast()表达式可以将非文本数据转文本数据。我们可以将bar数字值转文本,并像下面这样应用nocase排序:select cast(baz) collate nocase as baz_as text from foo order by baz_as text;

2.简单例子

  • 实现 length_first 排序规则,接收俩字符串,根据长度排序;比如在店里吃名字短的食物
  • shlite3_create_collation_v2()注册排序规则
int sqlite3_create_collation_v2(
    sqlite3* db, /* database handle */
    const char *zName, /* collation name in SQL */
    int pref16, /* encoding */
    void* pUserData, /* application data */
    int(*xCompare)(void*,int,const void*,int,const void*)
    void(*xDestroy)(void*)
);
  • sqlite3_create_function()相似。第一个参数是标准的数据库句柄,第二个是在SQL中使用的排序规则名称(与函数、聚合名称类似),然后编码方式(编码方式从属于传递给比较函数时被比较字符串的格式)
  • pUserData是另一个API的主要支撑,该真真传递给回调函数。函数sqlite3_create_collation_v2()基本上是删减版的sqlite3_create_function()
    • sqlite3_create_function()输入俩文本参数,返回一个int
    • sqlite3_create_collation_v2()去掉了聚合回调函数指针
2.1 Compare函数

参数xCompare指向实际解析文本值的比较函数。比较函数格式如下:

int compare( void* data, /* application data */
    int len1, /* length of string 1 */
    const void* str1, /* string 1 */
    int len2, /* length of string 2 */
    const void* str2) /* string 2 */
  • 字符串关系 < 返回负值,=返回0,>返回正值
int length_first_collation( void* data, int l1, const void* s1,
    int l2, const void* s2 )
    {
    int result, opinion;
    /* Compare lengths */
    if ( l1 == l2 ) result = 0;
    if ( l1 < l2 ) result = 1;
    if ( l1 > l2 ) result = 2;
    /* Form an opinion: is s1 really < or = to s2 ? */
    switch(result) {
        case 0: /* Equal length, collate alphabetically */
        opinion = strcmp(s1,s2);
        break;
        case 1: /* String s1 is shorter */
        opinion = -result;
        break;
        case 2: /* String s2 is shorter */
        opinion = result
        break;
        default: /* Assume equal length just in case */
        opinion = strcmp(s1,s2);
    }
    return opinion;
}
2.2 测试程序
int main(int argc, char **argv)
{
    char *sql; sqlite3 *db; int rc;
    sqlite3_open("foods.db", &db);
    /* Register Collation. */
    fprintf(stdout, "1. Register length_first Collation\n\n");
    sqlite3_create_collation_v2( db, "LENGTH_FIRST", SQLITE_UTF8, db,
    length_first_collation, length_first_collation_del );

    /* Turn SQL logging on. */
    log_sql(db, 1);
    /* Test default collation. */
    fprintf(stdout, "2. Select records using default collation.\n");
    sql = "select name from foods order by name";
    print_sql_result(db, sql);
    /* Test Length First collation. */
    fprintf(stdout, "\nSelect records using length_first collation. \n");
    sql = "select name from foods order by name collate LENGTH_FIRST";
    print_sql_result(db, sql);
    /* Done. */
    sqlite3_close(db);
    return 0;
}

2.3 结果
======================================================
1. Register length_first Collation
2. Select records using default collation.
TRACE: select name from foods order by name
name
-----------------
A1 Sauce
All Day Sucker
Almond Joy
Apple
Apple Cider
Apple Pie
Arabian Mocha Java (beans)
Arby's Roast Beef
Artichokes
Atomic Sub
...
Select records using length_first collation.
TRACE: select name from foods order by name collate LENGTH_FIRST
issue
-----------------
BLT
Gum
Kix
Pez
Pie
Tea
Bran
Duck
Dill
Life
...

======================================================

3.按需排序

  • SQLite提供一种知道实际需要时才注册排序的懒加载方式。
  • 若不确定您的 程序是否需要一些想length_first的排序规则,可以用sqlite3_collation_needed(),将注册延时到使用时
  • 只需提供回调函数,SQLite会在需要时注册未知排序规则,并给定名称

总结

  • 使用扩展C API 向SQLite核心库添加这些自定义函数是对SQLite核心库的重要补充。因此,您可以深挖和修改SQLite的核心内容,甚至可以说SQLite提供了友好的、易于使用的、可以更大范围扩展和定制自身的用户接口,特别是和SQLite汇总已有功能结合时
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容