从HTTP原理到HTTP网络组件封装

HTTP所在的协议层

TCP:IP参考模型.jpg

HTTP工作过程

一次HTTP操作称为一个事务,其工作整个过程如下:

地址解析

如用客户端浏览器请求这个页面:http://localhost:8080/info,从中分解出协议名、主机名、端口、对象路径等部分,对于我们的这个地址,解析得到的结果如下:

协议名:http 主机名:localhost 端口:8080 对象路径:/info

在这一步,需要域名系统DNS解析域名localhost,得主机的IP地址。

封装HTTP请求数据包

Header

常见的媒体格式类型如下:

    text/html : HTML格式
    text/plain :纯文本格式      
    text/xml :  XML格式
    image/gif :gif图片格式    
    image/jpeg :jpg图片格式 
    image/png:png图片格式

以application开头的媒体格式类型:

   application/xhtml+xml :XHTML格式
   application/xml     : XML数据格式
   application/atom+xml  :Atom XML聚合格式    
   application/json    : JSON数据格式
   application/pdf       :pdf格式  
   application/msword  : Word文档格式
   application/octet-stream : 二进制流数据(如常见的文件下载)
   application/x-www-form-urlencoded : form表单数据被编码为key/value格式(default表单提交)
   multipart/form-data : 需要在表单中进行文件上传时,就需要使用该格式

请求格式

  • Get
GET /plaintext?name=Shawn HTTP/1.1
Host: localhost:8080
User-Agent: ZHHTTP 0.1.0 rv:1 (iPhone; iOS 10.3.1; en_US)
Connection: keep-alive
Accept-Encoding: gzip
  • Post表单
POST /postName HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded; charset=utf-8
User-Agent: ZHHTTP 0.1.0 rv:1 (iPhone; iOS 10.3.1; en_US)
Content-Length: 16
Accept-Encoding: gzip
Connection: close

name=Shawn&sex=1
  • Post上传
    上传时,传了一个参数key:name, value:Shawn,一个文件upload0.txt,一个data数据块。
POST /upload HTTP/1.1
Host: localhost:8080
Content-Type: multipart/form-data; charset=utf-8; boundary=0xKhTmLbOuNdArY-089BC7C0-F1B0-44F6-B59C-6C86BDFED87A
User-Agent: ZHHTTP 0.1.0 rv:1 (iPhone; iOS 10.3.1; en_US)
Content-Length: 500
Accept-Encoding: gzip
Connection: close

--0xKhTmLbOuNdArY-089BC7C0-F1B0-44F6-B59C-6C86BDFED87A
Content-Disposition: form-data; name="name"

Shawn
--0xKhTmLbOuNdArY-089BC7C0-F1B0-44F6-B59C-6C86BDFED87A
Content-Disposition: form-data; name="data0"; filename="upload0.txt"
Content-Type: text/plain

Shawn0

--0xKhTmLbOuNdArY-089BC7C0-F1B0-44F6-B59C-6C86BDFED87A
Content-Disposition: form-data; name="data1"; filename="file"
Content-Type: application/octet-stream

Shawn1
--0xKhTmLbOuNdArY-089BC7C0-F1B0-44F6-B59C-6C86BDFED87A--

把以上部分结合本机自己的信息,封装成一个HTTP请求数据包

封装成TCP包,建立TCP连接(TCP的三次握手)

在HTTP工作开始之前,客户机(Web浏览器)首先要通过网络与服务器建立连接,该连接是通过TCP来完成的,该协议与IP协议共同构建Internet,即著名的TCP/IP协议族,因此Internet又被称作是TCP/IP网络。HTTP是比TCP更高层次的应用层协议,根据规则,只有低层协议建立之后才能,才能进行更层协议的连接,因此,首先要建立TCP连接,一般TCP连接的端口号是80。这里是8080端口

客户机发送请求命令

建立连接后,客户机发送一个请求给服务器,请求方式的格式为:统一资源标识符(URL)、协议版本号,后边是MIME信息包括请求修饰符、客户机信息和可内容。

服务器响应

服务器接到请求后,给予相应的响应信息,其格式为一个状态行,包括信息的协议版本号、一个成功或错误的代码,后边是MIME信息包括服务器信息、实体信息和可能的内容。
实体消息是服务器向浏览器发送头信息后,它会发送一个空白行来表示头信息的发送到此为结束,接着,它就以Content-Type应答头信息所描述的格式发送用户所请求的实际数据

服务器关闭TCP连接

一般情况下,一旦Web服务器向浏览器发送了请求数据,它就要关闭TCP连接,然后如果浏览器或者服务器在其头信息加入了这行代码
Connection:keep-alive
TCP连接在发送后将仍然保持打开状态,于是,浏览器可以继续通过相同的连接发送请求。保持连接节省了为每个请求建立新连接所需的时间,还节约了网络带宽。

HTTP组件封装

封装的好处

  • 使用者只需要了解如何通过类的接口使用类,而不用关心类的内部数据结构和数据组织方法。
  • 高内聚,低耦合一直是我们所追求的,用好封装恰恰可以减少耦合
  • 只要对外接口不改变,可以任意修改内部实现,这个可以很好的应对变化
  • 类具有了简洁清晰的对外接口,降低了使用者的学习过程

封装的过程

大致分为以下三步:设计接口、填充实现、单元测试。

设计接口

这一步很重要,接口的好坏,直接决定了整个组件的好坏。
这个接口,需要有以下内容:

  • 发起HTTP请求,具体是什么请求,需要自定义这个请求;
  • 需要一个请求完成回来的回调;
  • 回调中需要带一些内容。
    这样在ZHHTTPClient类,只有一个类方法:
+ (void)sendRequest:(ZHHTTPRequest *)request
           complete:(ZHCompleteHandler)completeHandler
            failure:(ZHFailureHandler)failureHandler;

request是一个继承自NSObject的类,需要接收外部配置参数:

@interface ZHHTTPRequest : NSObject
/**
 请求的url,若为GET请求,直接在url后面拼接参数。
 */
@property (nonatomic, copy) NSURL *url;
@property (nonatomic) NSTimeInterval timeoutInterval;
/**
 http请求头
 */
@property (nonatomic, strong) NSDictionary <NSString *, NSString *> *HTTPRequestHeaders;
/**
 http请求参数,GET请求会拼接到url后面,POST请求会拼接到body里面。若为GET请求,不要在此设置值。
 */
@property (nonatomic, strong) NSDictionary *postParams;
/**
 上传文件需要的数据,不需要设置此项。
 */
@property (nonatomic, strong) NSArray<ZHHTTPUploadComponent *> *uploadComponents;
/**
 defaut is NO,不对证书做校验
 */
@property (nonatomic) BOOL validatesSecureCertificate;
/*!
 @abstract Sets the HTTP request method of the receiver. POST or GET,default is GET.
 */
@property (nonatomic) ZHHTTPMethod HTTPMethod;

@property (nonatomic, strong) NSData *HTTPBody;
/**
 default is NO,不做同步请求
 */
@property (nonatomic) BOOL shouldSynchronous;
/**
 下载文件存储的目标路径,要精确到文件名,在设定之前,需要在外部判定文件是否存在,是否需要删除。
 */
@property (nonatomic, copy) NSString *downloadDestinationPath;
/**
 下载文件存储的临时路径,如果下载时不设定此项,会有默认的临时路径。
 */
@property (nonatomic, copy) NSString *downloadTemporaryPath;

- (instancetype)initWithURL:(NSURL *)url;
+ (instancetype)requestWithURL:(NSURL *)url;
@end

其中ZHHTTPUploadComponent, 是为上传准备的一个配置类,会接收上传文件的名称、路径、类型等:

@interface ZHHTTPUploadComponent : NSObject

/**
 dataKey: 每一个dataKey对应于一个filePath或者data数据,在同一次传输中要保证dataKey唯一,不能为空;
 fileName: 指定上传文件的名字,可以为空,为空时取原文件名字;
 filePath: 上传的文件路径
 data: 上传的data数据
 */
@property (nonatomic, copy, readonly) NSString *dataKey;
@property (nonatomic, copy, readonly) NSString *filePath;
@property (nonatomic, copy, readonly) NSString *fileName;
@property (nonatomic, copy, readonly) NSString *mimeType;
@property (nonatomic, strong, readonly) NSData *data;

/**
   Appends the HTTP header `Content-Disposition: file; filename=#{filename}; name=#{name}"` and `Content-Type: #{mimeType}`, followed by the data from the input stream and the multipart form boundary.
 @param dataKey 上传的data所需要的key,不能为空
 @param filePath 上传的文件路径,不能为空
  The fileName and MIME type for this data in the form will be automatically generated, using the last path component of the `filePath` and system associated MIME type for the `filePath` extension, respectively.
 @return A newly-created and autoreleased ZHHTTPUploadComponent instance.
 */
- (instancetype)initWithDataKey:(NSString *)dataKey filePath:(NSString *)filePath;
@end

这一个初始化方法是不够的,还有另外5个初始化方法,比如:

/**
  Appends the HTTP header `Content-Disposition: file; filename=#{filename}; name=#{name}"` and `Content-Type: #{mimeType}`, followed by the data from the input stream and the multipart form boundary.

 @param dataKey 上传的data所需要的key,不能为空
 @param filePath 上传的文件路径,不能为空
 @param fileName 指定上传文件的名字,不能为空
 @param mimeType The MIME type of the specified data. (For example, the MIME type for a JPEG image is image/jpeg.) For a list of valid MIME types, see http://www.iana.org/assignments/media-types/. This parameter must not be `nil`.
 @return A newly-created and autoreleased ZHHTTPUploadComponent instance.
 */
- (instancetype)initWithDataKey:(NSString *)dataKey
                        filePath:(NSString *)filePath
                       fileName:(NSString *)fileName
                       mimeType:(NSString *)mimeType;

再加上response,回调block,HTTPMethod enum:

@interface ZHHTTPResponse : NSObject
@property (nonatomic) NSInteger code;
@property (nonatomic, strong) NSData *data;
@property (nonatomic, copy) NSString *responseString;
@end

typedef void(^ZHCompleteHandler)(ZHHTTPResponse *response);
typedef void(^ZHFailureHandler)(NSError *error);
typedef NS_ENUM(NSInteger, ZHHTTPMethod) {
    POST,
    GET
};

这些是全部的ZHHTTPClient Header中的内容。

填充实现

对主接口的实现如下:

+ (void)sendRequest:(ZHHTTPRequest *)request
           complete:(ZHCompleteHandler)completeHandler
            failure:(ZHFailureHandler)failureHandler {
    if (request.uploadComponents.count > 0 || request.postParams || request.HTTPBody) {
        request.HTTPMethod = POST;
    }
    if (request.HTTPMethod == GET) {
        [self getMethodRequest:request complete:completeHandler failure:failureHandler];
    } else if (request.HTTPMethod == POST) {
        [self postMethodRequest:request complete:completeHandler failure:failureHandler];
    }
}

先是根据设置的参数,对请求方法做了一个校准,然后再是根据是POST还是GET方法,再相应的私有方法。到现在为止还没有看出来,这个ZHHTTPClient内部是封装的哪种网络库,以后所有的更换网络库这种操作,是不需要动接口和这个实现函数的。
在具体的实现中,可以选择自己需要的网络框架,因为历史原因,追求稳定性,这里选择了ASI,代码如下:

+ (void)getMethodRequest:(ZHHTTPRequest *)request
                complete:(ZHCompleteHandler)completeHandler
                 failure:(ZHFailureHandler)failureHandler {
    ASIHTTPRequest *asiRequest = [ASIHTTPRequest requestWithURL:request.url];
    [asiRequest setRequestMethod:@"GET"];
    [self configureASIRequest:asiRequest ZHHTTPRequest:request complete:completeHandler failure:failureHandler];
    if (!request.shouldSynchronous) {
        [asiRequest startAsynchronous];
    } else {
        [asiRequest startSynchronous];
    }
}

+ (void)postMethodRequest:(ZHHTTPRequest *)request
                 complete:(ZHCompleteHandler)completeHandler
                  failure:(ZHFailureHandler)failureHandler {
    ASIFormDataRequest *asiRequest = [ASIFormDataRequest requestWithURL:request.url];
    [asiRequest setRequestMethod:@"POST"];
    if (request.postParams) {
        for (id key in request.postParams) {
            [asiRequest setPostValue:request.postParams[key] forKey:key];
        }
    } else {
        if (request.HTTPBody) {
            [asiRequest setPostBody:[NSMutableData dataWithData:request.HTTPBody]];
        }
    }
    if (request.uploadComponents) {
        for (NSInteger i = 0; i < request.uploadComponents.count; i++) {
            ZHHTTPUploadComponent *component = request.uploadComponents[i];
            if (component.filePath) {
                [asiRequest addFile:component.filePath withFileName:component.fileName andContentType:component.mimeType forKey:component.dataKey];
            } else if (component.data) {
                [asiRequest addData:component.data withFileName:component.fileName andContentType:component.mimeType forKey:component.dataKey];
            }
        }
    }
    [self configureASIRequest:asiRequest ZHHTTPRequest:request complete:completeHandler failure:failureHandler];
    if (!request.shouldSynchronous) {
        [asiRequest startAsynchronous];
    } else {
        [asiRequest startSynchronous];
    }
}

+ (void)configureASIRequest:(ASIHTTPRequest *)asiRequest
              ZHHTTPRequest:(ZHHTTPRequest *)request
                   complete:(ZHCompleteHandler)completeHandler
                    failure:(ZHFailureHandler)failureHandler {
    [asiRequest setValidatesSecureCertificate:request.validatesSecureCertificate];
    [asiRequest setTimeOutSeconds:request.timeoutInterval];
    if (request.HTTPRequestHeaders) {
        NSMutableDictionary *dict = [request.HTTPRequestHeaders copy];
        [asiRequest setRequestHeaders:dict];
    }
    if (request.downloadDestinationPath) { //有下载路径时,认为是下载
        [asiRequest setDownloadDestinationPath:request.downloadDestinationPath];
        [asiRequest setTemporaryFileDownloadPath:request.downloadTemporaryPath];
    }
    __weak typeof(asiRequest) weakAsiRequest = asiRequest;
    asiRequest.completionBlock = ^{
        __strong typeof(weakAsiRequest) strongAsiRequest = weakAsiRequest;
        ZHHTTPResponse *response = [ZHHTTPResponse new];
        response.code = strongAsiRequest.responseStatusCode;
        response.data = strongAsiRequest.responseData;
        response.responseString = strongAsiRequest.responseString;
        if (completeHandler) {
            completeHandler(response);
        }
    };
    [asiRequest setFailedBlock:^{
        __strong typeof(weakAsiRequest) strongAsiRequest = weakAsiRequest;
        if (failureHandler) {
            failureHandler(strongAsiRequest.error);
        }
    }];
}

ZHHTTPUploadComponent的实现就是对多个初始化方法指向一个全能初始化方法,把传进来的参数赋值到实例变量中。
ZHHTTPRequest实现在,会做默认值处理:

@implementation ZHHTTPRequest
- (instancetype)initWithURL:(NSURL *)url{
    if (self = [super init]) {
        _url = url;
        _timeoutInterval = 10;
        _HTTPMethod = GET;
    }
    return self;
}
+ (instancetype)requestWithURL:(NSURL *)url {
    return [[self alloc] initWithURL:url];
}
@end

最后还有一个取MIME type的方法:

static NSString *const kDefaultMimeType = @"application/octet-stream";
static inline NSString * ZHContentTypeForPathExtension(NSString *extension) {
    NSString *UTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)extension, NULL);
    NSString *contentType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)UTI, kUTTagClassMIMEType);
    if (!contentType) {
        return kDefaultMimeType;
    } else {
        return contentType;
    }
}

单元测试

static NSString * const author = @"Shawn";

正常POST:

- (void)testNormalPost {
    ZHHTTPRequest *request = [[ZHHTTPRequest alloc] initWithURL:[NSURL URLWithString:@"http://localhost:8080/postName"]];
    request.HTTPMethod = POST;
    request.postParams = @{@"name": author, @"sex": @(1)};
    NSString *expectedResult = author;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [ZHHTTPClient sendRequest: request complete:^(ZHHTTPResponse *response) {
        NSString *responseString = [[NSString alloc] initWithData:response.data encoding:NSUTF8StringEncoding];
        NSLog(@"response: %@", responseString);
        XCTAssertTrue([[[NSString alloc] initWithData:response.data encoding:NSUTF8StringEncoding] isEqualToString:expectedResult], @"Strings are not equal %@ != %@", expectedResult, responseString);
        dispatch_semaphore_signal(semaphore);
    } failure:^(NSError *error) {
        NSLog(@"error: %@", error.description);
        dispatch_semaphore_signal(semaphore);
    }];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}

文件上传:

- (void)testFileUpload {
    ZHHTTPRequest *request = [[ZHHTTPRequest alloc] initWithURL:[NSURL URLWithString:@"http://localhost:8080/upload"]];
    request.HTTPMethod = POST;
    request.postParams = @{@"name": @"Shawn"};
    NSString *filePath1 = [[NSBundle mainBundle] pathForResource:@"upload0" ofType:@"txt"];
    NSData *data = [@"Shawn1" dataUsingEncoding:NSUTF8StringEncoding];

    ZHHTTPUploadComponent *comp0 = [[ZHHTTPUploadComponent alloc] initWithDataKey:@"data0" filePath:filePath1];
    ZHHTTPUploadComponent *comp1 = [[ZHHTTPUploadComponent alloc] initWithDataKey:@"data1" data:data];

    request.uploadComponents = @[comp0, comp1];
    NSString *expectedResult = @"Shawn0Shawn1";
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [ZHHTTPClient sendRequest: request complete:^(ZHHTTPResponse *response) {
        NSString *responseString = [[NSString alloc] initWithData:response.data encoding:NSUTF8StringEncoding];
        NSLog(@"response: %@", responseString);
        XCTAssertTrue([[[NSString alloc] initWithData:response.data encoding:NSUTF8StringEncoding] isEqualToString:expectedResult], @"Strings are not equal %@ != %@", expectedResult, responseString);
        dispatch_semaphore_signal(semaphore);
    } failure:^(NSError *error) {
        NSLog(@"error: %@", error.description);
        dispatch_semaphore_signal(semaphore);
    }];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}

文件下载:

- (void)testDownload {
    ZHHTTPRequest *request = [[ZHHTTPRequest alloc] initWithURL:[NSURL URLWithString:@"http://devthinking.com/wp-content/uploads/2017/07/runtime.jpg"]];
    NSString *cacheFolder = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
                                                          NSUserDomainMask,
                                                          YES) lastObject];
    NSString *docFolder = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
                                                               NSUserDomainMask,
                                                               YES) lastObject];
    NSDate *now = [NSDate new];
    NSString *cachePath = [cacheFolder stringByAppendingPathComponent:[NSString stringWithFormat:@"%@", now]];
    NSString *dstPath = [docFolder stringByAppendingPathComponent:@"temp.jpg"];
    NSLog(@"tmp: %@, cachePath: %@", dstPath, cachePath);
    request.downloadDestinationPath = dstPath;
    request.downloadTemporaryPath = cachePath;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [ZHHTTPClient sendRequest: request complete:^(ZHHTTPResponse *response) {
        XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:dstPath], @"file is not exist at path: %@", dstPath);
        dispatch_semaphore_signal(semaphore);
    } failure:^(NSError *error) {
        NSLog(@"error: %@", error.description);
        dispatch_semaphore_signal(semaphore);
    }];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}

直接填充body:

- (void)testBodyDataPost {
    ZHHTTPRequest *request = [[ZHHTTPRequest alloc] initWithURL:[NSURL URLWithString:@"http://localhost:8080/postBodyData"]];
    request.HTTPMethod = POST;
    NSDictionary *dict = @{@"name": author};
    NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:0 error:nil];
    request.HTTPBody = data;
    NSString *expectedResult = author;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [ZHHTTPClient sendRequest: request complete:^(ZHHTTPResponse *response) {
        NSString *responseString = [[NSString alloc] initWithData:response.data encoding:NSUTF8StringEncoding];
        NSLog(@"response: %@", responseString);
        XCTAssertTrue([[[NSString alloc] initWithData:response.data encoding:NSUTF8StringEncoding] isEqualToString:expectedResult], @"Strings are not equal %@ != %@", expectedResult, responseString);
        dispatch_semaphore_signal(semaphore);
    } failure:^(NSError *error) {
        NSLog(@"error: %@", error.description);
        dispatch_semaphore_signal(semaphore);
    }];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}

Get方法:

- (void)testGet {
    ZHHTTPRequest *request = [[ZHHTTPRequest alloc] initWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:8080/plaintext?name=%@", author]]];
    request.HTTPMethod = GET;
    NSString *expectedResult = author;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [ZHHTTPClient sendRequest: request complete:^(ZHHTTPResponse *response) {
        NSString *responseString = [[NSString alloc] initWithData:response.data encoding:NSUTF8StringEncoding];
        NSLog(@"response: %@", responseString);
        XCTAssertTrue([[[NSString alloc] initWithData:response.data encoding:NSUTF8StringEncoding] isEqualToString:expectedResult], @"Strings are not equal %@ != %@", expectedResult, responseString);
        dispatch_semaphore_signal(semaphore);
    } failure:^(NSError *error) {
        NSLog(@"error: %@", error.description);
        dispatch_semaphore_signal(semaphore);
    }];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容