NSURLSession 未下载完整数据,但成功完成
Posted
技术标签:
【中文标题】NSURLSession 未下载完整数据,但成功完成【英文标题】:NSURLSession not downloading full data, but finnish with success 【发布时间】:2018-07-06 15:20:39 【问题描述】:场景:我正在使用 NSURLSession 从 EWS API 下载一些大附件 (30-50 mb)。并将下载的 xml 数据保存到文件中。
我创建了使用 NSURLSession 的 HTTP 类,处理委托回调并有一个完成处理程序。 HTTP 类创建自己的 NSURLSession 并开始下载数据。这是我的 HTTP.m
//
// HTTP.m
// Download
//
// Created by Ankush Kushwaha on 7/6/18.
// Copyright © 2018 Ankush Kushwaha. All rights reserved.
//
#import "HTTP.h"
typedef void (^httpCompletionBlock)(NSData* result);
@interface HTTP()
@property (nonatomic) NSMutableData * data;
@property (nonatomic) NSString *fileNametoSaved;
@property (nonatomic) httpCompletionBlock completion;
@end
@implementation HTTP
- (instancetype)initWithAttachmntId:(NSString *)attachmentId
fileName:(NSString *)fileName
completion:(void (^)(NSData* result))completion
self = [super init];
if (self)
self.data = [NSMutableData data];
self.completion = completion;
self.fileNametoSaved = fileName;
NSURL *requestUrl = [NSURL URLWithString:@"https://outlook.office365.com/EWS/Exchange.asmx"];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:requestUrl];
request.HTTPMethod = @"POST";
NSString *soapXmlString = [NSString stringWithFormat:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
"<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
"xmlns:m=\"http://schemas.microsoft.com/exchange/services/2006/messages\"\n"
"xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\"\n"
"xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">\n"
"<soap:Body>\n"
"<m:GetAttachment>\n"
"<m:AttachmentIds>\n"
"<t:AttachmentId Id=\"%@\"/>\n"
"</m:AttachmentIds>\n"
"</m:GetAttachment>\n"
"</soap:Body>\n"
"</soap:Envelope>\n",attachmentId];
if (soapXmlString)
NSString *xmlLength = [NSString stringWithFormat:@"%ld", (unsigned long)soapXmlString.length];
request.HTTPBody = [soapXmlString dataUsingEncoding:NSUTF8StringEncoding];
[request addValue:@"text/xml; charset=utf-8" forHTTPHeaderField:@"Content-Type"];
[request addValue:xmlLength forHTTPHeaderField:@"Content-Length"];
dispatch_async(dispatch_get_main_queue(), ^
NSURLSessionConfiguration *defaultConfiguration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration:defaultConfiguration
delegate:self
delegateQueue:nil];
NSURLSessionDataTask *dataTask = [defaultSession dataTaskWithRequest:request];
[dataTask resume];
);
return self;
-(void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler
if (challenge.previousFailureCount == 0)
NSURLCredential* credential;
credential = [NSURLCredential credentialWithUser:@"MY_OUTLOOK.COM EMAIL" password:@"PASSWORD" persistence:NSURLCredentialPersistenceForSession];
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
else
// URLSession:task:didCompleteWithError delegate would be called as we are cancelling the request, due to wrong credentials.
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
-(void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
completionHandler(NSURLSessionResponseAllow);
-(void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data
[self.data appendData:data];
// NSLog(@"data : %lu", (unsigned long)self.data.length);
-(void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
NSLog(@"didCompleteWithError: %@", error);
if (error)
NSLog(@"Error: %@", error);
else
NSData *data;
if (self.data)
data = [NSData dataWithData:self.data];
NSLog(@"Success : %lu", (unsigned long)self.data.length);
NSString *filePath = [NSString stringWithFormat:@"/Users/startcut/Desktop/xxx/%@",
self.fileNametoSaved];
NSString *xmlString = [[NSString alloc] initWithData:self.data encoding:NSUTF8StringEncoding];
[xmlString writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
self.completion ? self.completion(self.data) : nil;
[session finishTasksAndInvalidate]; // We must release the session, else it holds strong referance for it's delegate (in our case EWSHTTPRequest).
// And it wont allow the delegate object to free -> cause memory leak
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
newRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler;
NSString *redirectLocation = request.URL.absoluteString;
if (response)
completionHandler(nil);
else
completionHandler(request); // new redirect request
@end
在我的 ViewController 中,我发出 5 个 HTTP 请求,以下载 5 个不同的附件。
HTTP *http = [[HTTP alloc] initWithAttachmntId:@"AAAaAGFua3VzaC5zdGFyY3V0QG91dGxvb2suY29tAEYAAAAAACd30qZd6oFAvoaMby5vOMUHAOsTbManU6VPoeQkUTl4/J0AAWUo5o0AAOsTbManU6VPoeQkUTl4/J0AAWruTj0AAAESABAAWGs6REUQc02OHF0x6uYJ+g=="
fileName:@"http1"
completion:^(NSData *result)
NSLog(@"Completion 1");
];
HTTP *http2 = [[HTTP alloc] initWithAttachmntId:@"AAAaAGFua3VzaC5zdGFyY3V0QG91dGxvb2suY29tAEYAAAAAACd30qZd6oFAvoaMby5vOMUHAOsTbManU6VPoeQkUTl4/J0AAWUo5o0AAOsTbManU6VPoeQkUTl4/J0AAWruTjsAAAESABAAP8zebUI1fkSiE8tQ+RtwiQ=="
fileName:@"http2"
completion:^(NSData *result)
NSLog(@"Completion 2");
];
HTTP *http3 = [[HTTP alloc] initWithAttachmntId:@"AAAaAGFua3VzaC5zdGFyY3V0QG91dGxvb2suY29tAEYAAAAAACd30qZd6oFAvoaMby5vOMUHAOsTbManU6VPoeQkUTl4/J0AAWUo5o0AAOsTbManU6VPoeQkUTl4/J0AAWruTjkAAAESABAAiPaJIPjp/k6iQHSMpi6aDw=="
fileName:@"http3"
completion:^(NSData *result)
NSLog(@"Completion 3");
];
HTTP *http4 = [[HTTP alloc] initWithAttachmntId:@"AAAaAGFua3VzaC5zdGFyY3V0QG91dGxvb2suY29tAEYAAAAAACd30qZd6oFAvoaMby5vOMUHAOsTbManU6VPoeQkUTl4/J0AAWUo5o0AAOsTbManU6VPoeQkUTl4/J0AAWruTjwAAAESABAA86vBkFlTNU2oEVq/eRtLGQ=="
fileName:@"http4"
completion:^(NSData *result)
NSLog(@"Completion 4");
];
HTTP *http5 = [[HTTP alloc] initWithAttachmntId:@"AAAaAGFua3VzaC5zdGFyY3V0QG91dGxvb2suY29tAEYAAAAAACd30qZd6oFAvoaMby5vOMUHAOsTbManU6VPoeQkUTl4/J0AAWUo5o0AAOsTbManU6VPoeQkUTl4/J0AAWruTjoAAAESABAAND6qbOQbnkyoyg0K17T9/Q=="
fileName:@"http5"
completion:^(NSData *result)
NSLog(@"Completion 5");
];
问题:由于文件或数据与 5 个单独的 HTTP 对象并行下载,最后当 NSUrlSession 会话委托被调用时,我将数据保存到我的 HTTP.m 的 -(void)URLSession (NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
方法中的文件中.大多数情况下,下载的数据(文件)不包含完整数据(例如,如果附件的大小为 30 mb,我的代码会下载 4 mb 或 10 mb 或 3.2 mb 等数据。数字不一致)。似乎 NSURLSession 终止或停止之间的数据下载并成功关闭连接。如果我一次下载 1 个附件(而不是在我的视图控制器中创建 5 个 HTTP 对象,我一次只创建 1 个对象)在大多数情况下它可以工作并下载完整的数据内容。
感谢任何帮助。我从 2 天起就陷入了困境。
【问题讨论】:
你的会话超时了吗? 你说它以成功结束,但只是检查错误对象。响应中的 statusCode 是什么?它可能由于某种原因失败,但没有给出错误。在 didCompleteWithError 方法中检查这个值:((NSHTTPURLResponse *)task.response).statusCode
尝试使用下载任务,而不是数据任务。文档指出:“数据任务用于向服务器发送简短的、通常是交互式的请求。”。
我没有提供任何超时。我试过下载任务,没有帮助。我也会检查状态码,让你们知道。马上离开工作地点。周末愉快
响应状态码为200
【参考方案1】:
不分先后:
您不应该为每个请求都创建一个新会话。这会阻止操作系统正确限制同时请求的数量,并且可能会导致其他问题。同样,您不应在每项任务完成后调用finishTasksAndInvalidate
。
您必须保留对会话的引用,直到没有更多未完成的请求。如果这不适合您的应用架构,您可以考虑使用默认会话而不是提供您自己的会话。
您的 Content-Length 标头值不正确。它应该是 byte 计数,而不是字符计数。首先将字符串转换为编码的 NSData,并将 that 的长度作为 Content-Length 发送。否则,一旦正文中出现一个多字节字符,它就会失败。
理想情况下,您的 didReceiveResponse: 方法应该清除数据存储,以便正确处理多部分响应(最后一个获胜),而不是连接它们。
您编写的身份验证质询处理程序可能会导致严重问题。你应该检查挑战的保护空间,看看它是否是你关心的,如果不是,你应该触发默认处理。否则,如果用户使用任何类型的代理等,您的应用将会失败。
解决这些问题,如果仍然无法正常工作,请就仍然无法正常工作的问题提出一个新问题。 :-)
【讨论】:
【参考方案2】:最后。我找到了原因。不是解决方案:(
它不是来自 ios 代码。 @dgatwood 提到的(谢谢)可能需要一些代码改进,但即使经过改进,我也面临同样的问题。
实际上,EWS 交换受到大数据下载的限制。由于哪个 EWS 服务器终止了两者之间的连接。这是blog
【讨论】:
以上是关于NSURLSession 未下载完整数据,但成功完成的主要内容,如果未能解决你的问题,请参考以下文章
GET 使用 NSURLSession 成功,但使用 AFHTTPSessionManager 失败
在后台使用 NSURLSession 逐个下载 100 个文件的列表