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

这就是我写蒸汽的方式:

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

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

4

2 回答 2

3

编辑:

为了提前提到这一点:

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

- (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 完成。建议的方法(但不是唯一的方法)是使用具有特殊DispositionPOST的媒体类型的请求。multipart/form-data

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

您提供的代码似乎在声明中有问题

[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 Upload Multiple Files。我也建议去那里看看。

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

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

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

每个部分必须有一个content-disposition标头(表示“参数名称”或“标签”),其“值”等于“表单数据”,并且具有指定字段名称的名称属性(通常但不专门指“HTTP 表单”中的字段)。例如:

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

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

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

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

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

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”

使用方法创建 HTTP 消息POST并将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--

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

第一的:

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

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

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

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

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

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

第二:

一个警告:

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

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

所以,你现在可能会接受挑战——这真的是高级的东西——并尝试正确地实现“ multipart/form-data body as NSInputStream ”,或者你尝试第三方解决方案,它可能在某些条件下工作,或者有时不是。


文件上传的提示和提示NSURLRequest

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

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

  • 将 a 设置NSInputStream为请求主体时,您必须覆盖 connection:needNewBodyStream:委托方法并再次提供新的流对象。(你做得对,虽然我不明白延迟的目的。)

  • 当提供输入流作为请求主体而不Content-Length明确设置 标头时,NSURLConnection将使用“分块传输编码”。通常,这对服务器来说不是问题 - 但在这种情况下,您可以Content-Length显式设置(如果您可以确定长度)并且NSURLConnection不再使用“分块传输编码”来传输请求正文。

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

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

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

于 2013-09-30T11:21:48.523 回答
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"]

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

于 2013-10-03T01:39:55.833 回答