网络请求NSURLSession

一. 前言

很多人都用过AFNetWorking和SDWebImage,其实底层就是封装了NSURLSession来请求任务。第三方用多了,对于苹果原生的网络请求知识却掌握得不够牢固,所以写这篇文章来总结回顾一下。

苹果在 iOS9 之后已经放弃了 NSURLConnection,所以在现在的实际开发中,一般使用的是 iOS7 之后推出的 NSURLSession。NSURLSession 和 NSURLConnection 都提供了与各种协议,诸如 HTTP 和 HTTPS ,进行交互的API。会话对象(NSURLSession 类对象)就是用于管理这种交互过程。它是一个高度可配置的容器,通过使用其提供的APPI,可进行细粒度的管理控制。它提供了在 NSURLConnection 中的所有特性,此外,它还可以实现 NSURLConnection 不能完成的任务,例如实现私密浏览。

总之,NSURLSession 有如下优势:

  • NSURLSession 支持 http2.0 协议
  • 在处理下载任务的时候可以直接把数据下载到磁盘(通过配置)
  • 支持后台下载|上传(通过配置)
  • 同一个 session 发送多个请求,只需要建立一次连接(复用了TCP)
  • 提供了全局的 session 并且可以统一配置,使用更加方便
  • 下载的时候是多线程异步处理,效率更高

一. NSURLSession的介绍

NSURLSession的配置和创建

NSURLSession对象是一个会话,你可以把他当成是生产网络请求任务的工厂。NSURLSession有三种工作模式:

  • 默认会话模式(default):工作模式类似于原来的NSURLConnection,使用的是基于磁盘缓存的持久化策略,使用用户keychain中保存的证书进行认证授权。

  • 瞬时会话模式(ephemeral):该模式不使用磁盘保存任何数据。所有和会话相关的caches,证书,cookies等都被保存在RAM中,因此当程序使会话无效,这些缓存的数据就会被自动清空。(可以实现私密浏览)

  • 后台会话模式(background):该模式在后台完成上传和下载(在系统的一个单独的进程中执行),在创建Configuration对象的时候需要提供一个NSString类型的ID用于标识完成工作的后台会话。

简单点理解,就是不同的工作模式用来解决不同的网络请求场景,当你需要缓存会话相关的caches,证书,cookies等是就使用默认会话模式,不需要就用瞬时会话模式,需要在后台进行上传下载就用后台会话模式。

那怎么设置这些模式呢?
这些工作模式是由NSURLSessionConfiguration决定的。以下三种创建方式一一对应NSURLSession的三种工作模式,其实本质上还是NSURLSessionConfiguration内部属性所决定,也省得我们一一去设置。

+ (NSURLSessionConfiguration *)defaultSessionConfiguration;  
+ (NSURLSessionConfiguration *)ephemeralSessionConfiguration;  
+ (NSURLSessionConfiguration *)backgroundSessionConfiguration:(NSString *)identifier; 

一个NSURLSessionConfiguration对象定义的行为和策略被用于使用NSURLSession上传和下载数据,创建一个配置对象始终是你必须采取的第一步。你可以使用这个对象去配置你想要使用的NSURLSession对象的超时时间、缓存策略、连接请求以及其它类型的信息。
以下我们了解下NSURLSessionConfiguration的常见属性(如果想了解更多属性,可以看这篇文章NSURLSessionConfiguration详解):

//如果在后台任务正在传输时程序退出,可以使用这个identifier在程序重新启动是创建一个新的configuration和session关联之前传输。
@property(readonly, copy) NSString  *identifier;

//默认为空,NSURLRequest附件的请求头。
//这个属性会给所有使用该configuration的session生成的tasks中的NSURLRequest添加额外的请求头。
//如果这里边添加的请求头跟NSURLRequest中重复了,侧优先使用NSURLRequest中的头
@property(copy) NSDictionary  *HTTPAdditionalHeaders;

//是否使用蜂窝网络,默认是yes.
@property BOOL allowsCellularAccess;

//给request指定每次接收数据超时间隔
//如果下一次接受新数据用时超过该值,则发送一个请求超时给该request。默认为60s
@property NSTimeInterval  timeoutIntervalForRequest;

//给指定resource设定一个超时时间,resource需要在时间到达之前完成
//默认是7天。
@property NSTimeInterval  timeoutIntervalForResource;

//discretionary属性为YES时表示当程序在后台运作时由系统自己选择最佳的网络连接配置,该属性可以节省通过蜂窝连接的带宽。
//在使用后台传输数据的时候,建议使用discretionary属性,而不是allowsCellularAccess属性,因为它会把WiFi和电源可用性考虑在内。
@property (getter=isDiscretionary) BOOL discretionary;

//表示当后台传输结束时,是否启动app.这个属性只对 生效,其他configuration类型会自动忽略该值。默认值是YES。
@property BOOL sessionSendsLaunchEvents;

//是否启动通道,可以用于加快网络请求,默认是NO
@property BOOL HTTPShouldUsePipelining;

/===========储存的相关属性=============/

//存储cookie,清除存储,直接set为nil即可。
//对于默认和后台的session,使用sharedHTTPCookieStorage。
//对于短暂的session,cookie仅仅储存到内存,session失效时会自动清除。
@property(retain) NSHTTPCookieStorage  *HTTPCookieStorage;

//默认为yes,是否提供来自shareCookieStorge的cookie
//如果想要自己提供cookie,可以使用HTTPAdditionalHeaders来提供。
@property BOOL  HTTPShouldSetCookies;

//证书存储,如果不使用,可set为nil.
//默认和后台session,默认使用的sharedCredentialStorage.
//短暂的session使用一个私有存储在内存中。session失效会自动清除。
@property(retain) NSURLCredentialStorage *URLCredentialStorage;

//缓存NSURLRequest的response。
//默认的configuration,默认值的是sharedURLCache。
//后台的configuration,默认值是nil
//短暂的configuration,默认一个私有的cache于内存,session失效,cache自动清除。
@property(retain) NSURLCache  *URLCache;

//缓存策略,用于设置该会话中的Request的cachePolicy,如果Request有单独设置的话,以Request为准。
//默认值是NSURLRequestUseProtocolCachePolicy
@property NSURLRequestCachePolicy requestCachePolicy;

那我们该怎么创建想要的会话对象呢?总共有三种创建方式,我们可以创建对应的配置类(NSURLSessionConfiguration)来配置。

//使用静态的sharedSession方法,该类使用共享的会话,该会话使用全局的Cache,Cookie和证书
+ (NSURLSession *)sharedSession;  

//通过sessionWithConfiguration:方法创建对象,也就是创建对应配置的会话,与NSURLSessionConfiguration合作使用
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration;  

//通过设置配置、代理、队列来创建会话对象
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration delegate:(id <NSURLSessionDelegate>)delegate delegateQueue:(NSOperationQueue *)queue; 

二三两种方式可以创建一个新会话并定制其会话类型。该方式中指定了session的委托和委托所处的队列。当不再需要连接时,可以调用Session的invalidateAndCancel直接关闭,或者调用finishTasksAndInvalidate等待当前Task结束后关闭。这时Delegate会收到URLSession:didBecomeInvalidWithError:这个事件。Delegate收到这个事件之后会被解引用。

NSURLSession支持的任务

之前提到的,NSURLSession对象生产网络请求任务的工厂,它能生成出三种类型的任务:加载数据,下载和上传。而这个任务类就是NSURLSessionTask。
NSURLSessionTask 是一个抽象类,如果要使用那么只能使用它的子类。

NSURLSessionTask.png
  • NSURLSessionDataTask,可以用来处理一般的网络请求,如 GET | POST 请求等。
  • NSURLSessionUploadTask,用于处理上传请求。
  • NSURLSessionDownloadTask,主要用于处理下载请求。

下面我们来详细介绍下三种任务:

① NSURLSessionDataTask

我们可以通过request对象或url创建NSURLSessionDataTask对象。也可以选择用代理或者Block来接收数据。

//这两个方法需要设置代理来接收数据
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request;
- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url;

//这两个方法在completionHandler来接收数据
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;
- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;

使用Block的回调接收数据的方法比较简洁,但是却没有设置代理,通过代理获取数据来得灵活。这边重点介绍代理的方式,顺便回顾一下上面所讲的NSURLSession的配置和创建。

以下就分别举两个例子来说明(代理和Block):
(1)代理的方式:
代理的方式的话需要设置代理人,遵守协议NSURLSessionDelegate和NSURLSessionTaskDelegate。
说到协议,几个类都有对应的协议,比如NSURLSessionDataTask就对应NSURLSessionDataDelegate,该协议主要用来处理dataTask的数据处理(比如接收到响应,接收到数据,是否缓存数据)。另外,也经常需要遵守公共的协议:NSURLSessionDelegate和NSURLSessionTaskDelegate。
NSURLSessionDelegate主要关于会话的,比如会话关闭,会话收到挑战。
NSURLSessionTaskDelegate主要关于任务的,可以理解为任务共用的协议,比如任务完成或失败,任务收到挑战,任务将要执行HTTP重定向。

还有一个重点,就是关于缓存。
之前提到的NSURLSession默认的会话模式,也就是defaultSessionConfiguration的配置,可以将缓存存储在磁盘上(本质就是使用NSURLCache,具体介绍可以看这边文章NSURLCache缓存使用简介)。
路径是沙盒路径下Library/Caches/bundid/Cache.db(对于webview的缓存,也一样,因为它也是用的NSURLCache)。

另外,还有一个常见的设置就是cachePolicy(缓存策略),该设置决定要不要从缓存中获取,比如request.cachePolicy = NSURLRequestUseProtocolCachePolicy。以下还有几种策略:

typedef NS_ENUM(NSUInteger, NSURLRequestCachePolicy)
{
    //对特定的 URL 请求使用网络协议中实现的缓存逻辑。这是默认的策略。
    NSURLRequestUseProtocolCachePolicy = 0,

    //数据需要从原始地址加载。不使用现有缓存。
    NSURLRequestReloadIgnoringLocalCacheData = 1,

    // 不仅忽略本地缓存,同时也忽略代理服务器或其他中间介质目前已有的、协议允许的缓存。
    NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4, 

    NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,

    //无论缓存是否过期,先使用本地缓存数据。如果缓存中没有请求所对应的数据,那么从原始地址加载数据。
    NSURLRequestReturnCacheDataElseLoad = 2,

    //无论缓存是否过期,先使用本地缓存数据。如果缓存中没有请求所对应的数据,那么放弃从原始地址加载数据,请求视为失败(即:“离线”模式)。
    NSURLRequestReturnCacheDataDontLoad = 3,

    //从原始地址确认缓存数据的合法性后,缓存数据就可以使用,否则从原始地址加载。
    NSURLRequestReloadRevalidatingCacheData = 5, 
};

例子中,我们设置了默认配置,请求的缓存策略为NSURLRequestReturnCacheDataElseLoad,这样当我们发送一个请求后,就会将数据和响应保存在沙盒中的数据库,当我们下次发送请求,会先从缓存中找,找不到再去服务器找。

#import "ViewController.h"

@interface ViewController ()<NSURLSessionDelegate,NSURLSessionTaskDelegate>
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self sendRequest];
}
- (void)sendRequest{
    //创建请求
    NSURL *url = [NSURL URLWithString:@"http://httpbin.org/get"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    //设置request的缓存策略(决定该request是否要从缓存中获取)
    request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;
    
    //创建配置(决定要不要将数据和响应缓存在磁盘)
    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    //configuration.requestCachePolicy = NSURLRequestReturnCacheDataElseLoad;
    
    //创建会话
    NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
    //生成任务
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request];
    //创建的task是停止状态,需要我们去启动
    [task resume];
}
//1.接收到服务器响应的时候调用
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler{
    NSLog(@"接收响应");
    //必须告诉系统是否接收服务器返回的数据
    //默认是completionHandler(NSURLSessionResponseAllow)
    //可以再这边通过响应的statusCode来判断否接收服务器返回的数据
    completionHandler(NSURLSessionResponseAllow);
}
//2.接受到服务器返回数据的时候调用,可能被调用多次
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data{
    NSLog(@"接收到数据");
    //一般在这边进行数据的拼接,在方法3才将完整数据回调
//    NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
}
//3.请求完成或者是失败的时候调用
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error{
    NSLog(@"请求完成或者是失败");
    //在这边进行完整数据的解析,回调
}
//4.将要缓存响应的时候调用(必须是默认会话模式,GET请求才可以)
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse * _Nullable cachedResponse))completionHandler{
    //可以在这边更改是否缓存,默认的话是completionHandler(proposedResponse)
    //不想缓存的话可以设置completionHandler(nil)
    completionHandler(proposedResponse);
}
@end

(2)Block的方式:

NSURL *url = [NSURL URLWithString:@"http://www.connect.com/login"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
request.HTTPBody = [@"username=Tom&pwd=123" dataUsingEncoding:NSUTF8StringEncoding];

//使用全局的会话
NSURLSession *session = [NSURLSession sharedSession];
// 通过request初始化task
NSURLSessionTask *task = [session dataTaskWithRequest:request
                                   completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { 
    NSLog(@"%@", [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil]);
 }];
//创建的task是停止状态,需要我们去启动
[task resume];
② NSURLSessionUploadTask

UploadTask继承自DataTask。因为UploadTask只不过在Http请求的时候,把数据放到Http Body中。所以,用UploadTask来做的事情,通常直接用DataTask也可以实现。
NSURLSessionUploadTask通过request创建,在上传时指定文件源或数据源,可以用代理或Block指定任务完成后的回调代码。

/=========代理方式===========/
//通过文件url来上传
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL;  
//通过文件data来上传
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData;  
//通过文件流来上传
- (NSURLSessionUploadTask *)uploadTaskWithStreamedRequest:(NSURLRequest *)request;

/=========Block方式===========/
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler;  
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler;  

这三种上传方式分别针对什么应用场景呢?

  • NSData:如果对象已经在内存里
  • File:如果对象在磁盘上,这样做有助于降低内存使用
  • Stream:通过流对象,你可以不用一次性将所有的流数据加载到内存中
    不过使用Stream一定要实现URLSession:task:needNewBodyStream:,因为Session没办法在重新尝试发送Stream的时候找到数据源。

这边我就举个上传图片的例子:

- (void)uploadRequest{
    //创建请求
    NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.freeimagehosting.net/upload.php"]];
    //如果是上传文字就是@"application/json"
    [request addValue:@"image/jpeg" forHTTPHeaderField:@"Content-Type"];
    [request addValue:@"text/html" forHTTPHeaderField:@"Accept"];
    [request setHTTPMethod:@"POST"];
    [request setCachePolicy:NSURLRequestReloadIgnoringCacheData];
    [request setTimeoutInterval:20];
    NSData * imagedata = UIImageJPEGRepresentation([UIImage imageNamed:@"person"],1.0);
    
    //创建配置(决定要不要将数据和响应缓存在磁盘)
    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    //创建会话
    NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
    
    NSURLSessionUploadTask * uploadtask = [session uploadTaskWithRequest:request fromData:imagedata completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        //发送完成的回调
        
    }];
    [uploadtask resume];
}
//发送数据过程中会执行(执行多次)
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend{
    NSLog(@"发送数据中");
    //在这边监听发送的进度
    //progress = totalBytesSent/(float)totalBytesExpectedToSend
}
③ NSURLSessionDownloadTask

在前面请求数据的时候就相当于一个简单的下载过程,所以说NSURLSessionDataTask也是可以做到简单下载功能的,比如下载图片(请求回图片数据)。
但是对比普通请求,NSURLSessionDownloadTask的功能更为强大:

  • 下载文件可以实现断点下载
  • 内部已经完成了边接收数据边写入沙盒的操作(直接下载到磁盘)
  • 支持BackgroundSession(后台下载)
    接下来我们来了解一下下载的API,除了通过url或request下载外,还可以通过之前已经下载的数据来创建下载任务(也就是我们说的断点续传)。同样地可以通过completionHandler指定任务完成后的回调代码块。
/=========代理方式===========/
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request;  
- (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url;  
//通过之前已经下载的数据来创建下载任务
- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData;  

/=========Block方式===========/
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURL *location, NSURLResponse *response, NSError *error))completionHandler;  
- (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSURL *location, NSURLResponse *response, NSError *error))completionHandler;  
- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData completionHandler:(void (^)(NSURL *location, NSURLResponse *response, NSError *error))completionHandler;  

关于断点续传,使用到的其他方法如下(断点续传的前提是服务器也支持断点续传):

// 使用这种方式取消下载可以得到将来用来恢复的数据,保存起来
[self.task cancelByProducingResumeData:^(NSData *resumeData) {
    self.resumeData = resumeData;
}];

// 由于下载失败导致的下载中断会进入此协议方法,也可以得到用来恢复的数据
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    // 保存恢复数据
    self.resumeData = error.userInfo[NSURLSessionDownloadTaskResumeData];
}

// 恢复下载时接过保存的恢复数据
self.task = [self.session downloadTaskWithResumeData:self.resumeData];
// 启动任务
[self.task resume];

我们举个例子来认识一下普通下载过程(代理方式,需要遵守NSURLSessionDelegate,NSURLSessionDownloadDelegate):

- (void)downloadRequest{
    //创建请求
    NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://httpbin.org/image/jpeg"]];
    
    //创建配置(决定要不要将数据和响应缓存在磁盘)
    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    //创建会话
    NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
    
    NSURLSessionDownloadTask * downloadtask = [session downloadTaskWithRequest:request];
    [downloadtask resume];
}

//1. downloadTask下载过程中会执行
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{
    NSLog(@"下载中...");
    NSLog(@"写入数据大小%lld,总写入数据大小%lld,总期望数据大小%lld",bytesWritten,totalBytesWritten,totalBytesExpectedToWrite);
    //监听下载的进度
}
//2.downloadTask下载完成的时候会执行
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{
    NSLog(@"下载完成");
       //该方法内部已经完成了边接收数据边写沙盒的操作,解决了内存飙升的问题
    //对数据进行使用,或者保存(默认存储到临时文件夹 tmp 中,需要剪切文件到 cache)
    
    //保存
    NSString *filePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:downloadTask.response.suggestedFilename];
    [[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:filePath] error:nil];

    //使用
    NSData * data = [NSData dataWithContentsOfURL:location.filePathURL];
    UIImage * image = [UIImage imageWithData:data];
    UIImageWriteToSavedPhotosAlbum(image, nil,nil,nil);
}
//3.请求完成或者是失败的时候调用(Session层次的Task完成的事件)
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error{
    NSLog(@"请求完成或者是失败");
}

如果想了解一下后台下载,可以看下这篇文章iOS7 Networking with NSURLSession

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

推荐阅读更多精彩内容