通过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来上传大文件 。 请参阅NSURLSession
, NSURLSessionTask
,特别是:
- (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--
现在,您需要使用NSURLConnection
和NSURLRequest
将这个“提纲”“翻译”到Objective-C中,这似乎是乍一看直截了当。 然而,出现了一些微妙的问题:
第一:
多部分消息主体由一个或多个部分组成。 正如你所看到的,一个零件本身包含边界和头部加上正文。 由于零件的主体是一个stream (您的文件inputstream),现在构build零件主体变得复杂了。 现在的任务是“合并”一个NSData
对象(边界和头文件)和文件inputstream,产生(一些抽象的) 新的input源 。 这个新的input源和其他部分(如果有的话)现在需要再次形成一个新的input源 ,这个新的input源最终是表示多 multipart/form-data
请求的整个多部分主体的NSInputStream
。 这个最终的inputstream必须被设置为NSMutableURLRequest
的HTTPBodyStream
属性。
我承认,这是一个挑战,需要一些辅助类和它自己的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"]
最后,错误日志消失了。 我不知道为什么这个工作。 我曾经以为内容长度被设置为上传文件,但实际上,内容长度被设置为包含发布数据和上传文件的文件的大小
- alamofire multipartformdata使用urlrequest
- 上传IPA,奇怪的问题:错误ITMS-90032:“无效的图像path – 在键'$ key'下引用的path没有find图像:'$ path'
- 错误ITMS-9000:“缺less代码签名权利。 在捆绑中找不到权利“ – 如何更改应用程序ID名称
- AFNetworking表单请求(在一个请求中上传多个文件)
- 使用ASIHTTPRequest从iOS上传图像
- 无法使用Xcode 7.1上传到App Store
- iOS:当应用程序在后台时执行上传任务
- 将UIImage中的图像与其他POST数据一起以JPEG格式上传
- 上传图像从iOS应用程序到PHP – 不能完全正确 – 我错过了什么?