通过流上传文件:显示错误日志“操作无法完成。(kCFErrorDomainCFNetwork错误303。)”

3

我正在尝试通过流式传输上传大文件,最近出现了以下错误日志:

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];
    }  
}

这是我写 steam 的方法:

    -(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;
}

这是我设置头文件和媒体输入流的地方。

-(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
    }            
}

大家有什么想法吗?是我漏了什么吗?


可能是什么是kCFErrorDomainCFNetwork Code=303的重复问题。 - Matt Sephton
2个回答

3

编辑:

为了事先提到这一点:

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

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

否则,您的代码存在一些问题:
  • The multipart message is not constructed properly (including the content length).

  • You use sendSynchronousRequest and mixing them with delegate methods. Delete the line:

    NSData *responseData = [NSURLConnection sendSynchronousRequest:request  returningResponse:&response error:&error];)    
    
  • Assuming you want to upload an asset by creating a temporary file, you can accomplish this (creating a temp file from an asset) much more easily. In fact, you don't need the stream delegate method. An alternative approach avoiding the temp file requires a "bound pair of streams" - and then you need the stream delegate. The latter is more complex, though.

根据您的要求,我强烈建议使用实现代理的异步模式的NSURLConnection。您的问题仍然有足够的内容可以分成三个或更多问题,因此我将限制回答一个问题:当上传文件到服务器时,有一些已经确定的方法可以使用HTTP完成这项工作。建议的方法(但不是唯一的方法)是使用带有特殊Disposition的multipart/form-data媒体类型的POST请求。让我们看看与上传文件相关的代码部分:您提供的代码在语句中似乎有问题。
[self appendBodyString:[NSString stringWithFormat:@"\r\n--%@--\r\n",[self getBoundaryStr]]];

在方法finishedRequestBody的开头。这看起来像是“多部分正文”的“结束分隔符”,必须出现在最后一部分之后,但不是更早。所以,这里有一个错误。
现在,让我们想办法构造正确的multipart/form-data消息:
构造用于文件上传的多部分消息
我们假设您已经拥有要上传的文件,路径为pathToBodyFile,表示为NSInputStream。这在语句中正确完成:
NSInputStream *bodyStream = [[NSInputStream alloc] initWithFileAtPath:pathToBodyFile];

通过multipart/form-data消息上传文件的规则在RFC 1867HTML中基于表单的文件上传”中定义,以及一系列相关和依赖的RFC,这些RFC详细指定了协议(您现在不需要阅读,但可能以后需要)。
最近在SO上有一个问题,我尝试澄清多部分媒体类型:NSURLRequest上传多个文件。我建议也去看看那里。
根据RFC 1867,文件上传基本上是一个multipart/form-data消息,除了它可以使用一个特殊的Disposition,在其中可以指定原始文件名作为一个disposition参数。相关的RFC包括RFC 2388“从表单返回值:multipart/form-data”,以及其他几十个,可能特别相关的有RFC 2047RFC 6657RFC 2231)。
注意:如果您对任何细节有具体问题,建议阅读相关的RFC。(找到最新和实际的RFC是一项挑战,但仍然值得一读。)
multipart/form-data消息包含一系列部分。form-data部分由一些“参数名称”或“标签”(通过disposition头表示),其他可选头和正文组成。
每个部分必须有一个content-disposition头(表示“参数名称”或“标签”),其“值”等于“form-data”,并且具有指定字段名称的name属性(通常但不仅限于指HTTP表单中的字段)。例如:
content-disposition: form-data; name="fieldname"

每个部分都可以有一个可选的Content-Type头。如果没有指定,则默认为text/plain
在头部(如果有)之后,跟随着正文
因此,可以将一个部分视为“参数/值”对(加上一些可选的头部)。
如果正文是文件内容,则可以在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,指定一个boundary

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,这乍一看似乎很简单。然而,会出现一些微妙的问题:
第一点:
一个多部分消息体由一个或多个部分组成。如您所见,一个部分本身包含边界和标头以及正文。构建部分正文现在变得复杂,因为部分正文是一个(您的文件输入流)。任务现在是将NSData对象(边界和标头)和文件输入流合并,从而产生(某种抽象的)新输入源。如果有其他部分,则这个新的输入源连同其他部分一起再次形成一个新的输入源,这最终是一个代表整个multipart/form-data请求的多部分正文NSInputStream。这个最终的输入流必须设置为NSMutableURLRequestHTTPBodyStream属性。

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

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

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

针对此单一问题的解决方案值得新的SO问题。(苹果有演示此技术的示例)。

已经存在的解决方案可以轻松设置由第三方库提供的multipart/form-data请求。然而,即使是众所周知的第三方库也难以正确处理此问题。

第二点:

警告:

任何“语言”一样,HTTP协议对于正确的语法非常挑剔,即分隔符元素的出现、字符编码、转义和引用等。例如,如果您错过了CRLF或者没有为某个字符串(例如文件名)应用正确的编码,或者在协议的某个元素中没有适当地应用引号(例如边界或文件名),则您的服务器可能无法理解消息或误解它。
有大量的RFC试图明确指定这些微小的细节。但是要注意,找到实际指定当前问题的RFC将需要一些努力。而且RFC偶尔会更新和废弃,最终导致不同的“当前”RFC。因此,在编写代码时请记住:可能存在边缘情况,您的代码未按照当前RFC编写,从而导致意外的行为。
因此,您现在可以尝试挑战 - 这确实是高级内容 - 并尝试正确地实现“multipart/form-data body as NSInputStream”,或者尝试第三方解决方案,在某些条件下可能有效,有时则不行。

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

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

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

  • 在将NSInputStream设置为请求正文时,您必须重写connection:needNewBodyStream:委托方法,并再次提供新的流对象。(您已经正确地执行了此操作,但我不理解延迟的目的。)

  • 当提供一个输入流作为请求正文时,没有明确设置Content-Length标头时,NSURLConnection将使用“分块传输编码”。 通常,这对服务器来说不是问题,但在某些情况下,您可能需要明确设置Content-Length(如果您能确定长度),并且NSURLConnection将不再使用“chunked传输编码”来传输请求正文。

  • 在设置“Content-Length”标头时,请确保您设置了正确的长度。

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

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


非常感谢您的快速回复和建议,非常感谢。顺便说一下,您提到我有3个或更多问题需要讨论,您能告诉我这些问题是什么吗?这样我就可以稍后在Google上搜索了。实际上,我只能上传大于80MB的文件,超过80MB,应用程序就会崩溃!我想这是您提到的另一个问题之一。 - prettydog
你好,我有一些问题想问:
  1. 你说使用iOS 7的“NSURLSession, NSURLSessionTask”更容易,是不是意味着如果我使用它们(NSURLSession, NSURLSessionTask),就不需要处理流的问题了?
  2. 如果我还是无法解决这些问题,你能否建议我使用哪个第三方库呢?例如AFNetworking或其他的?
- prettydog
更新:在考虑了对于资产的配对流(pair stream)之后,我实现了copyFileFromCameraroll方法(不使用绑定的配对流),这样媒体输入流(asset)文件就被复制到临时目录中。 - prettydog
我认为,你需要先决定解决问题的策略:1)使用仅限iOS 7的方法(如上所述)。如果这个方法适用于你,那么这是最简单的解决方案。2)使用第三方库,但要记住可能会出现问题(或不会)。这基本上也很容易。3)实施自己的解决方案。如果你希望它考虑到你的需求是“正确”的,那么这真的是一个相当困难和高级的挑战:大文件(>80 M字节),可取消请求,可重启请求,“回放”请求体。 - CouchDeveloper
“有界流对”是一种输入流和输出流的配对,通过一个固定大小的缓冲区连接。您可以使用CF函数CFStreamCreateBoundPair创建这些流。例如,当您有许多“输入数据”的片段组成完整的请求正文时,就可以使用它。您将每个片段写入输出流,在另一端,输入流通过请求的HTTPBodyStream属性由NSURLConnection读取。(输入流被分配为请求正文的属性HTTPBodyStream。) - CouchDeveloper
显示剩余2条评论

0

目前,我发现导致这个错误日志的原因是错误的内容长度。
最初,我设置的内容长度仅取决于上传文件的大小(不包括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(该文件包含POST数据)设置Content-Length的大小。

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"]

最后,错误日志消失了。我不知道为什么会这样。我曾经认为内容长度设置为上传文件,但实际上,内容长度设置为包括帖子数据和上传文件在内的文件大小。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接