无法在 iOS 中使用 NSURLSession 多部分表单数据上传文件

Posted

技术标签:

【中文标题】无法在 iOS 中使用 NSURLSession 多部分表单数据上传文件【英文标题】:unable to upload file using NSURLSession multi-part form data in iOS 【发布时间】:2016-04-28 11:34:43 【问题描述】:

我正在尝试使用- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL; 方法使用多部分表单数据上传视频/图像文件。但不知何故,我无法上传文件,我收到“stream ended unexpectedly”错误。

要求

    将视频/图像文件上传到服务器 应用应支持后台上传(应用进入后台后继续上传) 服务器期望使用多部分表单数据发送数据。

用于实现此目的的方法/API

    NSURLSession 后台会话 API(完整代码如下)

    2.- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL

面临的挑战/问题

    每次我使用此 API 进行上传过程时都会遇到“stream ended unexpectedly”错​​误

注意事项

    如果我使用 NSURLConnection 而不是 NSURLSession,则使用相同的代码上传会成功。

    NSURLSession 后台上传进程需要文件位置 (NSURL) 作为参数,不接受 NSData。它不允许我们在上传之前将文件转换为NSData,即我们不能将NSData添加到文件正文中。

以下几点需要帮助

    正在形成的多部分表单数据主体中是否有任何错误(注意 - 相同的代码正在使用 NSURLConnection)

    我的方法哪里出了问题?

    我们是否需要在服务器级别进行任何更改以支持 NSURLSession backgroundSession 上传? (在数据解析或其他方面?)

    这是用于上传文件的代码

NSString *BoundaryConstant = @"----------V2ymHFg03ehbqgZCaKO6jy";

    // string constant for the post parameter 'file'. My server uses this name: `file`. Your's may differ
    NSString* FileParamConstant = @"file";

    // the server url to which the image (or video) is uploaded. Use your server url here

    url=[NSURL URLWithString:[NSString stringWithFormat:@"%@%@%d",baseURL,@"posts/post/update/",createPostObject.PostID]];    


    // create request
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
    [request setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData];
    [request setHTTPShouldHandleCookies:NO];
    [request setTimeoutInterval:120];
    [request setHTTPMethod:@"POST"];
    [request addValue:@"multipart/form-data" forHTTPHeaderField:@"Content-Type"];

    [request setURL:url];

    // set Content-Type in HTTP header
    NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", BoundaryConstant];
    [request setValue:contentType forHTTPHeaderField: @"Content-Type"];

    if([[NSUserDefaults standardUserDefaults] objectForKey:@"accessToken"])

        [request setValue:[[NSUserDefaults standardUserDefaults] objectForKey:@"accessToken"] forHTTPHeaderField:AccessTokenKey];

    

    // post body
    NSMutableData *body = [NSMutableData data];

    // add params (all params are strings)
    for (NSString *param in self.postParams) 

        NSLog(@"param is %@",param);

        [body appendData:[[NSString stringWithFormat:@"--%@\r\n", BoundaryConstant] dataUsingEncoding:NSUTF8StringEncoding]];
        [body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n", param]             dataUsingEncoding:NSUTF8StringEncoding]];
        [body appendData:[[NSString stringWithFormat:@"%@\r\n", [self.postParams objectForKey:param]] dataUsingEncoding:NSUTF8StringEncoding]];
    

    // add video file name to body

        [body appendData:[[NSString stringWithFormat:@"--%@\r\n", BoundaryConstant] dataUsingEncoding:NSUTF8StringEncoding]];
        [body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"file.mp4\"\r\n", FileParamConstant] dataUsingEncoding:NSUTF8StringEncoding]];
        [body appendData:[[NSString stringWithString:@"Content-Type: video/mp4\r\n\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
      //  [body appendData:self.dataToPost];
        [body appendData:[[NSString stringWithFormat:@"\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];

        [body appendData:[[NSString stringWithFormat:@"--%@--\r\n", BoundaryConstant] dataUsingEncoding:NSUTF8StringEncoding]];



    // setting the body of the post to the request
    [request setHTTPBody:body];

    // set the content-length
    NSString *postLength = [NSString stringWithFormat:@"%lu", (unsigned long)[body length]];
    [request setValue:postLength forHTTPHeaderField:@"Content-Length"];

    NSLog(@"Request body %@", [[NSString alloc] initWithData:[request HTTPBody] encoding:NSUTF8StringEncoding]);

    NSURLSessionConfiguration * backgroundConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"backgroundtask1"];

    NSURLSession *backgroundSeesion = [NSURLSession sessionWithConfiguration: backgroundConfig delegate:self delegateQueue: [NSOperationQueue mainQueue]];


    NSURLSessionUploadTask *uploadTask = [backgroundSeesion uploadTaskWithRequest:request fromFile:self.videoUrl];
    [uploadTask resume];

【问题讨论】:

您似乎设置了两次“Content-Type”。不知道结果如何。不确定其余的。我做了类似的事情,但单独创建上传缓冲区(在 c++ 函数中)并添加孔缓冲区。所以,我不需要做更多的贡献。 【参考方案1】:

您没有上传您认为的内容。您的意图是按原样上传正文数据。相反,当您调用 uploadTaskWithRequest:fromFile: 时,该方法会有效地消除请求中的任何 HTTPBodyHTTPBodyStream 值,并将它们替换为您通过 fromFile: 参数传入的 URL 的内容。

因此,除非您将表单编码的正文数据块写入其他地方的文件 URL,否则您将自己上传文件而不是多部分表单数据。

您需要调整代码以将表单数据写入文件而不是将其存储在 HTTPBody 中,然后将该文件的 URL 传递给 fromFile: 参数。

【讨论】:

1.将 fromFile: 更改为 fromData:,并传递 body 的值。 2.取消注释 // [body appendData:self.dataToPost];并将 self.dataToPost 更改为 [NSData dataWithContentsOfURL:self.videoURL]。 现在你有了完整的代码sn-p。您还可以选择删除 [request setHTTPBody:body] 行,因为它没有做任何有价值的事情。【参考方案2】:

为了防止浪费时间处理它。

基于@dgatwood 答案的完整sn-p

private func http(request: URLRequest)
        let configuration = URLSessionConfiguration.default
        let session = URLSession(configuration: configuration, delegate: self, delegateQueue: .main)
        /*Tweaking*/
        let task = session.uploadTask(with: request, from: request.httpBody!)
        task.resume()
    

并且..不要忘记在请求对象上添加标头,例如

request.setValue("multipart/form-data; boundary=\(yourboundary)", forHTTPHeaderField: "Content-Type")

【讨论】:

以上是关于无法在 iOS 中使用 NSURLSession 多部分表单数据上传文件的主要内容,如果未能解决你的问题,请参考以下文章

iOS NSURLSession 实现网络请求-文件下载-上传-后台下载

iOS 使用NSURLSession下载大文件

iOS9 - NSURLSession 无法解析响应但 AlamoFire 工作

iOS网络开发使用NSURLSession

在IOS中使用web服务时发生NSURLSession内存泄漏

在 iOS 中使用 NSUrlSession 一次下载 1 个文件