我正在尝试通过流式传输上传大文件,最近我收到了这个错误日志:
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
}
}
任何人,任何想法?我错过了什么吗?
答案 0 :(得分: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];)
假设您想通过创建临时文件来上传资产,您可以更轻松地完成此操作(从资产创建临时文件)。实际上,您不需要流委托方法。避免临时文件的另一种方法需要一对绑定的流&#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 2047,RFC 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;使用NSURLConnection
和NSURLRequest
进入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"]
最后,错误日志消失了。我不知道为什么这项工作。我原以为内容长度设置为上传文件,但实际上,内容长度设置为包含帖子数据和上传文件的文件大小