你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 自己動手寫一個 iOS 網絡請求庫(四)——快速文件上傳

自己動手寫一個 iOS 網絡請求庫(四)——快速文件上傳

編輯:IOS開發基礎

0.jpg

自己動手寫一個 iOS 網絡請求庫(一)—— NSURLSession 初探

自己動手寫一個 iOS 網絡請求庫(二)——封裝接口

自己動手寫一個 iOS 網絡請求庫(三)——降低耦合

代碼示例:https://github.com/johnlui/Swift-On-iOS/blob/master/BuildYourHTTPRequestLibrary

開源項目:Pitaya,適合大文件上傳的 HTTP 請求庫:https://github.com/johnlui/Pitaya

本篇文章是此系列文章的終結篇,我們將一起給我們的網絡請求庫增加“快速文件上傳”的功能。

HTTP 協議解析

找資料

我翻出了以前買的《圖解 HTTP》:

1.jpg

找到第 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-Type

if 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
}

.nsdata 屬性是我對 String 做的一個擴展,代碼在:https://github.com/johnlui/Swift-On-iOS/blob/master/BuildYourHTTPRequestLibrary/BuildYourHTTPRequestLibrary/Network.swift#L46-L50

調整 Network.request 接口群,增加上傳文件 API

static 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()
}

檢驗成果

增加一張圖片用於上傳文件測試:

blob.png

測試代碼如下:

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("上傳文件成功!")
    }
}

http://pitayaswift.sinaapp.com/pitaya.php 會在收到 name="file" 的文件之後,輸出 1。

運行項目,點擊按鈕,輸出結果,成功!

快在哪裡?

Alamofire 並不支持表單文件上傳,似乎只支持流文件上傳(不確定),故我之前在使用 Alamofire 的時候,是把二進制文件讀出來之後進行 base64 編碼,然後當做字符串字段傳輸的,除了體積會增大三分之一外,最嚴重的問題在於非常長的 HTTP 准備時間(開始發送數據包之前的處理時間),這期間還是阻塞的。實際測試,無論是 A5 處理器的 touch5 還是 A8 處理器的 iPhone6,500KB 的語音文件都需要接近 30S 的預處理時間。阻塞問題可以通過超線程方式解決,但是總體上傳時間依然是非常長的,500 KB 的語音文件的預處理時間和網絡傳輸時間幾乎都一樣長了。

快在哪裡?采用 NSData 方式直接賦值給 HTTP Body,這種方式不會消耗任何預處理時間,當然也不會對主線程造成阻塞。而且傳輸的字符串的長度減少 25%,實際測試 500KB 語音文件上傳速度從 57S 縮短為 21S,增速十分可觀。

《自己動手寫一個 iOS 網絡請求庫》系列文章到此結束,謝謝大家!

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved