2018-08-23 NSURLSession

NSURLSession 概述

使用session完成网络通信,涉及以下几个类和协议:

  • NSURLSessionConfiguration
  • NSURLSesssion
  • NSURLSessionTask(NSURLSessionDataTask,NSURLSessionUploadTask,NSURLSessionDownloadTask)
  • NSURLSessionDelegate群
  • NSURLRequest , NSURLResponse

NSURLSessionConfiguration

所有共用session的task,共同拥有此session的NSURLSessionConfiguration配置。

1.单例session

没有configuration,用于最基础的请求。

2.默认session

与shareSession类似,不同在于,可设置你需要的配置,可在代理中递增的获取数据。
调用NSURLSessionConfiguration的defaultSessionConfiguration生成。

3.ephemeral session,临时配置

类似于shareSession,不同在于不能写caches,cookies,证书至disk。
调用NSURLSessionConfiguration的ephemeralSessionConfiguration生成。

4.background session,后台session。

被非app的进程唤醒,根据唯一的identifier来调用。
调用NSURLSessionConfiguration的backgroundSessionConfiguration生成。

限制:
a.必须提供代理
b.只支持http/https,不支持custom protocol
c.redirected always followed
d.上传操作,只支持文件数据


NSURLSession

两种生成方式

  • 单例模式 [NSURLSession sharedSession]
  • 自己定制[ [NSURLSession alloc] init]

支持两种回调方式,block和delegate。
支持操作:canceling , restarting,resuming,suspending。
支持URL Schemes:data,file,ftp,http,https
支持transport support:proxy servers,socks gateways。
支持http1.1,spdy,http2(RFC 7540,要求server支持ALPN或IVPN),custom protocols。
session 对其代理,是强引用,仅在invalid、app退出,系统报错的时候才会释放。所以要记得对不适用的session设invalid。
invalid和complete的区别:
invalid 之后,再次使用,会启用旧session。
complete之后,再次使用,会新建connect。


NSURLSessionTask

分三种task

  • NSURLSessionDataTask
  • NSURLSessionUploadTask
  • NSURLSessionDownloadTask

发送请求

1.shared session

NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
     NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
     NSLog(dataStr,nil);
}];
[dataTask resume];

2.custom configuration session

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    NSLog(@"%@ %@ %@ %@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding], response, error, [NSThread currentThread]);
}];
[dataTask resume];

3.custom configuration request session

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
request.HTTPMethod = @"GET";
request.HTTPBody = [@"username=cjm&password=cjmcjmcjm" dataUsingEncoding:NSUTF8StringEncoding];

NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];

NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
   NSLog(@"%@ %@ %@ %@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding], response, error, [NSThread currentThread]);
}];
[dataTask resume];

返回成功

data:NSData二进制内容,需要自己转成NSString或json等其他格式。

{"tsno":"xxx","code":"xxx","desc":"登录已过期,请重新登录","needSsoLogin":"","recId":"","memo":"","serverUsedTime":"","end":""} 

response

<NSHTTPURLResponse: 0x60000042eb40> 
{ URL: http://120.25.201.54:6085/ns/auth/address/list?os=android&devid=867695020241211&osver=6.0.1&tsno=ABa0e991d5d9de7c278c7f09a0811c798f&ver=1&channel=Official&accessToken=dbeac0a9-a417-4360-88ab-2b4036886bd6 
}
 { Status Code: 200, 
   Headers {
    "Access-Control-Allow-Credentials" =     ( true);
    "Access-Control-Allow-Headers" =     ("Origin, X-Requested-With, Content-Type, Accept");
    "Access-Control-Allow-Methods" =     ("OPTION,POST,GET");
    "Content-Type" =     ("application/json;charset=UTF-8");
    Date =     ("Thu, 23 Aug 2018 08:04:28 GMT");
    Server =     ("Apache-Coyote/1.1");
    "Transfer-Encoding" =     (Identity);
   } 
 }

error:nil

返回失败

data:nil
response:nil
error

Error Domain=NSURLErrorDomain Code=-1004 "Could not connect to the server." UserInfo={NSUnderlyingError=0x60400024aec0 {Error Domain=kCFErrorDomainCFNetwork Code=-1004 "(null)" UserInfo={_kCFStreamErrorCodeKey=61, _kCFStreamErrorDomainKey=1}}, NSErrorFailingURLStringKey=http://120.25.201.54:608500/ns/auth/address/list?os=android&devid=867695020241211&osver=6.0.1&tsno=ABa0e991d5d9de7c278c7f09a0811c798f&ver=1&channel=Official&accessToken=dbeac0a9-a417-4360-88ab-2b4036886bd6, NSErrorFailingURLKey=http://120.25.201.54:608500/ns/auth/address/list?os=android&devid=867695020241211&osver=6.0.1&tsno=ABa0e991d5d9de7c278c7f09a0811c798f&ver=1&channel=Official&accessToken=dbeac0a9-a417-4360-88ab-2b4036886bd6, _kCFStreamErrorDomainKey=1, _kCFStreamErrorCodeKey=61, NSLocalizedDescription=Could not connect to the server.}

4. delegate session

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url];
[dataTask resume];
返回成功
NSURLSessionDataDelegate didReceiveData -- 返回数据
NSURLSessionTaskDelegate didFinishCollectingMetrics --http信息
NSURLSessionTaskDelegate didCompleteWithError -- error为nil
返回失败
NSURLSessionTaskDelegate didFinishCollectingMetrics--http信息
NSURLSessionTaskDelegate didCompleteWithError -- 给出error 描述

session与task的关系

一个session可以执行多个task,并对每个task分配session内唯一的identifier。断点续传的时候,session根据identifier来恢复task。

一个session执行多个task

NSURL *url = [NSURL URLWithString:@"http://xxxx"];
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:nil delegateQueue:nil];
for (int i = 0; i<3; i++) {
    NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
       NSLog([NSString stringWithFormat:@"%@ begin session1",[[NSThread currentThread] description]],nil);
       for (int i = 0; i<1000000000; i++) {
           float a = 999.999 * 999.999;
       }
       NSLog([NSString stringWithFormat:@"%@ end session1",[[NSThread currentThread] description]],nil);
    }];
    NSLog([dataTask description],nil);
    [dataTask resume];
}
image.png
image.png

1.所有task,一经resume,立即发出,立即受到server端的返回数据
2.默认无状态连接,每个task都会新建连接,均有tcp3次握手建立连接、发送数据、4次挥手断开连接的过程。
3.session会安排completionHandler在哪个子线程里执行
4.一个session里的所有completionHandler串行同步分发。所以,即使有第3点,completionHandler仍是串行执行。在上图中,NSThread a040等待NSThread8d80执行完后再执行。

多个session同时执行多个task

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:nil delegateQueue:nil];
    for (int i = 0; i<3; i++) {
        NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            NSLog([NSString stringWithFormat:@"%@ begin session1",[[NSThread currentThread] description]],nil);
            for (int i = 0; i<1000000000; i++) {
                float a = 999.999 * 999.999;
            }
            NSLog([NSString stringWithFormat:@"%@ end session1",[[NSThread currentThread] description]],nil);
        }];
        NSLog([dataTask description],nil);
        [dataTask resume];
    }

    NSURLSessionConfiguration *configuration2 = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *session2 = [NSURLSession sessionWithConfiguration:configuration2 delegate:nil delegateQueue:nil];
    for (int i = 0; i<3; i++) {
        NSURLSessionDataTask *dataTask = [session2 dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            NSLog([NSString stringWithFormat:@"%@ begin session2",[[NSThread currentThread] description]],nil);
            for (int i = 0; i<1000000000; i++) {
                float a = 999.999 * 999.999;
            }
            NSLog([NSString stringWithFormat:@"%@ end session2",[[NSThread currentThread] description]],nil);
        }];
        NSLog([dataTask description],nil);
        [dataTask resume];
    }
image.png

多个session同时执行时,session之间并行执行。如NSThread 2800 begin session2 和 NSThread 30c0 begin session1,是同时开始的。


NSURLSessionDelegate 代理群

不需要实现过多的代理,代理之间互相冲突。如果实现了某些高级代理,那么会影响部分已实现低级代理的调起。
//NSURLSessionDelegate sessiondelegate的基础协议

@protocol NSURLSessionDelegate <NSObject>
@optional
 //session收到的最后一个回调。两种原因:系统故障 or 主动失效(error为nil)
- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullable NSError *)error;

 //connect 出现了level 权限挑战,在这里给连接提供证书。如果没有实现,则会调起默认处理方式?
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
                                             completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler;

 //当application 收到-application:handleEventsForBackgroundURLSession:completionHandler:message消息时,
 此回调会在session的代理中调起,标明此session队列中的消息已全部发送。可以安全的调起之前存储的completion handler,或者
 开始一些周期性的下载
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session API_AVAILABLE(ios(7.0), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos);
@end

NSURLSessionTaskDelegate task的协议

@protocol NSURLSessionTaskDelegate <NSURLSessionDelegate>
@optional

//当具备delayed start time设置的task准备启动的时候被调起。
//completionHandler被唤醒来做一些处理工作,如继续下载,替换request,cancel task
//若该回调没被实现,loading will proceed with the original request.
//只有在设置了earliestBeginDate属性的task,临近过期需要修改优先级来开始网络加载的时候,才需要实现该回调
//如果指定了新的request,新request的allowsCellularAccess不生效,旧request的allowsCellularAccess继续生效
//取消task,会调起URLSession:task:didCompleteWithError:,error为NSURLErrorCancelled
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
                        willBeginDelayedRequest:(NSURLRequest *)request
                              completionHandler:(void (^)(NSURLSessionDelayedRequestDisposition disposition, NSURLRequest * _Nullable newRequest))completionHandler
    API_AVAILABLE(macos(10.13), ios(11.0), watchos(4.0), tvos(11.0));

//当task无法启动网络加载的时候被调起。
//一个task最多调起此代理一次,仅在waitsForConnectivity属性被设置为YES的情况下会被调起。
//后台session不会调起此代理,因为后台session的waitForConnectivity被忽略。
- (void)URLSession:(NSURLSession *)session taskIsWaitingForConnectivity:(NSURLSessionTask *)task
    API_AVAILABLE(macos(10.13), ios(11.0), watchos(4.0), tvos(11.0));

//http request试图进行重定向。必须调起完整路由来允许重定向,提供一个修改的request,
//或给completionHandler传nil来把重定向的响应主体作为有效响应.
//默认是允许重定向。
//因为后台session永远允许重定向,所以该方法不会被调起
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
                     willPerformHTTPRedirection:(NSHTTPURLResponse *)response
                                     newRequest:(NSURLRequest *)request
                              completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler;

 当一个request被需要提供证书时,会被调起。若没有实现该回调session证书不会被调起,会采用默认的处理方式
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
                            didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge 
                              completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler;

 task需要一个新的body stream时,被调起。当含有body stream的请求,被校验失败的时候,必须被调起。
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
                              needNewBodyStream:(void (^)(NSInputStream * _Nullable bodyStream))completionHandler;

 周期性的调起,来反应上传进度。
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
                                didSendBodyData:(int64_t)bytesSent
                                 totalBytesSent:(int64_t)totalBytesSent
                       totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend;

 当task收集到所有静态信息的时候,被调起。
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

 task的最后一个回调。若task正常完成,error为nil
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
                           didCompleteWithError:(nullable NSError *)error;

@end

NSURLSessionDataDelegate

@protocol NSURLSessionDataDelegate <NSURLSessionTaskDelegate>
@optional

 task接收到响应,在completion没被调起之前,不会受到更多的相应。如要继续,执行completion(参数)。
 这样,便于用户取消request,或变更为download request。可选。后台上传任务,不会被调起(不能变更为download task)
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
                                 didReceiveResponse:(NSURLResponse *)response
                                  completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler;

 data task 变更为download task时被调起。data task再不会收到相应
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
                              didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask;

 当data task 变更为双向stram task时,被调起。新建的stream task会携带初始request和response,做为其属性。
 被管道化得request,stream 对象只能做读取操作,对象会被调起-URLSession:writeClosedForStream:。
 session的所有request都能取消管道,或者在NSURLRequest中设置HTTPShouldUsePipelining属性。
 underlying connection,不被认作http连接,不会占用每个host的最大连接数。
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
                                didBecomeStreamTask:(NSURLSessionStreamTask *)streamTask;

data delegate里,唯一一个接收数据的代理 。会多次被调起。
data可被使用的时候调用。这里假设代理会retain数据,而不是copy数据。由于数据可能是不连续的,使用[NSData enumerateByteRangesUsingBlock:]来访问。
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
                                     didReceiveData:(NSData *)data;

 启动有效的缓存机制来缓存数据,或传入nil来阻止缓存。不能依赖这个消息来接收resource data
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
                                  willCacheResponse:(NSCachedURLResponse *)proposedResponse 
                                  completionHandler:(void (^)(NSCachedURLResponse * _Nullable cachedResponse))completionHandler;

@end

1.didReceiveResponse
在这个代理里,调用completionHandler,决定下一步怎么走。
参数:

typedef NS_ENUM(NSInteger, NSURLSessionResponseDisposition) {
    NSURLSessionResponseCancel = 0,                                      /* Cancel the load, this is the same as -[task cancel] */
    NSURLSessionResponseAllow = 1,                                       /* Allow the load to continue */
    NSURLSessionResponseBecomeDownload = 2,                              /* Turn this request into a download */
    NSURLSessionResponseBecomeStream API_AVAILABLE(macos(10.11), ios(9.0), watchos(2.0), tvos(9.0)) = 3,  /* Turn this task into a stream task */
} NS_ENUM_AVAILABLE(NSURLSESSION_AVAILABLE, 7_0);

传入NSURLSessionResponseAllow,则继续下载,进入第2步。
2.didReceiveData,收到数据

NSURLSessionDownloadDelegate

@protocol NSURLSessionDownloadDelegate <NSURLSessionTaskDelegate>

 当download task完成下载时,会调起。在这里应该讲下载到的文件转存到新的地方,这样代理返回的时候,下载下来的文件能够被删除
 URLSession:task:didCompleteWithError:仍然会被调起
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
                              didFinishDownloadingToURL:(NSURL *)location;

@optional
周期性的调起,以反馈下载进度
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
                                           didWriteData:(int64_t)bytesWritten
                                      totalBytesWritten:(int64_t)totalBytesWritten
                              totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite;

 在下载动作重启时调用。当下载动作由于某些原因失败时,error里的userInfo字典会包含NSURLSessionDownloadTaskResumeData。
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
                                      didResumeAtOffset:(int64_t)fileOffset
                                     expectedTotalBytes:(int64_t)expectedTotalBytes;

@end

//NSURLSessionStreamDelegate

@protocol NSURLSessionStreamDelegate <NSURLSessionTaskDelegate>
@optional

/* Indiciates that the read side of a connection has been closed.  Any
 * outstanding reads complete, but future reads will immediately fail.
 * This may be sent even when no reads are in progress. However, when
 * this delegate message is received, there may still be bytes
 * available.  You only know that no more bytes are available when you
 * are able to read until EOF. */
 表示连接的read site已经被关闭。现有的read操作均已完成,接下来的read操作会立即失败。
 然后即使接收到此代理,仍然有bytes能读取。一会有数据读,一会没数据读,究竟是什么意思?
- (void)URLSession:(NSURLSession *)session readClosedForStreamTask:(NSURLSessionStreamTask *)streamTask;

 表示连接的write side 已经被关闭。所有已发出的write操作均完成,未来的write都会立即失败。
- (void)URLSession:(NSURLSession *)session writeClosedForStreamTask:(NSURLSessionStreamTask *)streamTask;

 表示系统检测到更好的链接路径(如wifi可用),暗示在接下来的任务中,可以创建并切换到新的task中。
 但注意,并不保证新的连接路径一定能链接成功,所以要有链接失败的准备
- (void)URLSession:(NSURLSession *)session betterRouteDiscoveredForStreamTask:(NSURLSessionStreamTask *)streamTask;

 given task已经完成,underlying 网络连接已经建立了unopened的NSInputStream和NSOutputStream。
 仅在所有enqueued IO都完成的时候被调起(包括必须的握手)。stramTask不再收到更多的代理消息。
- (void)URLSession:(NSURLSession *)session streamTask:(NSURLSessionStreamTask *)streamTask
                                 didBecomeInputStream:(NSInputStream *)inputStream
                                         outputStream:(NSOutputStream *)outputStream;

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

推荐阅读更多精彩内容