【问题标题】:upload file by streamming :shows error log "The operation couldn’t be completed. (kCFErrorDomainCFNetwork error 303.)"通过流式上传文件:显示错误日志“操作无法完成。(kCFErrorDomainCFNetwork 错误 303。)”
【发布时间】:2013-10-03 06:19:33
【问题描述】:

我正在尝试通过流媒体上传大文件,最近我收到了这个错误日志:

Error Domain=kCFErrorDomainCFNetwork Code=303 "The operation couldn’t be completed. (kCFErrorDomainCFNetwork error 303.)" UserInfo=0x103c0610 {NSErrorFailingURLKey=/adv,/cgi-bin/file_upload-cgic, NSErrorFailingURLStringKey/adv,/cgi-bin/file_upload-cgic}<br>

这是我设置bodystream的地方:

-(void)finishedRequestBody{ // set bodyinput stream
    [self appendBodyString:[NSString stringWithFormat:@"\r\n--%@--\r\n",[self getBoundaryStr]]];
    [bodyFileOutputStream close];
    bodyFileOutputStream = nil;
    //calculate content length
    NSError *fileReadError = nil;
    NSDictionary *fileAttrs = [[NSFileManager defaultManager] attributesOfItemAtPath:pathToBodyFile error:&fileReadError];
    NSAssert1((fileAttrs != nil),@"Couldn't read post body file",fileReadError);
    NSNumber *contentLength = [fileAttrs objectForKey:NSFileSize];

   NSInputStream *bodyStream = [[NSInputStream alloc] initWithFileAtPath:pathToBodyFile];
    [request setHTTPBodyStream:bodyStream];
    [bodyStream release];

    if (staticUpConneciton == nil) {          
        NSURLResponse *response = nil;
        NSError *error = nil;
        NSData *responseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];    
        staticUpConneciton = [[[NSURLConnection alloc]initWithRequest:request delegate:self] retain];                   
    }else{
        staticUpConneciton = [[NSURLConnection connectionWithRequest:request delegate:self]retain];
    }  
}

这就是我写蒸汽的方式:

    -(void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode{
        uint8_t buf[1024*100];
        NSUInteger len = 0;
        switch (eventCode) {
            case NSStreamEventOpenCompleted:
                NSLog(@"media file opened");
                break;
            case NSStreamEventHasBytesAvailable:
              //  NSLog(@"should never happened for output stream");
                len = [self.uploadFileInputStream read:buf maxLength:1024];
                if (len) {
                    [self.bodyFileOutputStream write:buf maxLength:len];
                }else{
                    NSLog(@"buf finished wrote %@",self.pathToBodyFile);
                    [self handleStreamCompletion];
                }
                break;
            case NSStreamEventErrorOccurred:
                NSLog(@"stream error");
                break;
            case NSStreamEventEndEncountered:
                NSLog(@"should never for output stream");
                break;
            default:
                break;
        }
}

关闭直播

-(void)finishMediaInputStream{
    [self.uploadFileInputStream close];
    [self.uploadFileInputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    self.uploadFileInputStream = nil;
}

-(void)handleStreamCompletion{
    [self finishMediaInputStream];
    // finish requestbody
    [self finishedRequestBody];
}

我在实现这个方法的时候发现了那个错误needNewBodyStream:看下面的代码:

-(NSInputStream *)connection:(NSURLConnection *)connection needNewBodyStream:(NSURLRequest *)request{
    [NSThread sleepForTimeInterval:2];
    NSInputStream *fileStream = [NSInputStream inputStreamWithFileAtPath:pathToBodyFile];
    if (fileStream == nil) {
        NSLog(@"NSURLConnection was asked to retransmit a new body stream for a request. returning nil!");
    }
    return fileStream;
}

这是我设置标题和 mediaInputStream 的地方

-(void)setPostHeaders{
    pathToBodyFile = [[NSString alloc] initWithFormat:@"%@%@",NSTemporaryDirectory(),bodyFileName];
    bodyFileOutputStream = [[NSOutputStream alloc] initToFileAtPath:pathToBodyFile append:YES];
    [bodyFileOutputStream open];

    //set bodysteam
    [self appendBodyString:[NSString stringWithFormat:@"--%@\r\n", [self getBoundaryStr]]];
    [self appendBodyString:[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n", @"target_path"]];
    [self appendBodyString:[NSString stringWithFormat:@"/%@",[NSString stringWithFormat:@"%@/%@/%@",UploaderController.getDestination,APP_UPLOADER,[Functions getDateString]]]];
    [self appendBodyString:[NSString stringWithFormat:@"\r\n--%@\r\n", [self getBoundaryStr]]];
    [self appendBodyString:[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"file_path\"; filename=\"%@\"\r\n", fileName]];
    [self appendBodyString:[NSString stringWithString:@"Content-Type: application/octet-stream\r\n\r\n"]];

    NSString *tempFile = [NSTemporaryDirectory() stringByAppendingPathComponent:@"uploadFile"]; 

    NSError *fileReadError = nil;
    NSDictionary *fileAttrs = [[NSFileManager defaultManager] attributesOfItemAtPath:tempFile error:&fileReadError];
    NSAssert1((fileAttrs != nil),@"Couldn't read post body file",fileReadError);
    NSNumber *contentLength = [fileAttrs objectForKey:NSFileSize];
    [request setValue:[contentLength stringValue] forHTTPHeaderField:@"Content-Length"];    
    NSInputStream *mediaInputStream = [[NSInputStream alloc] initWithFileAtPath:tempFile];
    self.uploadFileInputStream = mediaInputStream;    
    [self.uploadFileInputStream setDelegate:self];
    [self.uploadFileInputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [self.uploadFileInputStream open];    
}

这就是我从相机胶卷中复制数据的方式

-(void)copyFileFromCamaroll:(ALAssetRepresentation *)rep{
    //copy the file from the camarall to tmp folder (automatically cleaned out every 3 days)
    NSUInteger chunkSize = 100 * 1024;
    NSString *tempFile = [NSTemporaryDirectory() stringByAppendingPathComponent:@"uploadFile"];
    NSLog(@"tmpfile %@",tempFile);
    uint8_t *chunkBuffer = malloc(chunkSize * sizeof(uint8_t));
    NSUInteger length = [rep size];

    NSFileHandle *fileHandle = [[NSFileHandle fileHandleForWritingAtPath: tempFile] retain];
    if(fileHandle == nil) {
        [[NSFileManager defaultManager] createFileAtPath:tempFile contents:nil attributes:nil];
        fileHandle = [[NSFileHandle fileHandleForWritingAtPath:tempFile] retain];
    }

    NSUInteger offset = 0;
    do {
        NSUInteger bytesCopied = [rep getBytes:chunkBuffer fromOffset:offset length:chunkSize error:nil];
        offset += bytesCopied;
        NSData *data = [[NSData alloc] initWithBytes:chunkBuffer length:bytesCopied];
        [fileHandle writeData:data];
        [data release];
    } while (offset < length);
    [fileHandle closeFile];
    [fileHandle release];
    free(chunkBuffer);
    chunkBuffer = NULL;          
    NSError *error;
    NSData *fileData = [NSData dataWithContentsOfFile:tempFile options:NSDataReadingMappedIfSafe error:&error];
    if (!fileData) {
        NSLog(@"Error %@ %@", error, [error description]);
        NSLog(@"%@", tempFile);
        //do what you need with the error
    }            
}

任何人,有什么想法吗?我错过了什么吗?

【问题讨论】:

标签: ios upload streaming


【解决方案1】:

编辑:

为了提前说明这一点:

在 iOS 7 中,上传大型 文件 可能有一个简单的解决方案。请参考NSURLSessionNSURLSessionTask,特别是:

- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request 
                                         fromFile:(NSURL *)fileURL 
                                completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler;

否则,

您的代码存在许多问题:

  • 多部分消息构造不正确(包括内容长度)。

  • 您使用sendSynchronousRequest 并将它们与委托方法混合。删除该行:

    NSData *responseData = [NSURLConnection sendSynchronousRequest:request  returningResponse:&response error:&error];)    
    
  • 假设您想通过创建临时文件来上传资产,您可以更轻松地完成此操作(从资产创建临时文件)。实际上,您不需要流委托方法。避免临时文件的另一种方法需要“绑定的流对” - 然后您需要流委托。不过后者更复杂。

鉴于您的要求,我强烈建议在异步模式下使用 NSURLConnection 来实现委托。

您的问题仍然有足够的内容可以拆分为三个或更多问题,因此我将限制仅回答一个问题:

在将文件上传到服务器时,有一些既定的方法可以通过 HTTP 完成。建议的方法(但不是唯一的方法)是使用带有 multipart/form-data 媒体类型和特殊 DispositionPOST 请求。

让我们看一下与上传文件相关的部分代码:

您提供的代码似乎有问题

[self appendBodyString:[NSString stringWithFormat:@"\r\n--%@--\r\n",[self getBoundaryStr]]];

在方法finishedRequestBody的开头。这看起来像“多部分正文”的“结束分隔符”,它必须出现在最后一部分之后 - 但不能更早。所以,这是一个错误。

现在,让我们弄清楚如何构造一个正确的multipart/form-data 消息:

为文件上传构建多部分消息

我们假设您已经在以NSInputStream 表示的路径pathToBodyFile 处拥有要上传的文件。这在语句中正确完成:

NSInputStream *bodyStream = [[NSInputStream alloc] initWithFileAtPath:pathToBodyFile];

通过 multipart/form-data 消息上传文件的规则在 RFC 1867 "HTML 中基于表单的文件上传" 和一大堆相关和依赖的 RFC 中定义协议非常详细(您现在不需要阅读它,但可能稍后再阅读)。

最近有一个关于 SO 的问题,我试图澄清一个多部分媒体类型:NSURLRequest Upload Multiple Files。我也建议去那里看看。

根据 RFC 1867 的文件上传基本上是一个多部分/表单数据消息,除了它可以使用专门配置,您可以在其中指定原始文件名一个处置参数。相关的 RFC 是 RFC 2388“从表单返回值:multipart/form-data”,还有几十个,可能特别相关的 RFC 2047RFC 6657RFC 2231)。

注意:如果您对任何细节有任何具体问题,我们始终建议您阅读相关的 RFC。 (但找到最新的和实际的是一个挑战。)

multipart/form-data 消息包含一系列部分。表单数据部分由一些“参数名称”或“标签”(通过处置标头表示)、其他可选标头和正文组成。

每个 part 必须有一个 content-disposition 标头(代表“参数名称”或“标签”),其“值”等于“form-data”,并且有一个name 属性,它指定了一个字段名(通常但不完全是指“HTTP 表单”中的一个字段)。例如:

content-disposition: form-data; name="fieldname"

每个部分可能有一个可选的Content-Type 标头。如果没有指定,则假定为text/plain

在标题(如果有)之后是正文

因此,一个部分可以被视为“参数/值”对(加上一些可选的标头)。

如果 body 是文件内容,可以在 content-disposition 中用 filename 参数指定原始文件名,例如:

content-disposition: form-data; name="image"; filename="image.jpg"

此外,您应该相应地为此部分设置Content-Type 标头,以匹配实际文件类型,例如:

Content-Type: image/jpeg

multipart/form-data 消息正文由 一个 或多个部分组成。这些部分用边界分隔。

(如何设置边界,在 SO NSURLRequest Upload Multiple Files 上的给定链接和相关 RFC 中有更详细的描述。)


示例:

上传 MIME 类型为“image/jpeg”的文件“image.jpg”

使用POST 方法创建一个HTTP 消息,并将Content-Type 标头设置为multipart/form-data,指定一个边界

Content-type: multipart/form-data, boundary=AaB03x

一个部分组成的“multipart/form-data”消息的“multipart body”如下所示(注意:CRLF 显式可见):

\r\n--AaB03x\r\n
Content-Disposition: form-data; name="image"; filename="image.jpg"\r\n
Content-Type: image/jpeg\r\n
\r\n<file-content>--AaB03x--

现在,您需要使用NSURLConnectionNSURLRequest 将这个“大纲”“翻译”成Objective-C,乍一看似乎很简单。但是,出现了几个微妙的问题:

第一:

multipart 消息体由 一个 或多个部分组成。如您所见,部件本身包含边界和标题以及正文。现在构造零件主体变得复杂,因为零件主体是一个(您的文件输入流)。现在的任务是“合并”NSData 对象(边界和标题)和文件输入流,从而产生(一些抽象的)新输入源。这个新的输入源与其他部分(如果有的话)现在需要再次形成一个新的输入源,它最终是一个NSInputStream,代表整个多部分multipart/form-data 请求的正文。此最终输入流必须设置为NSMutableURLRequestHTTPBodyStream 属性。

我承认,这是一个挑战,需要许多帮助类和它自己的单元测试!

使用内存映射文件作为 large 资产文件的表示的简化可能是徒劳的,因为您需要形成(也称为合并)一个 完整 多部分主体(一个或更多部分)。这将最终成为一个包含标题和文件内容的NSData 对象,最终分配在堆上。对于非常大的资产 (>300MByte),这可能会失败。

一种解决方案是使用绑定的一对流(输入流和输出流通过固定大小的缓冲区连接),其中一端输出流用于写入所有部分(通过输入流的标题和文件内容),另一端输入流用于“绑定”到HTTPBodyStream 属性。

这个问题的解决方案值得一个新的 SO 问题。 (Apple 提供的示例演示了这种技术)。

现有的解决方案可以轻松设置由第三方库提供的多部分/表单数据请求。然而,即使是知名的第三方库也很难做到这一点。

第二:

警告:

与任何“语言”一样,HTTP 协议对正确的语法非常挑剔,即 - 分隔元素的出现、字符编码、转义和引用等。例如,如果您错过了 CRLF 或错过了为某个字符串(例如文件名)应用正确编码,或者如果您没有在协议的某些元素(例如边界或文件名)中必要时应用引用,您的服务器可能无法理解该消息,或误解。

有大量的 RFC 试图明确指定细节。但是要小心,找到实际指定当前问题的 RFC 需要付出一些努力。并且 RFC 偶尔会更新和过时,以不同的“当前”RFC 结尾。因此,在编写代码时请牢记这一点:在某些极端情况下,您的代码可能没有按照当前的 RFC 编写,并且您会遇到意外行为。

所以,您现在可以接受挑战 - 这确实是高级的东西 - 并尝试正确实现“multipart/form-data body as NSInputStream”,或者您尝试第三方解决方案,该解决方案可能在某些条件下有效,有时也无效。


使用NSURLRequest 上传文件的提示和提示

  • 对于较大的文件,使用NSInputStream 表示文件, 而不是NSData 表示。 (您可以尝试映射 文件和NSData 也是)。

  • NSInputStream 设置为请求正文时,不要打开输入 流。

  • NSInputStream 设置为请求正文时,您必须覆盖 connection:needNewBodyStream:委托方法并提供一个新的 再次流对象。 (你做得对,虽然我没有 了解延迟的目的。)

  • 当提供一个输入流作为请求主体没有设置一个 Content-Length 明确标头,NSURLConnection 将使用 “chunked transfer encoding”。 通常,这对服务器来说不是问题 - 但在它的情况下 是,您可以明确设置Content-Length(如果可以 确定长度)和NSURLConnection 不会使用“分块 传输编码”以传输请求正文。

  • 设置“Content-Length”标头时,请务必设置 正确长度。

  • 当使用NSData 对象作为请求主体时,您不需要设置 Content-Length 标头,NSURLConnection 将设置此 自动,除非明确指定。

  • Disposition 标头中的文件名 可能需要引用和编码(请参阅RFC 2231)。

【讨论】:

  • 非常感谢您的快速回复和建议,非常感谢您。顺便说一句,你提到我有 3 个或更多问题要讨论,你能告诉我这些问题是什么吗?这样我以后就可以用谷歌搜索了。实际上我只能上传> 80MB的文件,超过80MB,应用程序崩溃了!我认为这是您提到的我的另一个问题之一
  • 你好,我只是提出一些问题 1. 你说使用iOS 7 "NSURLSession, NSURLSessionTask" 更容易,你的意思是如果我使用那个(NSURLSession, NSURLSessionTask),就不需要处理流问题还是什么? 2. 如果不幸的是我仍然无法解决这些问题,你能建议我你推荐使用哪个第三方吗?像 AFNetworking 还是其他?
  • 你又提到了“一对绑定的流”。我对此还不是很清楚。这个输出流和输入流是否同时工作?我的意思是当写输出流(未完成)时也读输入流并同时设置httpbodystream?因为我实现的是等待完成整个流作业,然后 setHTTPBodyStream 进行上传。
  • 更新:在考虑了pair stream(对于asset)之后,我实现了copyFileFromCameraroll方法(不使用绑定的pair stream),以便将asset(mediaInputStream)文件复制到临时目录
  • 我认为,您首先需要确定解决问题的策略:1)仅使用 iOS 7 方法(如前所述)。如果这对您有用,这是最简单的解决方案。 2) 使用第三方库,牢记可能无论如何(或没有)问题。这基本上也很容易。 3) 实施您自己的解决方案。考虑到您的要求:大文件 (>80 MByte)、可取消的请求、可重新启动的请求、“可回退”的请求正文,当您希望它正确时,这是一个非常困难且高级的挑战。跨度>
【解决方案2】:

目前,我发现此错误日志的内容长度错误的原因。
原来,我设置 content-length 只是因为上传文件的大小(不包括 post 数据)。
这是 setPostHeaders 方法中的错误代码:

NSString *tempFile = [NSTemporaryDirectory() stringByAppendingPathComponent:@"uploadFile"]; 
NSError *fileReadError = nil;
NSDictionary *fileAttrs = [[NSFileManager defaultManager] attributesOfItemAtPath:tempFile error:&fileReadError];
NSAssert1((fileAttrs != nil),@"Couldn't read post body file",fileReadError);
NSNumber *contentLength = [fileAttrs objectForKey:NSFileSize];
[request setValue:[contentLength stringValue] forHTTPHeaderField:@"Content-Length"]; 

我使用 pathToBodyFile 设置 Content-Length 的大小(该文件包含 post 数据)

NSDictionary *fileAttrs = [[NSFileManager defaultManager] attributesOfItemAtPath:pathToBodyFile error:&fileReadError];
NSAssert1((fileAttrs != nil),@"Couldn't read post body file",fileReadError);
NSNumber *contentLength = [fileAttrs objectForKey:NSFileSize];
//NSLog(@"2 body length %@",[contentLength stringValue]);
[request setValue:[contentLength stringValue] forHTTPHeaderField:@"Content-Length"]

最后,错误日志消失了。我不知道为什么会这样。原以为内容长度设置为上传文件,但实际上内容长度设置为文件大小,包括发布数据和上传文件

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-09-08
    • 1970-01-01
    • 2023-03-29
    • 1970-01-01
    • 2016-05-04
    • 1970-01-01
    • 2017-07-10
    • 2018-01-26
    相关资源
    最近更新 更多