译自:https://www.donnywals.com/uploading-images-and-forms-to-a-server-using-urlsession/
了解多部分请求的实际外观
如果您曾经使用Charles之类的工具检查了多部分请求,则可能发现发帖请求的标头中包含以下密钥:
Content-Type: multipart/form-data; boundary=3A42CBDB-01A2-4DDE-A9EE-425A344ABA13
您看到的价值boundary
很可能会有所不同,但应该看起来很熟悉。您的帖子请求的正文通常看起来像以下内容:
--Boundary-3A42CBDB-01A2-4DDE-A9EE-425A344ABA13
Content-Disposition: form-data; name="family_name"
Wals
--Boundary-3A42CBDB-01A2-4DDE-A9EE-425A344ABA13
Content-Disposition: form-data; name="name"
Donny
--Boundary-3A42CBDB-01A2-4DDE-A9EE-425A344ABA13
Content-Disposition: form-data; name="file"; filename="somefilename.jpg"
Content-Type: image/png
-a long string of image data-
--Boundary-3A42CBDB-01A2-4DDE-A9EE-425A344ABA13—
当您检查请求并发现与上述内容相似的内容时,您可能认为这看起来很复杂,最好使用为您处理标头和HTTP主体创建的库。如果是这样,我完全理解。我做了最长的时间是同一件事。但是,一旦花时间对Content-Type
标头和HTTP主体进行了一些剖析,就会发现它遵循了一种合理的逻辑模式。
首先,有Content-Type
标题。它包含有关您要发送的数据类型(multipart/form-data;
)和的信息boundary
。该边界应始终具有唯一的,有些随机的值。在上面的示例中,我使用了UUID
。由于多部分表单并非总是一次全部发送到服务器,而是成块发送,因此服务器需要某种方式来知道要发送的表单的特定部分何时结束或开始。这就是该boundary
值的用途。这必须在标头中传达,因为这是接收服务器能够读取的第一件事。
接下来,让我们看一下http正文。它以以下文本块开头:
--Boundary-3A42CBDB-01A2-4DDE-A9EE-425A344ABA13
Content-Disposition: form-data; name="family_name"
Wals
我们发送两个破折号(--
),后跟预定义的边界字符串(Boundary-3A42CBDB-01A2-4DDE-A9EE-425A344ABA13
),以通知服务器它将要读取新的内容块。在这种情况下为表单字段。由于下一行的第一行,服务器知道它正在接收表单字段Content-Disposition: form-data;
。它还知道family-name
该Content-Disposition
行将要接收的表单字段是由于该行的第二部分而被命名的name=“family_name”
。这之后是空行和我们要发送服务器的表单字段的值。
对于示例正文中的其他表单字段,将重复此模式:
--Boundary-3A42CBDB-01A2-4DDE-A9EE-425A344ABA13
Content-Disposition: form-data; name="name"
Donny
示例中的第三个字段略有不同。这是Content-Disposition
这个样子的:
Content-Disposition: form-data; name="file"; filename="somefilename.jpg"
它有一个名为的额外字段filename
。这告诉服务器,一旦上传成功,它就可以使用该名称引用上传的文件。文件本身的最后一块也有其自己的Content-Type
字段。这告诉服务器有关上载文件的Mime类型。在此示例中,这是image/png
因为我们正在上载一个虚构的png图像。
在那之后,您应该看到另一个空行,然后是大量的秘密数据。那就是原始图像数据。在所有这些数据之后,您将找到HTTP正文的最后一行:
--Boundary-E82EE6C1-377D-486C-AFE1-C0CE9A03E9A3--
这是最后一个边界,后缀为--
。这告诉服务器,它现在已经收到了我们要发送的所有HTTP数据。
每个表单字段本质上都具有相同的结构:
BOUNDARY
CONTENT TYPE
-- BLANK LINE --
VALUE
一旦了解了这种结构,多部分请求的HTTP主体看起来就不那么令人生畏了,并且实现自己的多部分上载器URLSession
似乎再也不可怕了。让我们深入研究并实现URLRequest
可以由URLSession
!执行的多部分功能。
准备包含图像的多部分请求
在上一节中,我们着重于一个多部分表单请求的内容,试图使它的内容不神秘。现在是时候构建一个URLRequest
,对其进行配置并对其进行构建,httpBody
以便我们可以使用URLSession
而不是第三方解决方案将其发送到服务器。
由于我只想专注于为包含文件的请求构建一个多部分,因此我不会向您展示如何获取用户可以上传的图像。
这项任务的第一部分非常简单。我们将创建一个URLRequest
,使其成为POST请求并设置其Content-Type
标头:
let boundary = "Boundary-\(UUID().uuidString)"
var request = URLRequest(url: URL(string: "https://some-page-on-a-server")!)
request.httpMethod = "POST"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
接下来,让我们看一下HTTP正文。在上一节中,您看到了多部分请求中的每个块都是类似构造的。让我们创建一个输出这些身体数据块的方法。
func convertFormField(named name: String, value: String, using boundary: String) -> String {
var fieldString = "--\(boundary)\r\n"
fieldString += "Content-Disposition: form-data; name=\"\(name)\"\r\n"
fieldString += "\r\n"
fieldString += "\(value)\r\n"
return fieldString
}
上面的代码几乎可以说明一切。我们构建了一个String
包含所有先前讨论的元素的。请注意,\r\n
该行将在每行之后添加到字符串中。这需要在字符串中添加新行,以便获得所需的输出。
尽管这对于包含文本的表单字段非常整洁,但是我们需要一种单独的方法来创建文件数据块,因为它的工作原理与其余部分略有不同。这主要是因为我们需要为文件指定内容类型,并且将文件数据作为value
而不是String
。以下代码可用于为文件创建主体块:
func convertFileData(fieldName: String, fileName: String, mimeType: String, fileData: Data, using boundary: String) -> Data {
let data = NSMutableData()
data.appendString("--\(boundary)\r\n")
data.appendString("Content-Disposition: form-data; name=\"\(fieldName)\"; filename=\"\(fileName)\"\r\n")
data.appendString("Content-Type: \(mimeType)\r\n\r\n")
data.append(fileData)
data.appendString("\r\n")
return data as Data
}
extension NSMutableData {
func appendString(_ string: String) {
if let data = string.data(using: .utf8) {
self.append(data)
}
}
}
这次String
创建的是而不是Data
。原因是双重的。一是我们已经有了文件数据。将其转换为a String
,然后再返回到Data
将其添加到HTTP正文时,这是浪费的。第二个原因是必须创建HTTP正文本身Data
而不是String
。为了向Data
对象添加文本,我们在上添加了扩展名NSMutableData
,将扩展名安全地添加为Data
。从方法的结构中,您应该能够得出它与前面显示的HTTP正文匹配的信息。
让我们将所有这些内容放在一起,完成网络请求的准备!
let httpBody = NSMutableData()
for (key, value) in formFields {
httpBody.appendString(convertFormField(named: key, value: value, using: boundary))
}
httpBody.append(convertFileData(fieldName: "image_field",
fileName: "imagename.png",
mimeType: "image/png",
fileData: imageData,
using: boundary))
httpBody.appendString("--\(boundary)--")
request.httpBody = httpBody as Data
print(String(data: httpBody as Data, encoding: .utf8)!)
此时,前面的代码应该不会太令人惊讶。您可以使用之前编写的方法来构造HTTP正文。添加表单字段后,您将在最后一个边界处加上两个尾划线,并将结果数据设置为请求的httpBody
。请注意此代码段末尾的打印语句。尝试打印HTTP正文,您会发现它与本文开头的格式完全匹配。
现在剩下要做的就是像平常一样运行您的请求:
URLSession.shared.dataTask(with: request) { data, response, error in
// handle the response here
}.resume()
class UploadImageTool {
func extractedFunc(_ fileUploadPath : String,_ imageName : String,_ uploadImageName : String,_ requestClosore : @escaping(_ data : [String : Any],_ error : String)->()) {
// Do any additional setup after loading the view.
let formFields = ["name": "Donny", "family_name": "Wals"]
let image = UIImage(named: uploadImageName)
let imageData = image?.pngData()
let boundary = "Boundary-\(UUID().uuidString)"
//curl -v -X POST 'http://218.85.55.50:8680/gateway/fileUpload' -H 'User-Agent: ning de shi zhong yi yuan zhang shang ban gong/1.0.2 (iPhone; iOS 13.3; Scale/3.00)' -H 'Accept-Language: en;q=1, zh-Hans-US;q=0.9' -H 'Content-Type: multipart/form-data; boundary=Boundary+92022B093FEEC055' -H 'Content-Length: 47609' -H 'x-metadata: {"platform":"1","checknum":"f776"}' -H 'Cookie:'
var request = URLRequest(url: URL(string: fileUploadPath)!)
request.httpMethod = "POST"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
let httpBody = NSMutableData()
for (key, value) in formFields {
httpBody.appendString(convertFormField(named: key, value: value, using: boundary))
}
httpBody.append(convertFileData(fieldName: "image_field",
fileName: "\(imageName).jpg",
mimeType: "image/jpeg",
fileData: imageData ?? Data(),
using: boundary))
httpBody.appendString("--\(boundary)--")
request.httpBody = httpBody as Data
URLSession.shared.dataTask(with: request) { data, response, error in
let json = try? JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments) as? [String : Any]
requestClosore(json ?? [String : Any](),error.debugDescription)
}.resume()
}
func convertFormField(named name: String, value: String, using boundary: String) -> String {
var fieldString = "--\(boundary)\r\n"
fieldString += "Content-Disposition: form-data; name=\"\(name)\"\r\n"
fieldString += "\r\n"
fieldString += "\(value)\r\n"
return fieldString
}
func convertFileData(fieldName: String, fileName: String, mimeType: String, fileData: Data, using boundary: String) -> Data {
let data = NSMutableData()
data.appendString("--\(boundary)\r\n")
data.appendString("Content-Disposition: form-data; name=\"\(fieldName)\"; filename=\"\(fileName)\"\r\n")
data.appendString("Content-Type: \(mimeType)\r\n\r\n")
data.append(fileData)
data.appendString("\r\n")
return data as Data
}
}
extension NSMutableData {
func appendString(_ string: String) {
if let data = string.data(using: .utf8) {
self.append(data)
}
}
}
使用:
UploadImageTool().extractedFunc("服务器地址", "上传名称", "本地图片名称") { (data, error) in
print(data)
}
网友评论