【譯】教你用16個小時從0構(gòu)建一個Rust應(yīng)用

我們在2019年的最后兩天,參加了Prodigy Education舉辦的黑客馬拉松,許多團隊聚在一起努力將他們的想法變成現(xiàn)實砰粹。

我們之中有的人只是單純?yōu)榱撕猛妫械氖窍雽W(xué)一些新的知識牛隅,還有些人可能是想證明一些概念或想法。

我在過去幾周總是被動的獲取Rust相關(guān)信息或使用Rust的代碼,因此我認(rèn)為hackathon是一次學(xué)習(xí)Rust的絕佳時機。

hackathon的時間緊迫性使我更加快速的去學(xué)習(xí)种玛,同時也會去解決現(xiàn)實世界的一些問題。

為什么是Rust

Getting a chance to peek under the hood again

在我職業(yè)生涯的前10年中瓤檐,有8年都在使用C和C++赂韵。

從好的方面來講,我喜歡像C++這樣可以提供靜態(tài)類型的語言距帅,因為它能在編譯期就能夠早早的發(fā)現(xiàn)錯誤右锨。

我個人對于C++的一些看法是:

  • 工程師很容易搬起石頭砸自己的腳
  • 作為一門編程語言括堤,它已經(jīng)非常臃腫且復(fù)雜
  • 缺乏良好的碌秸、標(biāo)準(zhǔn)的廣泛適用的包管理系統(tǒng)

自從我改做Web應(yīng)用以來绍移,一直是做Python和JavaScript開發(fā),使用像Django讥电、Flask和Express這樣的框架蹂窖。

到目前為止,我在Python和JavaScript中的開發(fā)經(jīng)驗是恩敌,它們可以提供良好的程序迭代和交付速度瞬测,但有時會占用大量的CPU和內(nèi)存,即使服務(wù)是相對空閑的纠炮。

我經(jīng)常發(fā)現(xiàn)自己寫好的C++程序月趟,會缺失一些安全性、速度和精簡性恢口。

我想要尋找一種像Rust這樣精簡的孝宗、裸機編程語言來開發(fā)web應(yīng)用。

沒有運行時耕肩,沒有垃圾回收因妇。直接加載二進(jìn)制代碼,交給內(nèi)核執(zhí)行猿诸。

目標(biāo)

我的目標(biāo)是完成一個后端由Rust編寫婚被,前端是JavaScript+React完成的類似于S3作為圖床的應(yīng)用程序,用戶可以做以下事情:

  • 瀏覽圖床中所有的圖片(分頁可選)
  • 上傳圖片
  • 上傳圖片時可以給圖片增加標(biāo)簽
  • 通過名稱進(jìn)行查詢或過濾

所有有趣的hackathon項目都有一個名字梳虽,所以我決定將這個項目命名為:

RustIC -> Rust + Image Contents

Let’s hack something great

我認(rèn)為如果我做到了以下這些事情址芯,那么這次hackathon之行對我個人來說就是成功的:

  • 對Rust有一個基本的理解,包括它的類型系統(tǒng)和內(nèi)存模型
  • 探索S3的對于文件和任意標(biāo)簽的預(yù)簽名鏈接功能
  • 寫出一個可以驗證的功能正常的應(yīng)用

由于我的主要目標(biāo)是開發(fā)功能窜觉,同時兼顧學(xué)習(xí)是复。很多代碼是我一邊學(xué)一邊寫的,所以代碼組織和效率可能并不是最理想的竖螃,因為這些屬于次要目標(biāo)淑廊。

Rust的原則

在我開始之前,我?guī)е闷嫘娜チ私饬艘獙W(xué)習(xí)的語言的設(shè)計師在創(chuàng)建這門語言時內(nèi)心的原則是什么特咆。我找到了一個簡化版本和一個詳細(xì)版本季惩。

與我在許多博客上讀到的內(nèi)容相反,Rust是有可能發(fā)生內(nèi)存泄露(循環(huán)引用)和之行不安全的操作(unsafe代碼塊中)的腻格,詳細(xì)描述在上面的FAQ中画拾。

“We [the language creators] do not intend [for Rust] to be 100% static, 100% safe, 100% reflective.”

Dazzling, intricate, sophisticated

從后端開始

Google搜索“Rust web framework“,排在最前面的是Rocket菜职。我進(jìn)入這個網(wǎng)站青抛,發(fā)現(xiàn)文檔的示例都一目了然。

有一點需要注意的是Rocket需要Rust的nightly版本酬核,不過在hackathon上這都是小問題蜜另。

GitHub的代碼庫中有著非常豐富的例子适室。完美!

我使用Cargo創(chuàng)建了一個新的項目举瑰,在TOML文件中加入了Rocket依賴捣辆,然后跟著Rocket的入門指南,寫了第一段代碼:

#[get("/")]
fn index() -> &'static str {
    "Hello, world!"
}

fn main() {
    rocket::ignite().mount("/", routes![index]).launch();
}

對于熟悉Django此迅、Flask汽畴、Express等框架等同學(xué)來說,這段代碼讀起來非常容易耸序。作為一名Rocket用戶忍些,你可以使用宏作為裝飾器來將路由映射到對應(yīng)的處理函數(shù)上。

在編譯時坎怪,宏將被擴展坐昙。這對開發(fā)者是完全透明的。如果你想看擴展后的代碼芋忿,可以使用cargo-expand炸客。

以下是我在構(gòu)建Rust應(yīng)用程序時的一些有趣的或者有挑戰(zhàn)性的亮點:

指定路由響應(yīng)

我想要以JSON的數(shù)據(jù)格式返回S3中所有的文件列表。

你可以看到路由關(guān)聯(lián)的處理函數(shù)的代碼決定了響應(yīng)類型戈钢。

設(shè)置響應(yīng)結(jié)構(gòu)非常容易痹仙,如果你想要返回JSON格式的數(shù)據(jù),并且每個字段都有自己的結(jié)構(gòu)和類型殉了,那對應(yīng)的就是Rust的struct开仰。

所以你應(yīng)該先定義一個結(jié)構(gòu)體struct(S)來接受響應(yīng),并且需要進(jìn)行標(biāo)注:

#[derive(Serialize)]

struct(s)被標(biāo)記了#[derive(Serialize)]薪铜,因此可以通過`rocket_contrib::json::Json將它轉(zhuǎn)換成JSON众弓。

#[derive(Serialize)]
struct BucketContents {
    data: Vec<S3Object>,
}

#[derive(Serialize)]
struct S3Object {
    file_name: String,
    presigned_url: String,
    tags: String,
    e_tag: String, // AWS generated MD5 checksum hash for object
    is_filtered: bool,
}

#[get("/contents?<filter>")]
fn get_bucket_contents(
    filter: Option<&RawStr>
) -> Result<Json<BucketContents>, Custom<String>> {
    // Returns either Ok(Json(BucketContents)) or,
    // a Custom error with a reason
}

處理分段上傳

當(dāng)我意識到我的前端很有可能使用POST方法上傳格式為multipart/form-data的表單數(shù)據(jù)時,我就開始深入研究如何使用Rocket來構(gòu)建程序了隔箍。

不幸的是谓娃,Rocket0.4版本不支持multipart,看起來在0.5版本會支持蜒滩。

這意味著我需要使用multipart crate并集成到Rocket中滨达。最終代碼可以正常運行,但是如果Rocket支持multipart將會使代碼更加簡潔俯艰。

#[post("/upload", data = "<data>")]
// signature requires the request to have a `Content-Type`. The preferred way to handle the incoming
// data would have been to use the FromForm trait as described here: https://rocket.rs/v0.4/guide/requests/#forms
// Unfortunately, file uploads are not supported through that mechanism since a file upload is performed as a
// multipart upload, and Rocket does not currently (As of v0.4) support this. 
// https://github.com/SergioBenitez/Rocket/issues/106
fn upload_file(cont_type: &ContentType, data: Data) -> Result<Custom<String>, Custom<String>> {
    // this and the next check can be implemented as a request guard but it seems like just
    // more boilerplate than necessary
    if !cont_type.is_form_data() {
        return Err(Custom(
            Status::BadRequest,
            "Content-Type not multipart/form-data".into()
        ));
    }

    let (_, boundary) = cont_type.params()
                                 .find(|&(k, _)| k == "boundary")
                                 .ok_or_else(
        || Custom(
            Status::BadRequest,
            "`Content-Type: multipart/form-data` boundary param not provided".into()
        )
    )?;

    // The hot mess that ensues is some weird combination of the two links that follow
    // and a LOT of hackery to move data between closures.
    // https://github.com/SergioBenitez/Rocket/issues/106
    // https://github.com/abonander/multipart/blob/master/examples/rocket.rs
    let mut d = Vec::new();
    data.stream_to(&mut d).expect("Unable to read");
    let mut mp = Multipart::with_body(Cursor::new(d), boundary);

    let mut file_name = String::new();
    let mut categories_string = String::new();
    let mut raw_file_data = Vec::new();

    mp.foreach_entry(|mut entry| {
        if *entry.headers.name == *"fileName" { 
            let file_name_vec = entry.data.fill_buf().unwrap().to_owned();
            file_name = from_utf8(&file_name_vec).unwrap().to_string()
        } else if *entry.headers.name == *"tags" {
            let tags_vec = entry.data.fill_buf().unwrap().to_owned();
            categories_string = from_utf8(&tags_vec).unwrap().to_string();
        } else if *entry.headers.name == *"file" {
            raw_file_data = entry.data.fill_buf().unwrap().to_owned()
        }
    }).expect("Unable to iterate");

    let s3_file_manager = s3_interface::S3FileManager::new(None, None, None, None);
    s3_file_manager.put_file_in_bucket(file_name.clone(), raw_file_data);

    let tag_name_val_pairs = vec![("tags".to_string(), categories_string)];
    s3_file_manager.put_tags_on_file(file_name, tag_name_val_pairs);

    return Ok(
        Custom(Status::Ok, "Image Uploaded".to_string())
    );
}

配置CORS

路由寫好了以后捡遍,我就開始用curl或Postman來進(jìn)行測試了,現(xiàn)在已經(jīng)是時候開始把前端集成進(jìn)來了竹握。我需要適當(dāng)設(shè)置響應(yīng)頭以避免跨域問題画株。

Rocket依舊沒有支持這個特性。

然后我在GitHub代碼庫中找到了一些解決方案:

// CORS Solution below comes from: https://github.com/SergioBenitez/Rocket/issues/25
extern crate rocket;

use std::io::Cursor;
use rocket::fairing::{Fairing, Info, Kind};
use rocket::{Request, Response};
use rocket::http::{Header, ContentType, Method};

struct CORS();

impl Fairing for CORS {
    fn info(&self) -> Info {
        Info {
            name: "Add CORS headers to requests",
            kind: Kind::Response
        }
    }

    fn on_response(&self, request: &Request, response: &mut Response) {
        if request.method() == Method::Options || 
           response.content_type() == Some(ContentType::JSON) || 
           response.content_type() == Some(ContentType::Plain) {

            response.set_header(Header::new("Access-Control-Allow-Origin", "http://localhost:3000"));
            response.set_header(Header::new("Access-Control-Allow-Methods", "POST, GET, OPTIONS"));
            response.set_header(Header::new("Access-Control-Allow-Headers", "Content-Type"));
            response.set_header(Header::new("Access-Control-Allow-Credentials", "true"));
        }

        if request.method() == Method::Options {
            response.set_header(ContentType::Plain);
            response.set_sized_body(Cursor::new(""));
        }
    }
}

fn main() {
    
    rocket::ignite().attach(
        CORS()
    ).mount(
        "/", 
        routes![get_bucket_contents, upload_file]
    ).launch();
}

過了一會,我發(fā)現(xiàn)了rocket_cors谓传,它幫助我大幅縮減了代碼量蜈项。

fn main() -> Result<(), Error> {
    let allowed_origins = AllowedOrigins::some_exact(&["http://localhost:3000"]);

    let cors = rocket_cors::CorsOptions {
        allowed_origins,
        allowed_methods: vec![Method::Get, Method::Post].into_iter().map(From::from).collect(),
        allowed_headers: AllowedHeaders::some(&["Content-Type", "Authorization", "Accept"]),
        allow_credentials: true,
        ..Default::default()
    }
    .to_cors()?;


    rocket::ignite().attach(cors)
                    .mount("/", routes![get_bucket_contents, upload_file])
                    .launch();

    Ok(())
}

運行起來

我們只需要一個簡單的cargo run命令就可以讓程序運行起來

output

我機器上的活動監(jiān)視器告訴我這個程序正在運行中,并且只消耗了2.7MB內(nèi)存良拼。

而且這還只是沒有經(jīng)過優(yōu)化的調(diào)試版本。項目使用- release標(biāo)簽打包的話充边,運行時只需要1.6MB內(nèi)存庸推。

memory

基于Rust的后端服務(wù)器,我們請求/contents這個路由會得到如下響應(yīng):

{
    "data": [
        {
            "file_name": "Duck.gif",
            "presigned_url": "https://s3.amazonaws.com/rustic-images/Duck.gif?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIARDWJNDW3U8329UDNJ%2F20200107%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20200107T050353Z&X-Amz-Expires=1800&X-Amz-Signature=1369c003b2f54510882bf9982ab56d024d6c9d2655a4d86f8907313c7499b56d&X-Amz-SignedHeaders=host",
            "tags": "animal",
            "e_tag": "\"93c570cadd6b8b2f85b47c2f14fd82a1\"",
            "is_filtered": false
        },
        {
            "file_name": "GIZMO.png",
            "presigned_url": "https://s3.amazonaws.com/rustic-images/GIZMO.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIARDWJNDW3U8329UDNJ%2F20200107%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20200107T050353Z&X-Amz-Expires=1800&X-Amz-Signature=040e76c2df5a9a54ed4fbc8490378cf732b32bae78f628448536fc610018c0c3&X-Amz-SignedHeaders=host",
            "tags": "robots",
            "e_tag": "\"2cde221a0c7a72c0a7a60cffce29a0bc\"",
            "is_filtered": false
        },
        {
            "file_name": "GreenSmile.gif",
            "presigned_url": "https://s3.amazonaws.com/rustic-images/GreenSmile.gif?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIARDWJNDW3U8329UDNJ%2F20200107%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20200107T050354Z&X-Amz-Expires=1800&X-Amz-Signature=d115b107de530ce15b3590abdbab355c2a9481a81131f88bf4ad2a59ca11bbac&X-Amz-SignedHeaders=host",
            "tags": "smile-face",
            "e_tag": "\"86854a599540f50bdc5e837d30ca34f9\"",
            "is_filtered": false
        }
    ]
}

前端的工作相對簡單一些浇冰,我們使用的是:

  • React
  • React Bootstrap
  • react-grid-gallery
  • react-tags-input

用戶可以在我們的頁面瀏覽圖片贬媒,也可以通過文件名或標(biāo)簽來進(jìn)行檢索或過濾。

images

用戶還可以通過拖拽來上傳文件肘习,并且可以在提交上傳之前打上標(biāo)簽际乘。

upload

我喜歡使用Rust構(gòu)建應(yīng)用程序的原因

  • Cargo對于依賴和應(yīng)用管理的程度簡直令人驚嘆
  • 編譯器對于我們處理編譯錯誤幫助非常大,有位博主在博客中描述了他是如何按照編譯器大指導(dǎo)來寫代碼的漂佩。我的經(jīng)驗也比較類似脖含。
  • 我需要的每一項功能都有crate,這讓我感到非常驚喜
Crates galore on crates.io
  • 在線的Rust Playground投蝉,讓我可以運行小的代碼片段养葵。
  • Rust語言服務(wù)器,已經(jīng)很好的集成到了Visual Studio Code瘩缆,它能夠提供實時錯誤檢查关拒、格式設(shè)置、符號查找等庸娱。這讓我可以在幾個小時內(nèi)不編譯就能取得不錯的進(jìn)展着绊。

不便、驚喜和麻煩

盡管Rust的文檔很棒熟尉,但我不得不依賴一些crates的文檔和例子归露。有些crates有很棒的集成測試,提供了一些關(guān)于如何使用的提示斤儿。當(dāng)然了靶擦,Stack Overflow和Reddit也給我提供了很多幫助。

“Where’s the documentation?”

另外還要注意的是:

  • 理解所有權(quán)雇毫、生命周期和所有權(quán)借用會使學(xué)習(xí)難度陡增玄捕,特別是在為期兩天的黑客馬拉松中努力提供功能時。我將它們與C++做比較并且弄清楚棚放,但有時還是會感到困惑枚粘。
  • 在所有的事情中,Strings攔住了我?guī)追昼娖牵貏e是String&str的區(qū)別更是令人困惑——直到我花了些時間來理解所有權(quán)馍迄、生命周期和所有權(quán)借用才搞清楚這些福也。

其他的一些觀察

  • Rust中沒有真正意義上的null類型,通常情況下攀圈,空值需要用Option類型的None來表示
  • 模式匹配非常棒暴凑,這是我在Scala中最喜歡的一個特性,在Rust中也一樣赘来。這種代碼看起來表現(xiàn)力很強现喳,并且允許編譯器標(biāo)記未處理的情況。
match bucket_contents {
    Err(why) => match why {
        S3ObjectError::FileWithNoName => Err(Custom(
            Status::InternalServerError,
            "Encountered bucket objects with no name".into()
        )),
        S3ObjectError::MultipleTagsWithSameName => Err(Custom(
            Status::InternalServerError,
            "Encountered a file with a more than one tag named 'tags'".into()
        ))
    },
    Ok(s3_objects) => {
        let visible_s3_objects: Vec<S3Object> = s3_objects.into_iter()
                                                          .filter(|obj| !obj.is_hidden())
                                                          .collect();
        Ok(Json(BucketContents::new(visible_s3_objects)))
    }
}
  • 說起安全和不安全模式犬辰,你仍然可以進(jìn)行更底層的編程嗦篱,比如說在不安全的模式下可以和C語言代碼通過接口交互。盡管Rust中有很多正確性檢查幌缝,但你仍然可以在不安全模塊中做一些騷操作灸促,例如解引用。讀代碼的人也可以從不安全模塊中獲取到很多信息涵卵。
  • 通過Box在堆中分配內(nèi)存空間浴栽,而不是newdelete。剛開始感覺比較奇怪轿偎,但是也很容易理解吃度。標(biāo)準(zhǔn)庫中還定義了其他的一些智能指針,如果你需要使用引用數(shù)量或者弱引用時就可以直接使用贴硫。
  • Rust中的異常也很有趣椿每,因為它沒有異常。你可以選擇使用Result<T, E>表示可以恢復(fù)的錯誤英遭,也可以用panic!宏表示不可恢復(fù)的錯誤间护。
// This code:
// 1. Takes a vector of objects representing S3 contents
// 2. Uses filter to remove entries we don't care about
// 3. Uses map to transform each object into another type, but terminates iteration
// .  if the lambda passed to map returns an Err. 
// 4. If all iterations produced an Ok(S3Object) result, these are collected into a Vec<S3Object>
let bucket_contents: Result<Vec<S3Object>, S3ObjectError> = bucket_list
        .into_iter()
        .filter(|bucket_obj| bucket_obj.size.unwrap_or(0) != 0) // Eliminate folders
        .map(|bucket_obj| {
            if let None = bucket_obj.key {
                return Err(S3ObjectError::FileWithNoName);
            }

            let file_name = bucket_obj.key.unwrap();
            let e_tag = bucket_obj.e_tag.unwrap_or(String::new());
            let tag_req_output = s3_file_manager.get_tags_on_file(file_name.clone());
            let tags_with_categories: Vec<Tag> = tag_req_output.into_iter()
                                                            .filter(|tag| tag.key == "tags")
                                                            .collect();
            if tags_with_categories.len() > 1 {
                return Err(S3ObjectError::MultipleTagsWithSameName);
            }

            let tag_value = if tags_with_categories.len() == 0 {
                "".to_string()
            } else {
                tags_with_categories[0].value.clone()
            };

            let presigned_url = s3_file_manager.get_presigned_url_for_file(
                file_name.clone()
            );
            Ok(S3Object::new(
                file_name,
                e_tag,
                tag_value,
                presigned_url,
                false,
            ))
        })
        .collect();

手冊中是這樣描述的:

在多數(shù)情況下,Rust需要你盡可能了解錯誤挖诸,并且在編譯之前對其做出相應(yīng)的處理汁尺。這個需求使你的程序更加健壯,保證你在發(fā)布之前就可以發(fā)現(xiàn)并處理其中的錯誤多律。

要點和教訓(xùn)

  • John Carmack曾經(jīng)將編寫Rust的經(jīng)歷描述為“非常有益”痴突。我同意這種感受,這次hackathon給我的感覺就像是打開了一扇新世界的大門并且發(fā)現(xiàn)了很多新鮮事物狼荞,這些收獲絕不僅僅是停留在代碼層面的辽装。
  • 事后看來,我應(yīng)該更加嚴(yán)謹(jǐn)?shù)倪x擇網(wǎng)絡(luò)框架的相味。再多想一下的話拾积,我可能會走出一條不同的道路。我下次可能會選擇ironactix-web, 或者是 tiny-http拓巧。
  • 我只學(xué)到了Rust的皮毛斯碌,16個小時是不可能完全成為一名Rustacean的,即使我對這門語言充滿了好奇心肛度,也做了一些深入的了解傻唾。我對Rust的未來感到興奮,我認(rèn)為它為構(gòu)建應(yīng)用程序帶來了很多規(guī)范承耿,它是一種表現(xiàn)力非常豐富的語言冠骄,并且能為我們提供與C++性能相當(dāng)?shù)倪\行速度和內(nèi)存性能呢。

資源

RustIC后端代碼

RustIC前端代碼

Rusoto:一個Rust的AWS SDK

原文鏈接

https://medium.com/better-programming/learning-to-use-rust-over-a-16-hour-hackathon-5f0ac2f604df

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末瘩绒,一起剝皮案震驚了整個濱河市猴抹,隨后出現(xiàn)的幾起案子带族,更是在濱河造成了極大的恐慌锁荔,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蝙砌,死亡現(xiàn)場離奇詭異阳堕,居然都是意外死亡,警方通過查閱死者的電腦和手機择克,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進(jìn)店門恬总,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人肚邢,你說我怎么就攤上這事壹堰。” “怎么了骡湖?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵贱纠,是天一觀的道長。 經(jīng)常有香客問我响蕴,道長谆焊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任浦夷,我火速辦了婚禮辖试,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘劈狐。我一直安慰自己罐孝,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布肥缔。 她就那樣靜靜地躺著肾档,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上怒见,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天俗慈,我揣著相機與錄音,去河邊找鬼遣耍。 笑死闺阱,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的舵变。 我是一名探鬼主播酣溃,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼纪隙!你這毒婦竟也來了赊豌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤绵咱,失蹤者是張志新(化名)和其女友劉穎碘饼,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體悲伶,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡艾恼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了麸锉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钠绍。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖花沉,靈堂內(nèi)的尸體忽然破棺而出柳爽,到底是詐尸還是另有隱情,我是刑警寧澤碱屁,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布磷脯,位于F島的核電站,受9級特大地震影響忽媒,放射性物質(zhì)發(fā)生泄漏争拐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一晦雨、第九天 我趴在偏房一處隱蔽的房頂上張望架曹。 院中可真熱鬧,春花似錦闹瞧、人聲如沸绑雄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽万牺。三九已至罗珍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間脚粟,已是汗流浹背覆旱。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留核无,地道東北人扣唱。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像团南,于是被迫代替她去往敵國和親噪沙。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內(nèi)容