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

我想通过stream媒体上传大文件,最近我得到这个错误日志:

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

closuresstream

 -(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 7中,可能有一个简单的解决scheme来上传大文件 。 请参阅NSURLSessionNSURLSessionTask ,特别是:

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

除此以外,

你的代码有一些问题:

  • 多部分消息没有正确构build(包括内容长度)。

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

     NSData *responseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];) 
  • 假设你想通过创build一个临时文件来上传一个资产,你可以更简单地完成这个任务(从一个资产创build一个临时文件)。 实际上,你不需要stream委托方法。 避免临时文件的另一种方法需要一个“绑定的stream对” – 然后你需要stream委托。 后者更复杂,虽然。

鉴于你的要求, 我强烈build议使用NSURLConnection以asynchronous模式实现委托。

你的问题仍然有足够的东西分成三个或更多的问题,因此我将限制只回答一个问题:

文件上传到服务器时,有几种已经build立的方法可以用HTTP来完成。 build议的方法(但不是唯一的方法)是使用带有特殊处理multipart/form-data媒体types的POST请求。

让我们看看你的代码在与上传文件有关的部分:

你提供的代码似乎有一个问题的声明

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

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

现在,我们来看看如何构build一个正确的multipart/form-data消息:

构buildfile upload的多部分消息

我们假设你已经有了一个你想上传的文件,pathpathToBodyFile表示为一个NSInputStream 。 这在声明中是正确的:

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

通过多部分/表单数据消息上传文件的规则在RFC 1867 “ 基于HTML的基于表单的file upload ”以及大量相关和相关的RFC中进行了定义,这些RFC 详细说明了协议(您不需要现在阅读,但可能稍后)。

最近有一个关于SO的问题,我试图澄清一个多部分媒体types: NSURLRequest上传多个文件 。 我也build议在那里看看。

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

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

multipart/form-data消息包含一系列部分 。 表单数据部分由一些“参数名称”或“标签”(通过configuration标题表示),其他可选标题和主体组成。

每个部分必须有一个content-disposition头部(表示“参数名称”或“标签”),其“值”等于“form-data”,并具有一个名称属性 ,用于指定一个字段名称 (通常但不是专指字段以“HTTPforms”)。 例如:

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

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

正文后面(如果有的话) 正文后面。

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

如果主体是文件内容,则可以使用文件参数在内容configuration中指定原始文件名,例如:

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

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

 Content-Type: image/jpeg 

multipart/form-data消息体由一个或多个部分组成。 部件之间用边界分开。

(您如何设置边界,在SO NSURLRequest上传多个文件的相关RFC中给出的链接中有更详细的描述。)


例:

上传MIMEtypes为“image / jpeg”的文件“image.jpg”

使用方法POST创build一个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中,这似乎是乍一看直截了当。 然而,出现了一些微妙的问题:

第一:

多部分消息主体由一个或多个部分组成。 正如你所看到的,一个零件本身包含边界和头部加上正文。 由于零件的主体是一个stream (您的文件inputstream),现在构build零件主体变得复杂了。 现在的任务是“合并”一个NSData对象(边界和头文件)和文件inputstream,产生(一些抽象的) 新的input源 。 这个新的input源和其他部分(如果有的话)现在需要再次形成一个新的input源 ,这个新的input源最终是表示 multipart/form-data请求的整个多部分主体NSInputStream 。 这个最终的inputstream必须被设置为NSMutableURLRequestHTTPBodyStream属性。

我承认,这是一个挑战,需要一些辅助类和它自己的unit testing!

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

一个解决scheme是使用一对绑定的stream (一个Input Stream和一个OutputStream通过一个固定大小的缓冲区连接),其中一端Output Stream用于写入所有的部分(通过inputstream的头和文件内容),而inputstream的另一端用于“绑定”到HTTPBodyStream属性。

这个单一问题的解决scheme值得一个新的SO问题。 (有苹果公司的样品展示了这种技术)。

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

第二:

一个警告:

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

有大量的RFC试图明确地指定基本的细节。 但要小心,find实际指定当前问题的RFC将需要一些努力。 并且RFC在一个不同的“当前”RFC中得到更新和废弃。 因此,在编写代码时请记住这一点:可能存在边缘情况,根据当前的RFC,您的代码不会被写入,并且会出现意想不到的行为。

所以,你现在可以接受这个挑战 – 这真的是高级的东西 – 并尝试正确地实现一个“ 多部分/表单数据体”作为NSInputStream ,或者你尝试第三方解决scheme,这可能在某些条件下工作,或者有时不。


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

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

  • NSInputStream设置为请求主体时,请勿打开inputstream。

  • NSInputStream设置为请求主体时,您必须重写connection:needNewBodyStream: delegate方法并再次提供一个新的stream对象。 (你做得对,虽然我不明白延误的目的。)

  • 当提供一个inputstream作为请求体而不明确设置Content-Length头时, NSURLConnection将使用“ chunked transfer encoding ”。 通常情况下,这对服务器来说不是问题 – 但是在这种情况下,你可以明确地设置Content-Length (如果你能确定长度), NSURLConnection不会使用“chunked transfer encoding”来传输请求体了。

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

  • 当使用NSData对象作为请求体时,你不需要设置一个Content-Length头, NSURLConnection会自动设置它,除非明确指定。

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

目前,我发现是什么原因导致这个错误日志是错误的内容长度。
本来,我设置的内容长度只是由于上传文件的大小(不包括发布数据)。
这是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"] 

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