自己动手写一个 iOS 网络请求库(四)——快速文件上传
2015-5-29 / 阅读数:19337 / 分类: iOS & Swift
本篇文章是此系列文章的终结篇倒数第二篇,我们将一起给我们的网络请求库增加“快速文件上传”的功能。
HTTP 协议解析
找资料
我翻出了以前买的《图解 HTTP》:
找到第 46-47 页,“发送多种数据的多部分对象集合”:
multipart/form-data// HTTP 头 开始
Content-Type: multipart/form-data; boundary=PitayaUGl0YXlh
// HTTP 头 结束
// HTTP Body 开始
--PitayaUGl0YXlh
Content-Disposition: form-data; name="field1"
John Lui
--PitayaUGl0YXlh
Content-Disposition: form-data; name="text"; filename="file1.txt"
···[file1.txt 的数据]···
--PitayaUGl0YXlh--
// HTTP Body 结束
详解
HTTP 协议是一种非常基础的“字符串格式化约定”,本质上传输的依然是一堆字符,只是由于遵守了标准协议,后端的 HTTP 服务软件(Apache、nginx)和前端的浏览器、NSData、NSURLSession 等接口可以顺畅地交流。
在 HTTP 协议中,上传文件可以进行如下设置:
设定 Content-Type 头字段如下:Content-Type: multipart/form-data; boundary=PitayaUGl0YXlh
boundary 是我们自己指定的间隔符。
之后设定 HTTP Body 如下:--PitayaUGl0YXlh
Content-Disposition: form-data; name="field1"
John Lui
--PitayaUGl0YXlh
Content-Disposition: form-data; name="text"; filename="file1.txt"
···[file1.txt 的数据]···
--PitayaUGl0YXlh--
每个字段以 “--间隔符” 开头,最后总体以 “--间隔符--” 结尾。
换行
HTTP 协议中,换行必须用 \r\n,我尝试过只使用 \n 换行,系统会直接原封不动地发送这个换行,如果后端的 HTTP 服务器不支持这种容错的话,可能就会出问题,所以建议大家还是要遵守标准协议。
代码实现
构建 File 结构体
上传文件也是表单,也需要一个 name,所以我们需要构造一个 File 结构体,来描述要上传的文件:struct File {
let name: String!
let url: NSURL!
init(name: String, url: NSURL) {
self.name = name
self.url = url
}
}
上面代码中,我们使用 NSURL 来描述文件地址。
增加 files 类成员变量并初始化class NetworkManager {
let method: String!
let params: Dictionary
let callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void
// add files
var files: Array
let session = NSURLSession.sharedSession()
let url: String!
var request: NSMutableURLRequest!
var task: NSURLSessionTask!
// add files
init(url: String, method: String, params: Dictionary = Dictionary(), files: Array = Array(), callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
self.url = url
self.request = NSMutableURLRequest(URL: NSURL(string: url)!)
self.method = method
self.params = params
self.callback = callback
// add files
self.files = files
}
......
}
增加 boundary 类成员常量class NetworkManager {
let boundary = "PitayaUGl0YXlh"
......
}
更改 Content-Typeif self.files.count > 0 {
request.addValue("multipart/form-data; boundary=" + self.boundary, forHTTPHeaderField: "Content-Type")
} else if self.params.count > 0 {
request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
}
修改 buildBody 函数func buildBody() {
let data = NSMutableData()
if self.files.count > 0 {
if self.method == "GET" {
NSLog("\n\n------------------------\nThe remote server may not accept GET method with HTTP body. But Pitaya will send it anyway.\n------------------------\n\n")
}
for (key, value) in self.params {
data.appendData("--\(self.boundary)\r\n".nsdata)
data.appendData("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".nsdata)
data.appendData("\(value.description)\r\n".nsdata)
}
for file in self.files {
data.appendData("--\(self.boundary)\r\n".nsdata)
data.appendData("Content-Disposition: form-data; name=\"\(file.name)\"; filename=\"\(file.url.description.lastPathComponent)\"\r\n\r\n".nsdata)
if let a = NSData(contentsOfURL: file.url) {
data.appendData(a)
data.appendData("\r\n".nsdata)
}
}
data.appendData("--\(self.boundary)--\r\n".nsdata)
} else if self.params.count > 0 && self.method != "GET" {
data.appendData(buildParams(self.params).nsdata)
}
request.HTTPBody = data
}
调整 Network.request 接口群,增加上传文件 APIstatic func request(method: String, url: String, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
let manager = NetworkManager(url: url, method: method, callback: callback)
manager.fire()
}
static func request(method: String, url: String, params: Dictionary, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
let manager = NetworkManager(url: url, method: method, params: params, callback: callback)
manager.fire()
}
static func request(method: String, url: String, files: Array, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
let manager = NetworkManager(url: url, method: method, files: files, callback: callback)
manager.fire()
}
static func request(method: String, url: String, params: Dictionary, files: Array, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
let manager = NetworkManager(url: url, method: method, params: params, files: files, callback: callback)
manager.fire()
}
检验成果
增加一张图片用于上传文件测试:
测试代码如下:let file = File(name: "file", url: NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("Pitaya", ofType: "png")!)!)
Network.request("POST", url: "http://pitayaswift.sinaapp.com/pitaya.php", files: [file]) { (data, response, error) -> Void in
let string = NSString(data: data, encoding: NSUTF8StringEncoding) as! String
if string == "1" {
println("上传文件成功!")
}
}
运行项目,点击按钮,输出结果,成功!
快在哪里?
Alamofire 并不支持表单文件上传,似乎只支持流文件上传(不确定),故我之前在使用 Alamofire 的时候,是把二进制文件读出来之后进行 base64 编码,然后当做字符串字段传输的,除了体积会增大三分之一外,最严重的问题在于非常长的 HTTP 准备时间(开始发送数据包之前的处理时间),这期间还是阻塞的。实际测试,无论是 A5 处理器的 touch5 还是 A8 处理器的 iPhone6,500KB 的语音文件都需要接近 30S 的预处理时间。阻塞问题可以通过超线程方式解决,但是总体上传时间依然是非常长的,500 KB 的语音文件的预处理时间和网络传输时间几乎都一样长了。
快在哪里?采用 NSData 方式直接赋值给 HTTP Body,这种方式不会消耗任何预处理时间,当然也不会对主线程造成阻塞。而且传输的字符串的长度减少 25%,实际测试 500KB 语音文件上传速度从 57S 缩短为 21S,增速十分可观。
WRITTEN BY
程序员,Swift Contributor,正在写《iOS 可视化编程与 Auto Layout》。
评论:
BDSir
2016-10-26 11:08
能不能写一个HomeKit的demo供我们这些小白学习下
rose
2015-11-27 17:11
我用你的方法把图片上传给后台之后, 后台说要把图片赋给他指定的参数, 要怎么做啊 急 急 急!!!
rose
2015-11-27 17:07
请问上传图片的时候 图片的名字是随便起的么,后台需要一个参数来接收这些图片么?我怎么把这个图片放到后台的参数里面呢
2015-11-27 17:12
@rose:1. 名字是随便起的,这只是一个普通参数
2. 后台根据 name 来获取表单的参数,这是 HTTP 协议规定的
3. 使用我的方法就可以把图片放到请求中,至于后台怎么接收,你可以去问问后端程序员。。。
rose
2015-11-27 17:19
@JohnLui:后台说 他有一参数名photo 用来接收图片信息,一张图片的话就是一张图片信息, 多张就是一个数组,存放多张图片信息,我要怎么这多张图片放到 photo这个参数里面呢
2015-11-27 23:37
@rose:你的后端应该不懂 HTTP 协议的细节,只局限在他所使用的语言提供的功能里。我直接说解决方案吧:多张照片,一个名字,叫 name[]
rose
2015-11-30 13:43
@JohnLui:
非常感谢 我现在明白了 还了解了一点 http 协议 非常感谢 !!!
游客
2015-09-28 09:53
很不错,从里面学到了很多东西, 请问添加 摘要认证 怎么做? 最好在多些其它方式的证书认证,希望您在出一篇这样的问章。
3ks
喵~
2015-07-13 13:55
啥时候图解一下size classes喵~
发表评论:
昵称
邮件地址 (选填)
个人主页 (选填)