NSURLSession 相关

一、 NSURLSession 基础知识

NSURLSession协调一组相关的网络数据传输任务的对象,本身是不会进行请求的,而是通过创建 task (NSURLSessionTask) 的形式进行网络请求(resume()方法的调用)。

1.同一个 NSURLSession 可以创建多个 task,并且这些 task 之间的 cache 和 cookie 是共享的。
2.如果您的应用程序创建一个或多个NSURLSession实例,每个实例协调一组相关的数据传输任务。

通过NSURLSession 发起一个网络请求 一般有如下几步:

  • 1、创建NSURLSession对象

    创建 NSURLSession 对象之前 根据需求 可能 会 创建一个NSURLSessionConfiguration配置请求。

  • 2、通过session对象发起网络请求,并获取task对象
  • 3、 调用[task resume]方法发起网络请求

NSURLSession的使用相对于之前的NSURLConnection更简单,而且不用处理Runloop相关的东西。

NSURLSession相关类为 :

  • NSURLSession
  • NSURLSessionConfiguration
  • NSURLSessionDelegate
  • NSURLSessionTask
  • NSURLSessionTaskMetrics
  • NSURLSessionTaskTransactionMetrics

二、NSURLSession 创建方式

NSURLSession:请求会话对象,可以用系统提供的单例对象,也可以自己创建。

  • NSURLSession有三种方式创建:

    • 1、sharedSession共享的单例会话对象(没有配置对象),用于基本请求,可以和其他使用这个session的task共享连接和请求信息,共享会话使用当前设置的全局NSURLCache, NSHTTPCookieStorageNSURLCredentiaStorage对象
      有关更多信息,请参见:shareSession

    • 2、sessionWithConfiguration:使用指定的会话配置创建会话。

      • 2.1、configuration:一个配置对象,用于指定某些行为,例如缓存策略,超时,代理,管道,支持的TLS版本,cookie策略,凭据存储等。有关更多信息,请参见 NSURLSessionConfiguration
    • 3、sessionWithConfiguration:delegate:delegateQueue: 使用指定的会话配置、委托和操作队列创建会话。 如果想更好的控制请求过程以及回调线程,需要上面的方法进行初始化操作,并传入delegate来设置回调对象和回调的线程。

      • 3.1、 delegate:会话委托对象,用于处理对身份验证问题、做出缓存决策以及处理其他与会话相关的的事件的请求。如果为nil,则该类只能与接受完成处理程序的方法一起使用。

      从继承关系上,我们就可以理解在初始化的时候,只通过设置NSURLSession对象的delegate就可以了。因为根据不同的task,其实就是设置了不同的delegate。这个设计避免了多次设置delegate的情况,同时也根据不同的task实现不同的delegate方法。


      NSURLSessionDelegate: 作为所有代理的基类,定义了网络请求最基础的代理方法。NSURLSessionTaskDelegate – 定义了网络请求任务相关的代理方法。NSURLSessionDownloadDelegate – 用于下载任务相关的代理方法,比如下载进度等等。NSURLSessionDataDelegate – 用于普通数据任务和上传任务

      注意:session对象保持对委托的强引用,直到你的应用程序退出或显式地使 会话失效。 如果您没有通过调用invalidateAndCancel 或 finishTasksAndInvalidate 方法来使会话 无效,则您的应用程序会 泄漏内存,直到退出。

      • 3.2、 delegateQueue:用于调度 委托调用完成处理程序的操作队列。 该队列应该是一个串行队列,以确保回调的正确顺序。 如果为nil,则会话将创建一个串行操作队列,用于执行所有委托方法调用和完成处理程序调用。
@property (class, readonly, strong) NSURLSession *sharedSession;
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration;
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration delegate:(nullable id <NSURLSessionDelegate>)delegate delegateQueue:(nullable NSOperationQueue *)queue;

有关更多信息,请参见 sessionWithConfiguration:delegate:delegateQueue:

三、NSURLSessionConfiguration 会话配置方式

NSURLSessionConfiguration 负责对 NSURLSession 初始化时进行配置。
通过 NSURLSessionConfiguration 可以设置请求的Cookie、密钥、缓存、请求头等参数,将网络请求的一些配置参数从 NSURLSession 中分离出来。

NSURLSessionConfiguration 对象定义了使用 NSURLSession 对象 上传和 下载 数据时使用的行为和策略。当上传或下载数据时,创建配置对象总是您必须采取的第一步。您可以使用此对象来配置超时值、缓存策略、连接要求和其他类型的信息,这些信息您打算与 NSURLSession 对象一起使用。

在使用 NSURLSessionConfiguration 对象初始化一个会话对象之前,适当地配置它是很重要的。会话对象复制您提供的配置设置,并使用这些设置来配置会话。配置完成后,会话对象(session)将忽略您对NSURLSessionConfiguration对象所做的任何更改。如果您需要修改您的传输策略,您必须更新会话配置对象并使用它来创建一个新的 NSURLSession 对象。

请注意:在某些情况下,这个配置中定义的策略可能会被一个为任务提供的 NSURLRequest 对象指定的策略所覆盖。 除非会话的策略更具限制性,否则将遵守在请求对象(NSURLRequest)上指定的任何策略。 例如,如果会话配置指定不允许使用蜂窝网络(会话配置限制使用蜂窝网络),则NSURLRequest对象将无法请求蜂窝网络

3.1、NSURLSessionConfiguration 会话配置的类型

类方法,创建对象

  • 3.1.1、defaultSessionConfiguration: 默认会话配置,使用基于磁盘的持久缓存将结果下载到文件时除外),并将账户信息存储在用户的钥匙串中。 它还将cookie(默认情况下)存储在与NSURLSessionNSURLDownload类相同的共享cookie存储中。

注意:修改 返回的会话配置对象 不会影响 以后调用defaultSessionConfiguration方法 返回的任何配置对象 ,并且不会更改现有会话的默认行为。因此,将 返回的 对象用作其他自定义的起点 始终是安全的。

@property (class, readonly, strong) NSURLSessionConfiguration *defaultSessionConfiguration;
@property (class, readonly, strong) NSURLSessionConfiguration *ephemeralSessionConfiguration;

+ (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier:(NSString *)identifier API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));

有关更多信息,请参见:defaultSessionConfiguration

  • 3.1.2、ephemeralSessionConfiguration临时会话配置,跟defaultSessionConfiguration不同的是 会话对象不将缓存、凭据存储或任何与会话相关的数据存储到磁盘。相反,与会话相关的数据存储在RAM中(只会存在内存里),所以当您的应用使会话无效时,将自动清除所有临时会话数据。 此外,在iOS中,暂停应用程序时不会自动清除内存中的缓存,但是如果用户退出并重新启动您的应用,则保证不会存在。

使用临时会话的主要优点是隐私。 通过不将可能敏感的数据写入磁盘,可以减少数据被以后拦截和使用的可能性。 因此,临时会话非常适合Web浏览器和其他类似情况下的私有浏览模式。由于临时会话不会将缓存的数据写入磁盘,因此缓存的大小受可用RAM的限制,可能会降低感知的性能,具体取决于您的应用程序。

有关更多信息,请参见:ephemeralSessionConfiguration

  • 3.1.3、 backgroundSessionConfigurationWithIdentifier: :使系统在单独的进程中执行上载和下载任务的一个配置对象(后台会话配置对象),该会话配置对象允许程序在后台执行HTTP和HTTPS上传或下载任务。另外,系统会根据设备的负载程度决定分配下载的资源,因此有可能会很慢甚至超时失败
    • identifier:配置对象的唯一标识符。 此参数不能为nil或为空字符串。

注意:1. 如果iOS应用被系统终止并重新启动,则该应用可以使用相同的标识符)来创建新的配置对象和会话,并检索终止时正在进行的传输状态,可以使用同一个 identifier创建configuration和session,并且能恢复终止时的传输状态。此行为仅适用于系统正常终止应用程序的情况 。
2.如果用户从多任务屏幕中终止该应用程序,则系统会取消所有会话的后台传输。此外,系统不会自动重新启动由用户强制退出的应用程序。用户必须明确重新启动该应用程序,然后才能再次开始传输。

有关更多信息,请参见:backgroundSessionConfigurationWithIdentifier:
有关使用后台配置的示例,请参阅Downloading Files in the Background.

四、NSURLSessionTask 对象的创建

NSURLSessionTask 类是 URL 会话中任务的基类,通过 request 对象或 URL 创建,但一般不会直接是 NSURLSessionTask 类,而是基于不同任务类型,通过调用 NSURLSession 实例中的一个任务创建方法来创建任务。您调用的方法决定了任务的类型。

NSURLSessionTask 类型:

  • 1、NSURLSessionDataTask:处理普通的GetPost数据请求。使用 NSURLSession 的相关方法来创建NSURLSessionDataTask实例。 默认(default),临时(ephemeral)和共享(shared)会话(sessions)均支持它们,但后台会话(background sessions)不支持它们。

    • 1.1、 NSURLSessionUploadTask处理上传请求,通过request创建,在上传时指定文件源或数据源(以数据流的方式进行上传,这种方式好处就是大小不受限制,上传需要服务器端脚本支持)。此外,在后台会话中支持上传任务。是NSURLSessionDataTask的子类。
      有关更多信息,请参见:NSURLSessionUploadTask
  • 2、NSURLSessionDownloadTask处理下载任务。下载任务直接将服务器的响应数据写入一个临时文件,以便在数据从服务器到达时为您的应用提供进度更新。 当您在后台会话中使用下载任务时,即使您的应用处于挂起状态未运行,这些下载也会继续。您可以暂停(取消)下载任务,并在以后恢复它们(如果服务器支持断点下载的话)。 您也可以恢复由于网络连接问题而失败的下载。
    有关更多信息,请参见:NSURLSessionDownloadTask

  • 3、NSURLSessionStreamTask基于流的URL会话任务。使用 NSURLSession 的相关方法来创建NSURLSessionStreamTask实例。NSURLSessionStreamTask对象执行同步读取和写入,这些读取和写入将依次排队和执行,并在会话委托队列上完成时调用处理程序。 如果取消该任务,则所有排队的读写操作将以适当的错误调用其完成处理程序。
    有关更多信息,请参见:NSURLSessionStreamTask

常见方法:

#pragma mark ------------  普通数据请求方法  ------------
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request;
- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url;

#pragma mark ------------  上传方法  ------------
//上传NSData类型的数据
//以数据流的方式进行上传,这种方式好处就是大小不受限制
// bodyData 请求的正文数据
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData;

//创建一个任务,该任务执行HTTP请求以上传指定的文件
//fileURL 要上传本地的文件的URL
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request   fromFile:(NSURL *)fileURL;

//提供URL、缓存策略、请求类型等的URL请求对象。
//request 通过流数据初始化的请求对象,上传流数据
//这个请求对象request中的body流和body数据被忽略,而由fromData提供,会话调用其委托的URLSession:task:needNewBodyStream:方法来提供body数据。
- (NSURLSessionUploadTask *)uploadTaskWithStreamedRequest:(NSURLRequest *)request;



#pragma mark ------------  下载方法  ------------
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request;
//通过之前已经下载的数据来创建下载任务,以恢复先前取消或失败的下载,支持断点下载
//resumeData 之前已经下载的数据。 
- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData;


#pragma mark ------------  基于流的任务方法  ------------
//创建 一个 给定主机 和 端口 的双向流任务
- (NSURLSessionStreamTask *)streamTaskWithHostName:(NSString *)hostname port:(NSInteger)port;
//使用NSNetService创建一个双向流任务来标识端点。NSNetService将在任何IO完成之前被解析。
- (NSURLSessionStreamTask *)streamTaskWithNetService:(NSNetService *)service;

/*
  minBytes:读取的最小字节数
  maxBytes:读取的最大字节数
  timeout:读取字节超时。如果读取未在指定的时间间隔内完成,则读取将被取消,并且会调用errorCompleteHandler。 传递0以防止读取超时。

completionHandler :读取所有字节或发生错误时调用的完成处理程序。 该处理程序在委托队列上执行。

该完成处理程序采用以下参数:  
  
  data: 从流中读取的数据
  atEOF:流是否到达文件结尾(EOF),从而无法读取更多数据
  error:一个错误对象,指示读取失败的原因,如果读取成功,则为nil 
*/
- (void)readDataOfMinLength:(NSUInteger)minBytes 
                  maxLength:(NSUInteger)maxBytes 
                    timeout:(NSTimeInterval)timeout 
          completionHandler:(void (^)(NSData *data, BOOL atEOF, NSError *error))completionHandler;

/*
   异步将指定的数据写入流,并在完成时调用处理程序
   
 data:要写入的数据
 timeout :写入字节超时。 如果写操作未在指定的间隔内完成,则取消写操作,并且会调用errorCompleteHandler。 传递0以防止写超时。
*/
- (void)writeData:(NSData *)data 
          timeout:(NSTimeInterval)timeout 
completionHandler:(void (^)(NSError *error))completionHandler;

//获取流
 //完成所有已排队的读取和写入,然后调用URLSession:streamTask:didBecomeInputStream:outputStream:delegate消息。
// 收到该消息时,任务对象被视为已完成,并且不会再收到任何委托消息。
- (void)captureStreams;

//完成所有排队的读写操作,然后关闭底层套接字的读端。
//调用此方法后,可以继续使用writeData:timeout:completionHandler:方法写入数据。 
//调用此方法后,对readDataOfMinLength:maxLength:timeout:completionHandler:的任何调用都会导致错误。 
- (void)closeRead;

//在调用closeWrite方法之后,你可以使用readDataOfMinLength:maxLength:timeout:completionHandler:方法继续读取数据。
//在调用这个方法之后,任何对writeData:timeout:completionHandler:的调用都会导致一个错误。
//因为服务器可能会继续向客户端写入字节,所以建议您继续读取,直到流到达文件结束符(EOF)。

//完成所有排队的读写操作,然后关闭底层套接字的写端。
- (void)closeWrite;
//完成所有进入队列的读写操作,并建立安全连接。
//使用URLSession:task:didReceiveChallenge:completionHandler:方法,将身份验证回调发送到会话的委托。 
- (void)startSecureConnection;

#pragma mark ------------ 控制任务状态  ------------
//取消当前请求。任务会被标记为取消,并在未来某个时间调用URLSession:task:didCompleteWithError:方法。
-(void)cancel;
//开始或继续请求,创建后的task默认是挂起的,需要手动调用resume才可以开始请求。
//如果任务被挂起,则恢复任务。
-(void)resume;
//挂起当前请求(暂时中止任务)。主要是下载请求用的多一些,
//普通请求挂起后都会重新开始请求。下载请求挂起后,
//只要不超过NSURLRequest设置的timeout时间,调用resume就是继续请求。
-(void) suspend;


常见属性:

#pragma mark ------- 获取任务进展 -------

//进度,整个任务进度的表示
@property (readonly, strong) NSProgress *progress;
//task 期望在请求体中发送的字节数,和 Content-Length of the HTTP request 有关
@property (readonly) int64_t countOfBytesExpectedToSend;
//task 期望在响应体中从服务器接收到的字节数,通常来自HTTP响应的内容长度标题。
@property(readonly) int64_t countOfBytesExpectedToReceive;
// task 在响应体中从服务器接收到的字节数,实际接受的字节数。
@property (readonly) int64_t countOfBytesReceived;
// task 在请求体中发送给服务器的字节数,实际发送的字节数。
@property (readonly) int64_t countOfBytesSent;

#pragma mark ------- 获取常规任务信息  -------

//当前任务的状态 :活动,暂停,正在取消或完成的过程,
//可以通过KVO的方式监听状态的改变。
@property(readonly) NSURLSessionTaskState state;

//此任务的标识符,由所属会话分配且唯一,多个session之间可能存在相同的标识
@property (readonly)  NSUInteger    taskIdentifier;
//主要用于重定向操作,用来记录重定向前的请求(创建任务时传递的原始请求对象)
//如果这是一个流任务,可能是nil
@property (nullable, readonly, copy) NSURLRequest  *originalRequest;
//该任务当前正在处理的URL请求对象。一般和originalRequest是一样的,除非发生重定向才会有所区别
@property (nullable, readonly, copy) NSURLRequest  *currentRequest;

//在给定会话中唯一标识任务的标识符
@property(readonly) NSUInteger taskIdentifier;
//服务器对当前活动请求的响应。
@property(nullable, readonly, copy) NSURLResponse *response;
//任务描述
@property(copy) NSString *taskDescription;

//您希望主机处理任务的相对优先级,指定为0.0(最低优先级)到1.0(最高优先级)之间的浮点值。您可以随时指定或更改任务的优先级,但是并非所有网络协议都可以在任务启动后响应更改。 没有API可让您从主机的角度确定任务的有效优先级。 
@property float priority;

五、AFNetWorking 的简单使用

AFNetworking4.0 是对NSURLSession的封装,之前版本有NSURLConnection的封装,现在已经被废弃。

简单聊一下,为啥AF要弃用之前的NSURLConnection封装,改成对NSURLSession封装。

首先,NSURLSession是在iOS7.0的时候苹果推出来的。而NSURLSession又能支持Http2.0的。大家都知道Http是基于TCP协议的,早期的Http是短连接的,每次传输数据都需要重新连接,而每次连接的话需要进行三次握手,这就造成了资源以及时间的浪费。然后,在Http2.0的时候更新了Connection:keep-alive选项,这个优化项,使客户端与服务器在相同config的时候复用了同一个TCP连接,减少了每次请求的时间,提升了数据的传输速率。所以,AFNetworking也果断的改变成对NSURLSession的封装。

了解一下AFNetWorking 的 体系结构

NSURLSession

主要对象NSURLSession对象进行了进一步的封装,包含以下核心的类:

  • AFURLSessionManager
  • AFHTTPSessionManager

Serialization

提供了与解析数据相关的操作接口,包含以下核心的类:

  • AFURLRequestSerialization (请求序列化协议)
  • AFHTTPRequestSerializer (请求序列化父类)
    • AFJSONRequestSerializer
    • AFPropertyListRequestSerializer
  • AFURLResponseSerialization (响应序列化协议)
  • AFHTTPResponseSerializer(响应序列化父类)
    • AFJSONResponseSerializer
    • AFXMLParserResponseSerializer
    • AFXMLDocumentResponseSerializer (macOS)
    • AFPropertyListResponseSerializer
    • AFImageResponseSerializer
    • AFCompoundResponseSerializer

AFHTTPResponseSerializer 和 AFHTTPRequestSerializer 都'符合' AFURLRequestSerialization '和' AFURLResponseSerialization '协议

Additional Functionality

额外功能

  • AFSecurityPolicy
    AFSecurityPolicy 提供了与安全性相关的操作接口
  • AFNetworkReachabilityManager
    AFNetworkReachabilityManager 提供了与网络状态相关的操作接口

UIKit,提供了大量网络请求过程中与UI界面显示相关的操作接口,通常用于网络请求过程中提示,使用户交互更加友好,包含以下核心的分类/类:

  • AFAutoPurgingImageCache
  • AFImageDownloader
  • AFNetworkActivityIndicatorManager
  • UIActivityIndicatorView+AFNetworking
  • UIButton+AFNetworking
  • UIImageView+AFNetworking
  • UIProgressView+AFNetworking
  • UIRefreshControl+AFNetworking
  • WKWebView+AFNetworking
初始化

1、AFHttpSessonManager 继承 自AFURLSessonManager。
2、AFHttpSessonManager 内部实现并不是一个单例,而是每次都会创建一个新的HttpSessonManager对象。
3、其中父类(AFURLSessonManager)的初始化方法主要设置了configuration,并且通过设置maxConcurrentOperationCount = 1设置了一个串行队列。以及解析的方式、网络状态的监听并创建了一个lock来保证线程安全等

  • 1.AFHTTPSessionManager 类初始化

- (instancetype)initWithBaseURL:(NSURL *)url
           sessionConfiguration:(NSURLSessionConfiguration *)configuration
{
    self = [super initWithSessionConfiguration:configuration];
    if (!self) {
        return nil;
    }
 
    // 拼接路径
    if ([[url path] length] > 0 && ![[url absoluteString] hasSuffix:@"/"]) {
        url = [url URLByAppendingPathComponent:@""];
    }
 
    // 设置baseURL
    self.baseURL = url;
 
    // 请求序列化类
    self.requestSerializer = [AFHTTPRequestSerializer serializer];
    // 响应序列化类
    self.responseSerializer = [AFJSONResponseSerializer serializer];
 
    return self;
}

2.AFURLSessionManager 类初始化

- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
    self = [super init];
    if (!self) {
        return nil;
    }
 
    // 设置初始化NSURLSessionConfiguration
    if (!configuration) {
        configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    }
 
    self.sessionConfiguration = configuration;
 
    // 设置操作队列及控制最大并发数
    self.operationQueue = [[NSOperationQueue alloc] init];
    self.operationQueue.maxConcurrentOperationCount = 1;
 
    // 设置NSURLSession
    self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
    // 解析方式,设置默认最后解析接口返回数据类(用什么方式去解析)
    self.responseSerializer = [AFJSONResponseSerializer serializer];
    // 设置一些服务器认证请求
    self.securityPolicy = [AFSecurityPolicy defaultPolicy];
 
    // 实时监控当前网络状况
    self.reachabilityManager = [AFNetworkReachabilityManager sharedManager];
 
    // 加锁  来保证线程安全
    self.lock = [[NSLock alloc] init];
    self.lock.name = AFURLSessionManagerLockName;
 
    // 这是为了防止后台回来,重新初始化这个session,一些之前的后台请求任务,导致程序的crash。
    // 将所有回调清nil
    [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
        for (NSURLSessionDataTask *task in dataTasks) {
            [self addDelegateForDataTask:task uploadProgress:nil downloadProgress:nil completionHandler:nil];
        }
 
        for (NSURLSessionUploadTask *uploadTask in uploadTasks) {
            [self addDelegateForUploadTask:uploadTask progress:nil completionHandler:nil];
        }
 
        for (NSURLSessionDownloadTask *downloadTask in downloadTasks) {
            [self addDelegateForDownloadTask:downloadTask progress:nil destination:nil completionHandler:nil];
        }
    }];
 
    return self;
}
 
请求方法
  • 1.AFHTTPSessionManager 请求方法

// GET
- (NSURLSessionDataTask *)GET:(NSString *)URLString
                   parameters:(id)parameters
                      headers:(nullable NSDictionary <NSString *, NSString *> *)headers
                     progress:(void (^)(NSProgress * _Nonnull))downloadProgress
                      success:(void (^)(NSURLSessionDataTask * _Nonnull, id _Nullable))success
                      failure:(void (^)(NSURLSessionDataTask * _Nullable, NSError * _Nonnull))failure;
 
// HEAD
- (NSURLSessionDataTask *)HEAD:(NSString *)URLString
                    parameters:(id)parameters
                       headers:(NSDictionary<NSString *,NSString *> *)headers
                       success:(void (^)(NSURLSessionDataTask * _Nonnull))success
                       failure:(void (^)(NSURLSessionDataTask * _Nullable, NSError * _Nonnull))failure;
 
// POST
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
                             parameters:(nullable id)parameters
                                headers:(nullable NSDictionary <NSString *, NSString *> *)headers
                               progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
                                success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
                                failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;
 
// PUT
- (NSURLSessionDataTask *)PUT:(NSString *)URLString
                   parameters:(id)parameters
                      headers:(NSDictionary<NSString *,NSString *> *)headers
                      success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
                      failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure;
 
// PATCH
- (NSURLSessionDataTask *)PATCH:(NSString *)URLString
                     parameters:(id)parameters
                        headers:(NSDictionary<NSString *,NSString *> *)headers
                        success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
                        failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure;
 
// DELETE
- (NSURLSessionDataTask *)DELETE:(NSString *)URLString
                      parameters:(id)parameters
                         headers:(NSDictionary<NSString *,NSString *> *)headers
                         success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
                         failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure;
  • GET:用于获取服务器上的数据,可将参数拼接在请求URL后面,上传参数有限制,默认为1024;
    • HEAD:用于获取服务器响应首部,不包含实体部分,方便用于根据服务器响应字段,更好地在对请求方式首部进行设置,更精确得去获取到自己所需要的数据;
  • POST:用于获取服务器上的数据,可将参数放在请求BODY中,参数个数无限制,POST相对应GET请求安全一点;
  • PUT:通常用于向服务器发送请求,如果URL不存在,则要求服务器根据请求创建资源,如果存在,服务器就接受请求内容,并修改URL资源的原始内容;
  • PATCH:请求服务器对资源进行局部更新,与PUT方法的区别是,PATCH可用于更新局部资源或字段,而PUT则是更新整个资源文件,要求PUT资源一定是完整的;
  • DELETE: 请求服务器删除指定的资源;
  • 2. AFURLSessionManager 请求方法

- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request
                               uploadProgress:(nullable void (^)(NSProgress *uploadProgress))uploadProgressBlock
                             downloadProgress:(nullable void (^)(NSProgress *downloadProgress))downloadProgressBlock
                            completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject,  NSError * _Nullable error))completionHandler;


- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request
                                         fromFile:(NSURL *)fileURL
                                         progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgressBlock
                                completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject, NSError  * _Nullable error))completionHandler;

- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request
                                         fromData:(nullable NSData *)bodyData
                                         progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgressBlock
                                completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject, NSError * _Nullable error))completionHandler;

- (NSURLSessionUploadTask *)uploadTaskWithStreamedRequest:(NSURLRequest *)request
                                                 progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgressBlock
                                        completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject, NSError * _Nullable error))completionHandler;


- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request
                                             progress:(nullable void (^)(NSProgress *downloadProgress))downloadProgressBlock
                                          destination:(nullable NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination
                                    completionHandler:(nullable void (^)(NSURLResponse *response, NSURL * _Nullable filePath, NSError * _Nullable error))completionHandler;


- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData
                                                progress:(nullable void (^)(NSProgress *downloadProgress))downloadProgressBlock
                                             destination:(nullable NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination
                                       completionHandler:(nullable void (^)(NSURLResponse *response, NSURL * _Nullable filePath, NSError * _Nullable error))completionHandler;

请求方法的使用:

1、Creating a Download Task
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];

NSURL *URL = [NSURL URLWithString:@"http://example.com/download.zip"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];

NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:nil destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
    NSURL *documentsDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
    return [documentsDirectoryURL URLByAppendingPathComponent:[response suggestedFilename]];
} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
    NSLog(@"File downloaded to: %@", filePath);
}];
[downloadTask resume];
2、Creating an Upload Task
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];

NSURL *URL = [NSURL URLWithString:@"http://example.com/upload"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];

NSURL *filePath = [NSURL fileURLWithPath:@"file://path/to/image.png"];
NSURLSessionUploadTask *uploadTask = [manager uploadTaskWithRequest:request fromFile:filePath progress:nil completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
    if (error) {
        NSLog(@"Error: %@", error);
    } else {
        NSLog(@"Success: %@ %@", response, responseObject);
    }
}];
[uploadTask resume];
Creating an Upload Task for a Multi-Part Request, with Progress
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];

NSURL *URL = [NSURL URLWithString:@"http://example.com/upload"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];

NSURL *filePath = [NSURL fileURLWithPath:@"file://path/to/image.png"];
NSURLSessionUploadTask *uploadTask = [manager uploadTaskWithRequest:request fromFile:filePath progress:nil completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
    if (error) {
        NSLog(@"Error: %@", error);
    } else {
        NSLog(@"Success: %@ %@", response, responseObject);
    }
}];
[uploadTask resume];

Creating an Upload Task for a Multi-Part Request, with Progress
NSMutableURLRequest *request = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:@"POST" URLString:@"http://example.com/upload" parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
        [formData appendPartWithFileURL:[NSURL fileURLWithPath:@"file://path/to/image.jpg"] name:@"file" fileName:@"filename.jpg" mimeType:@"image/jpeg" error:nil];
    } error:nil];

AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];

NSURLSessionUploadTask *uploadTask;
uploadTask = [manager
              uploadTaskWithStreamedRequest:request
              progress:^(NSProgress * _Nonnull uploadProgress) {
                  // This is not called back on the main queue.
                  // You are responsible for dispatching to the main queue for UI updates
                  dispatch_async(dispatch_get_main_queue(), ^{
                      //Update the progress view
                      [progressView setProgress:uploadProgress.fractionCompleted];
                  });
              }
              completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
                  if (error) {
                      NSLog(@"Error: %@", error);
                  } else {
                      NSLog(@"%@ %@", response, responseObject);
                  }
              }];

[uploadTask resume];
3、 Creating a Data Task
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];

NSURL *URL = [NSURL URLWithString:@"http://httpbin.org/get"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];

NSURLSessionDataTask *dataTask = [manager dataTaskWithRequest:request completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
    if (error) {
        NSLog(@"Error: %@", error);
    } else {
        NSLog(@"%@ %@", response, responseObject);
    }
}];
[dataTask resume];

请求 AFURLRequestSerialization

- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
                                 URLString:(NSString *)URLString
                                parameters:(id)parameters
                                     error:(NSError *__autoreleasing *)error
{
    NSURL *url = [NSURL URLWithString:URLString];
 
    NSParameterAssert(url);
 
    NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
    // 设置请求方式
    mutableRequest.HTTPMethod = method;
 
    // 通过监听用户mutableRequest里的属性值是否改变,
    //若有则将该属性及属性值设置给mutableRequest
    for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
        
        if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
            [mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
        }
    }
 
    // 设置请求参数字段
    mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters error:error] mutableCopy];
 
    return mutableRequest;
}
 
//设置请求首部及请求参数字段数据body
- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
                               withParameters:(id)parameters
                                        error:(NSError *__autoreleasing *)error
{
    NSMutableURLRequest *mutableRequest = [request mutableCopy];
 
    // 用户通过当前类添加需要设置的首部属性值,在该类中是将属性值加到self.HTTPRequestHeaders中的
    [self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) {
        
        //若首部中未包含此属性则添加该属性到该请求的首部HTTPHeader中
        if (![request valueForHTTPHeaderField:field]) { 
            [mutableRequest setValue:value forHTTPHeaderField:field];
        }
    }];
 
    NSString *query = nil;
    if (parameters) {
        if (self.queryStringSerialization) { // 用户自定义 实现拼接参数方法
            NSError *serializationError;
            query = self.queryStringSerialization(request, parameters, &serializationError);
        } else {
            switch (self.queryStringSerializationStyle) { // 默认样式
                case AFHTTPRequestQueryStringDefaultStyle:
                    // 默认样式进行将字典转换后的参数拼接字符串
                    query = AFQueryStringFromParameters(parameters);
                    break;
            }
        }
    }
 
    // self.HTTPMethodsEncodingParametersInURI包含@"GET", @"HEAD", @"DELETE",使用拼接在url后面
    if ([self.HTTPMethodsEncodingParametersInURI containsObject:[[request HTTPMethod] uppercaseString]]) {
        if (query && query.length > 0) {
            //URL.query判断是否已经有数据参数,若有则直接用&拼接,否则用?拼接
            mutableRequest.URL = [NSURL URLWithString:[[mutableRequest.URL absoluteString] stringByAppendingFormat:mutableRequest.URL.query ? @"&%@" : @"?%@", query]];
        }
    } else {
        if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) {
            // POST请求设置body的默认编码方式
            [mutableRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
        }
        
        // 用NSUTF8StringEncoding编码格式将query参数字符串转为NSData
        [mutableRequest setHTTPBody:[query dataUsingEncoding:self.stringEncoding]];
    }
 
    return mutableRequest;
}

  • 手动监听mutableRequest中网络请求属性的最新值,若出现了改变,则实时更新并设置到mutableRequest中;

  • 将用户设置的请求首部设置给mutableRequest的HTTPHeaderField;
    将请求参数进行拼接设置给mutableRequest的HTTPBody;

  • 通过对mutableRequest的相关属性设置参数值后,将返回的mutableRequest生成任务Task并开启任务,此时一个请求任务启动,等待网络请求结果!

请求回调代理

- (void)addDelegateForDataTask:(NSURLSessionDataTask *)dataTask
                uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock
              downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock
             completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler
{
    // 将delegate与manage之间进行关联
    AFURLSessionManagerTaskDelegate *delegate = [[AFURLSessionManagerTaskDelegate alloc] initWithTask:dataTask];
    delegate.manager = self;
    delegate.completionHandler = completionHandler;
 
    dataTask.taskDescription = self.taskDescriptionForSessionTasks;
    [self setDelegate:delegate forTask:dataTask];
 
    delegate.uploadProgressBlock = uploadProgressBlock;
    delegate.downloadProgressBlock = downloadProgressBlock;
}
 
- (void)URLSession:(NSURLSession *)session
              task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
    AFURLSessionManagerTaskDelegate *delegate = [self delegateForTask:task];
 
    // 数据请求完成时回调AFURLSessionManagerTaskDelegate的代理方法
    if (delegate) {
        [delegate URLSession:session task:task didCompleteWithError:error];
 
        [self removeDelegateForTask:task];
    }
 
    if (self.taskDidComplete) {
        self.taskDidComplete(session, task, error);
    }
}
 
// AFURLSessionManagerTaskDelegate的代理方法
- (void)URLSession:(__unused NSURLSession *)session
              task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
    __strong AFURLSessionManager *manager = self.manager;
 
    // 省略部分代码
 
    if (error) { // 出错时的处理
        userInfo[AFNetworkingTaskDidCompleteErrorKey] = error;
 
        dispatch_group_async(manager.completionGroup ?: url_session_manager_completion_group(), manager.completionQueue ?: dispatch_get_main_queue(), ^{
            if (self.completionHandler) {
                self.completionHandler(task.response, responseObject, error);
            }
 
        });
    } else { // 未出错时的处理
        dispatch_async(url_session_manager_processing_queue(), ^{
 
            responseObject = [manager.responseSerializer responseObjectForResponse:task.response data:data error:&serializationError];
 
    }
}

完成数据请求时,会回调didCompleteWithError对数据进行相关处理,这里若manager设置了跟AFURLSessionManagerTaskDelegate关联起来,则在任务完成回调后,将调用AFURLSessionManagerTaskDelegate类中的方法对数据进行处理,数据处理的方式按照responseSerializer对应设置的类型方式进行!

响应处理AFURLResponseSerialization

- (id)responseObjectForResponse:(NSURLResponse *)response
                           data:(NSData *)data
                          error:(NSError *__autoreleasing *)error
{
 
    // 省略部分代码
    for (id <AFURLResponseSerialization> serializer in self.responseSerializers) {
 
        NSError *serializerError = nil;
        id responseObject = [serializer responseObjectForResponse:response data:data error:&serializerError];
        if (responseObject) {
            return responseObject;
        }
    }
 
    return [super responseObjectForResponse:response data:data error:error];
}

AFSecurityPolicy安全认证类

随着互联网的快速发展,导致人们的隐私也变得越来越重要了!苹果爸爸一直都很注重用户隐私问题,所以也提倡应更缜密地传输用户数据信息,防止数据泄露!所以在这里也引申出了HTTPS这个概念,HTTPS具体原理可以参考我写的文章 HTTPS协议:叫个外卖咋这么复杂呢!!

  • 苹果认证过程
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;
 
    if (self.sessionDidReceiveAuthenticationChallenge) {
        // 自定义处理证书方式
        disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
    } else {
        // 判断服务器返回的证书是否是服务器信任的
        if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
            if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) { // 通过认证
                // 若验证通过,生成用NSURLCredential类生成证书
                credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
                if (credential) {
                    // NSURLSessionAuthChallengeUseCredential为使用正式
                    disposition = NSURLSessionAuthChallengeUseCredential;
                } else {
                    // 无证书时,忽略证书,这是系统默认的做法
                    disposition = NSURLSessionAuthChallengePerformDefaultHandling;
                }
            } else { // 证书未通过认证,直接取消认证,忽略证书
                disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
            }
        } else {
            // 忽略证书,系统默认处理方式
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        }
    }
 
    if (completionHandler) {
        // 使用completionHandler回调,让系统去对接完成服务器端的认证过程
        completionHandler(disposition, credential);
    }
}

实现HTTPS认证过程中,我们可以使用证书或非证书认证的方式,这个具体看公司需求,下面我们来看看这个具体过程!

CA机构认证证书:这种方式可以让客户端开发感觉很爽,只需要将请求URL中的http换成https后,便可以放心地进行数据传输,因为服务器端只需要将用于认证的加密证书配置为CA机构信任认证的证书即可,这样服务器在第一次加密认证时,用认证证书加密服务器公钥发送给客户端时,当加密数据到达客户端后,客户端自带的CA机构证书会认证服务器加密时使用的加密证书,若验证成功后,客户端便可取出服务器用于客户端传输数据到服务器所使用的公钥!

非CA机构认证证书:这个过程一般是使用服务器端配置的证书,而这个证书并非CA机构认证的,但我们可以先将证书给客户端,让客户端去认可这个配置的证书,完成后面的HTTPS验证过程,若验证通过则双方可进行交换数据!

  • 认证过程

    • 1、当客户端需要对服务器发送到的证书进行认证时,会调用didReceiveChallenge代理方法

    • 2、若证书是否是服务器所信任的,则通过三种认证模式其中的一种进行认证

      • 2.1、AFSSLPinningModeNone:验证是否允许非权威证书(自签名)或是认证机构信任的证书,是的话则验证通过;

      • 2.2、AFSSLPinningModeCertificate:获取客户端证书链中是否有证书包含服务器端证书链中的证书,是的话则验证通过;

      • 2.3、AFSSLPinningModePublicKey:从客户端证书链中将证书转换为公钥key,服务器端证书链也一样转换成公钥key,通过各自的公钥key进行判断,若相等则通过验证;

    1. 若证书验证通过,则生成NSURLCredential证书,使用completionHandler回调,让系统去对接完成服务器端的认证过程

AFNetworkReachabilityManager实时监控

AFNetworkReachabilityManager是AFN提供的可以对网络状态进行检测,并在主线程完成监控状态的回调!苹果对需要联网的应用要求很高,就是必须要进行联网检查。另外,当网络发生异常时能够及时提示用户网络已断开,而不是程序问题造成卡顿;

[[AFNetworkReachabilityManager sharedManager] setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
        // 一共有四种状态
        switch (status) {
            case AFNetworkReachabilityStatusNotReachable:
                NSLog(@"AFNetworkReachability Not Reachable");
                break;
            case AFNetworkReachabilityStatusReachableViaWWAN:
                NSLog(@"AFNetworkReachability Reachable is WWAN");
                break;
            case AFNetworkReachabilityStatusReachableViaWiFi:
                NSLog(@"AFNetworkReachability Reachable is WiFi");
                break;
            case AFNetworkReachabilityStatusUnknown:
            default:
                NSLog(@"AFNetworkReachability Unknown");
                break;
        }
    }];
[[AFNetworkReachabilityManager sharedManager] startMonitoring];

当我们使用单例sharedManager简单地使用startMonitoring开启网络检测,可以实时检测到网络状态,但是存在一点不足之处

但是这里并不能检测到服务器是否真的可达,只能检测设备是否连接到局域网,以及用的WiFi还是WWAN。即使把设备网络关了,立马检测出NotReachable,连接到路由器立马检测出的还是可以连接到WIFI !有时候虽然联网了,但是网络不一定就能上网是一种可能性,另一种可能性是数据包在传输过程中可以会受影响而被丢弃,毕竟数据包在网际层传输,每经过一个路由器默认经过一跳,但达到255跳时,路由器就会自动丢包!

参考文献:NSURLSession.h
苹果官网 NSURLSession
https://www.jianshu.com/p/ac79db251cbf
https://blog.csdn.net/jbr5740/article/details/89598766

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容