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

时间:2013-09-27 04:03:31

标签: ios upload streaming

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

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

我在实现此方法时发现错误:NewBodyStream:请参阅以下代码:

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

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

2 个答案:

答案 0 :(得分:3)

编辑:

为了提前提到这一点:

在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];)    
    
  • 假设您想通过创建临时文件来上传资产,您可以更轻松地完成此操作(从资产创建临时文件)。实际上,您不需要流委托方法。避免临时文件的另一种方法需要一对绑定的流&#34; - 然后你需要流委托。但后者更为复杂。

根据您的要求, 我强烈建议在实施代理的异步模式下使用NSURLConnection

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

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

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

您提供的代码似乎在声明

中存在问题
[self appendBodyString:[NSString stringWithFormat:@"\r\n--%@--\r\n",[self getBoundaryStr]]];

在方法finishedRequestBody的开头。这看起来像是关闭分隔符&#34;一个&#34;多部分身体&#34;必须出现在最后一部分之后 - 但不是更早。所以,这是一个错误。

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

构建用于文件上传的多部分消息

我们假设已经将您要在路径 pathToBodyFile 上传的文件表示为NSInputStream。这在声明中正确完成:

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

通过多部分/表单数据邮件上传文件的规则在RFC 1867&#34; 基于表单的HTML文件上传&#34;中定义。以及一大堆相关且依赖的RFC,它们非常详细地指定协议 (您现在不需要阅读它,但可能稍后阅读)。

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

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

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

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

每个部分必须有内容处置标题(表示&#34;参数名称&#34;或&#34;标签&#34;)其& #34;值&#34;等于&#34;形式数据&#34;并且具有名称属性,用于指定字段名称(通常但不完全是指&#34; HTTP表单中的字段&#34;)。例如:

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

每个部分可能都有一个可选的Content-Type标题。如果未指定任何人,则假定为text/plain

在标题(如果有)之后, body 跟随。

因此,一部分可被视为&#34;参数/值&#34;对(加上一些可选的标题)。

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

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

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

Content-Type: image/jpeg

multipart/form-data邮件正文包含一个或更多部分。部件用边界分隔。

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


实施例

上传文件&#34; image.jpg&#34;其MIME类型为&#34; image / jpeg&#34;

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

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

&#34;多部分身体&#34;一个&#34; multipart / form-data&#34;由一个部分组成的消息如下所示(注意: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--

现在,您需要&#34;翻译&#34;这个&#34;大纲&#34;使用NSURLConnectionNSURLRequest进入Objective-C,乍一看似乎是直截了当的。然而,出现了一些微妙的问题:

首先:

multipart 邮件正文包含一个或更多部分。如您所见,零件本身包含边界和标题以及正文。构建零件体现在变得复杂,因为零件的主体是(您的文件输入流)。现在的任务是&#34;合并&#34; NSData对象(边界和标题)和文件输入流导致(某些抽象)新输入源。这个新的输入源以及其他部分(如果有的话)现在需要再次形成一个新的输入源,最后是一个代表整个NSInputStream > multipart/form-data请求的多部分正文。必须将此最终输入流设置为HTTPBodyStream的{​​{1}}属性。

我承认,这是一个挑战,需要一些辅助类,以及它自己的单元测试!

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

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

这个单一问题的解决方案值得一个新的问题。 (有来自Apple的样品证明了这种技术)。

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

第二:

警告:

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

有大量的RFC试图明确指定细节。但仔细一点,找到实际指定当前问题的RFC将需要一些努力。并且RFC会在一段时间内更新并废弃,并以不同的当前状态结束。 RFC。因此,在编写代码时请记住这一点:可能存在根据当前RFC编写代码并且您得到意外行为的边缘情况。

所以,您现在可能会接受挑战 - 这是非常高级的东西 - 并尝试实现&#34; multipart / form-data正文作为NSInputStream &#34;正确地,或者您尝试第三方解决方案,可能在某些条件下工作,或有时不工作。


使用HTTPBodyStream

进行文件上传的提示和提示
  • 对于较大的文件,请使用NSURLRequest表示文件, 而不是NSInputStream表示。 (您可以尝试映射 文件和NSData虽然也是如此。

  • NSData设置为请求正文时,请勿打开输入 流。

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

  • 提供输入流作为请求正文而不设置a 明确connection:needNewBodyStream:标头,Content-Length将使用 &#34; NSURLRequest Upload Multiple Files&#34 ;. 通常,这不是服务器的问题 - 但是在它的情况下 是的,您可以明确设置NSURLConnection(如果可以的话) 确定长度)Content-Length不会使用&#34; chunked 转移编码&#34;再发送请求正文。

  • 设置&#34; Content-Length&#34;标题,请务必设置 正确长度。

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

  • 处置标题中的文件名可能需要引用和编码(请参阅chunked transfer encoding)。

答案 1 :(得分:0)

目前,我发现了导致此错误日志内容长度错误的原因 最初,我设置的内容长度只是由于上传文件的大小(不包括帖子数据) 这是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的大小(此文件包含帖子数据)

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

最后,错误日志消失了。我不知道为什么这项工作。我原以为内容长度设置为上传文件,但实际上,内容长度设置为包含帖子数据和上传文件的文件大小