多部分表单上传和 NSURLSession.uploadTaskWithRequest 之间的区别

Difference between multipart form upload and NSURLSession.uploadTaskWithRequest

来自网络编程领域,我非常擅长使用多部分表单请求来上传文件。但是,在iOS中,我们有一个叫做NSURLSession的东西,方法是uploadTaskWithRequest,这似乎是调用图片上传之类的方法。

您能解释一下这两种方法的区别吗,多部分表单上传 vs uploadTaskWithRequest?如果我已经有一个处理多部分表单上传的后端,我可能需要进行什么样的调整才能使其也支持 uploadTaskWithRequest

文件上传 multipart/form-data

第一种使用 multipart/form-data Content-type 的方法最初是在 RFC 1867, then moved to the World Wide Web Consortium, which included it in the specification for HTML 4.0 中定义的,其中表单以 HTML 表示,表单值通过 HTTP 和电子方式发送邮件。当用户填写表单后,表单将发送到服务器。此技术得到浏览器和 Web 服务器的广泛支持和使用。

但是 multipart/form-data 也可用于定义以 HTML 以外的其他表示形式呈现的表单数据。也就是说,您不一定需要 Web 浏览器或 Web 服务器。可被多种应用程序使用并由多种协议传输的当前规范是 RFC 7578(来自 IETF)。

但必须提到的是,multipart/form-data 内容类型并非 always/is 并非没有问题。它本身就相当复杂。此外,它 uses/refers 到许多其他 RFC,并且 - 作为清理的结果 - 它和它所依赖的那些已经被频繁地更改、废弃和更新。由于其复杂性,序列化器和解析器也变得相当复杂,并且存在很多错误和其他问题的空间。

NSURLSession uploadTaskWithRequest

如何 NSURLSession 撰写请求未被准确记录。不过,它肯定不使用 multipart/form-data 内容类型。

对于上传任务,NSURLSession 使用带有 NSURLRequest 的 POST 请求作为您可以自行设置的参数。也就是说,您可以选择设置内容类型(例如 text/plain; charset=utf-8),以及其他 header。NSURLSession 也可以从给定的内容(文件、流或NSData)。也就是说,我们可以说,它变成了一个“简单”的POST请求。由于复杂度较低,请求也不那么麻烦。

因此,为了让您的服务器支持应上传文件的 uploadTaskWithRequest,它应该只支持带有一些“简单”内容类型的 POST 请求。也就是说,与 multipart/form-data 内容类型的“文件上传”相反,其中包含配置 header 中的文件名,服务器需要 return URL写入资源(文件)的位置。

uploadTaskWithRequest 只是发送 NSData、文件或流作为请求的主体。除此之外它什么都不做。它的好处就是可以与后台会话一起使用。

因此,如果您的 Web 服务需要 multipart/form-data 请求,则您必须自己构建该请求(除非您使用 AFNetworking 或 Alamofire 之类的工具来为您执行此操作)。构建该请求后,您可以使用 dataTaskWithRequest(已设置 NSMutableURLRequestHTTPBody)或 uploadTaskWithRequest(在这种情况下您不设置 HTTPBody,而是将其作为参数提供给 uploadTaskWithRequest).

顺便说一下,像 Charles 这样的工具在这些情况下非常有用,可以让您观察幕后发生的事情。

幸运的是,在后台执行 multipart/form-data POST 请求非常容易,例如,如果您想要上传图像和其他一些信息。

首先,以与创建同步请求相同的方式创建 NSMutableURLRequest(参见示例 POST multipart/form-data with Objective-C)。

然后,将请求的主体写入文件并将其提供给您使用 backgroundSessionConfiguration 创建的 NSURLSession 的 uploadTaskWithRequest 方法:

NSString *filePath = [[NSSearchPathForDirectoriesInDomains(
                           NSCachesDirectory, NSUserDomainMask, YES) lastObject] 
                           stringByAppendingPathComponent:imageUUID];

[request.HTTPBody writeToFile:filePath atomically:YES];

NSURLSessionUploadTask *task = [urlSession uploadTaskWithRequest:request 
                                           fromFile:[NSURL fileURLWithPath:filePath]];
[task resume];

如果您有多个任务并且希望能够在委托回调中区分它们,您可以使用 NSURLProtocol class 设置一个参数(在您创建请求之后):

[NSURLProtocol setProperty:imageUUID
                    forKey:@"yourKeyForTheImageUUID" 
                 inRequest:request];

然后像这样在回调中取回它:

- (void)URLSession:(NSURLSession *)session
        task:(nonnull NSURLSessionTask *)task
        didCompleteWithError:(nullable NSError *)error
{
    NSString *imageUUID = [NSURLProtocol propertyForKey:@"yourKeyForTheImageUUID"
                                              inRequest:task.originalRequest];