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 委托未调用

在后台使用 NSURLSession 逐个下载 100 个文件的列表

将 NSData 从 NSURLSession 转换为 JSON

NSURLSession - 请求超时

使用 NSURLSession UploadTask 暂停、恢复、取消上传任务