原文
去了餓廠面試後了解到了自己計算機基礎的薄弱, 非科班出身薄弱也是自然的, 說實話, 我也並不是特別想要往底層深究, 因為越底層的東西越會抽象成服務輸送給大眾, 就好比自來水, 一般人都不會想要去了解自來水的底層邏輯吧, 但作為開發者, 我們還是得了解下基礎的網絡概念.
序
關於HTTP與HTML的發明有個很有趣的插曲, 那就是首個萬維網服務器與浏覽器是在一台NeXTStep計算機上編寫的, 在1997年, Apple收購了NeXTStep Computer並將NeXTStep作為mac OS的基礎後來成為了iOS的基礎.
URL
每個Web資源被稱為統一資源標識符(Uniform Resource Identifier, URI) 其中包括 URL 和 URN, 現在幾乎所有的URI都是URL.
URL 通用格式
幾乎沒有哪個URL中包含了所有這些組件, URL最重要的三個部分是 (scheme 方案), (host 主機), (path 路徑)
比如說, 你想要獲取URL https://www.apple.com/index.html 那麼URL包含以下三個部分:
https 是URL方案(scheme). 方案告訴客戶端如何訪問資源.
www.apple.com 是服務器的位置, 告知客戶端資源位於何處.
/index.html 是資源路徑, 說明了請求的是服務器上哪個指定文件資源.
構建網絡架構URL時遵循服務版本化和服務定位器原則.
報文
所有的HTTP報文都可以分為兩類, 請求報文(request message) 和 響應報文 (response message) 我們可以通過Chrome模擬請求得到請求報文和響應報文, 我們來簡單的看一下首部中的一些簡單的概念.
響應首部
HTTP/1.1 200 OK Access-Control-Allow-Origin: http://localhost:3000 Access-Control-Allow-Methods: GET,HEAD,PUT,POST,DELETE Content-Type: application/json; charset=utf-8 Content-Length: 2180 Date: Mon, 31 Jul 2017 02:56:17 GMT Connection: keep-alive
由上述響應首部, 我們可得知以下信息:
應用程序支持最高的HTTP版本號為1.1.
狀態碼200表示請求成功. 如為3XX表示重定向, 4XX表示客戶端錯誤, 5XX表示服務器錯誤.
原因短語OK僅為顯示, 並無實際含義.
Content-Type就是MIME Type, 用以區分傳輸資源, 例子中主體部分是字符集為utf-8的json數據.
Content-Length表示主體部分包含了2180字節的數據.
Date表示了服務器產生響應的日期.
Connection 連接類型為keep-alive.
Access-Control-Allow-Origin 服務器域名為http://localhost:3000.
Access-Control-Allow-Methods服務器實現的方法為GET,HEAD,PUT,POST,DELETE.
請求首部
GET /api/J1/getJ1List HTTP/1.1
Host: localhost:3001
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Accept: application/json, text/plain, */*
Origin: http://localhost:3000
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1
Referer: http://localhost:3000/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.8
由上述請求首部, 我們可以得知以下信息:
使用了GET方法進行請求, 請求的路由為/api/J1/getJ1List, HTTP版本號為1.1
Host 提供了接受請求的服務器的主機名和端口號localhost:3001.
Connection 和響應首部信息對照.
Pragma 隨報文傳送指示的方式, 並不專用於緩存.
Cache-Control 用於隨報文傳送緩存指示.
Accept 接受的任意媒體類型, 和響應首部的Content-Type信息對照
Origin 當前訪問域名, 與Access-Control-Allow-Origin信息對照
User-Agent 將發起請求的應用程序名稱告知服務器.
Referer 提供了包含當前請求URI的文檔的URL.
Accept-Encoding 告訴服務器能夠發送哪些編碼方式.
Accept-Language 告訴服務器能夠發送哪些語言.
實體
具體來說, HTTP承載的實體需要滿足以下條件.
可以被正確識別(通過Content-Type首部說明媒體格式, Content-Language首部說明語言), 以便浏覽器和其他客戶端能正確處理內容.
可以被正確地解包(通過Content-Length首部和Content-Encoding首部).
是最新的(通過實體驗證碼和緩存過期控制).
符合用戶的需要(基於Accept系列的內容協商首部).
在網絡上可以快速有效地傳輸(通過范圍請求, 差異編碼以及其他數據壓縮方法).
完整到達, 未被篡改(通過傳輸編碼首部和Content-MD5校驗和首部).
HTTP / 1.1版定義了一下10個基本字體首部字段.
Content-Type 實體中所承載對象的類型.
Content-Length 所傳送實體的長度或大小.
Content-Language 與所傳送實體主體的長度或大小.
Content-Encoding 對象數據所做的任意變換 (比如, 壓縮).
Content-Location 一個備用位置, 請求時可通過它獲得對象.
Content-MD5 實體主體內容的校驗和.
Last-Modified 所傳輸內容在服務器上創建或最後修改的日期時間.
Expires 實體數據將要失效的日期時間.
Allow 該資源所允許的各種請求方法, 例如, GET和HEAD.
ETag 這份文檔特定實例的唯一驗證碼. ETag首部沒有正式定義為實體首部, 但它對許多涉及實體的操作來說, 都是一個重要的首部.
Chahe-Control 指出應該如何緩存該文檔, 和ETag首部類似, Chche-Control首部也沒有正式定義為實體首部.
連接
世界上幾乎所有的HTTP通信都是由TCP/IP承載的, HTTP要傳送一條報文時, 會以流的形式將報文數據的內容通過一條打開的TCP鏈接按序傳輸, TCP收到數據流之後, 會將數據流砍成被稱作段的小數據塊, 並將段封裝在IP分組中.
iOS URLSession
我們先來簡單的看下iOS中如何使用HTTP網絡, 使用系統的URLSession進行網絡請求, 將請求方法設置為GET, 當然默認就是GET, 使用單例創建URLSession進行任務回調, URLSession是異步請求, dataTask默認是關閉狀態, 需要手動開啟dataTask.resume().
var request = URLRequest(url: URL(string: "http://localhost:3001/api/J1/getJ1List")!) request.httpMethod = "GET" let session = URLSession.shared let dataTask = session.dataTask(with: request) { (data, response, err) in if err != nil { print(err.debugDescription) } else { let responseStr = String(data: data!, encoding: String.Encoding.utf8) print(responseStr!) print("mimeType: \(String(describing: response?.mimeType))") } if let response = response as? HTTPURLResponse { print("statusCode: \(response.statusCode)") for (tab, result) in response.allHeaderFields { print("\(tab.description) - \(result)") } if response.statusCode == 200 { print(response) } } } dataTask.resume()
結合上面的內容, 我們發送了一個GET請求到http://localhost:3001/api/J1/getJ1List, 現在我們就會分析URL了, 方案是http, 主機為localhost, 端口號為3001, 路徑為/api/J1/getJ1List
{ URL: http://localhost:3001/api/J1/getJ1List } { status code: 200, headers { "Access-Control-Allow-Methods" = "GET,HEAD,PUT,POST,DELETE"; "Access-Control-Allow-Origin" = "*"; Connection = "keep-alive"; "Content-Length" = 2180; "Content-Type" = "application/json; charset=utf-8"; Date = "Mon, 31 Jul 2017 07:29:52 GMT"; } }
返回的響應報文與Chrome中顯示相同, 在iOS9之後系統推薦使用URLSeesion, 使用起來非常的方便快捷. 當然URLSession的功能不止於此, 若想深究請看官方文檔, 在網絡可達性方面使用系統Reachability框架.
緩存
HTTP為我們提供了幾個用來對已緩存對象進行再驗證的工具嗎但最常用的是If-Modified-Since和If-None-Match首部. 將這個首部添加到GET請求中去, 就可以告訴服務器, 只有在緩存了對象的副本之後, 又對其進行了修改的情況下, 才發送此對象.
iOS NSURLRequestCachePolicy
typedef NS_ENUM(NSUInteger, NSURLRequestCachePolicy) { NSURLRequestUseProtocolCachePolicy = 0, NSURLRequestReloadIgnoringLocalCacheData = 1, NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4, // Unimplemented NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData, NSURLRequestReturnCacheDataElseLoad = 2, NSURLRequestReturnCacheDataDontLoad = 3, NSURLRequestReloadRevalidatingCacheData = 5, // Unimplemented };
設置緩存策略會對應的添加請求首部Cache-Control等到URLRequest中.
緩存的處理步驟
接收: 緩存從網絡中讀取抵達的請求報文.
解析: 緩存對報文進行解析, 提取出URL和各種首部.
查詢: 緩存查看是否有本地副本可用, 如果沒有, 就獲取一份副本 (並將其保存在本地).
新鮮度檢測: 緩存查看已緩存副本是否足夠新鮮, 如果不是, 就詢問服務器是否有任何更新.
創建響應: 緩存會用新的首部和一緩存的主體來構建一條響應報文.
發送: 緩存通過網絡將響應發回客戶端.
日志: 緩存可選地創建一個日志文件條目來描述這個事務.
Cookie
可以籠統的將cookie分為兩類: 會話cookie和持久cookie. 會話cookie是一種臨時cookie, 它記錄了用戶訪問站點時的設置和偏好. 用戶退出浏覽器時, 會話cookie就被刪除了. 持久cookie的生存時間更長一些, 他們存儲在硬盤上, 浏覽器退出, 計算機重啟時他們仍然存在, 通常會用持久cookie維護某個用戶會周期性訪問的站點的配置文件或登錄名.
會話cookie和持久cookie之間唯一的區別就是它們的過期時間, 如果設置了Discard參數, 或者沒有設置Expires或Max-Age參數來說明擴展的過期時間, 這個cookie就是一個會話cookie.
iOS HTTPCookie / HTTPCookieStorage
// 阻止應用保存`cookie`. HTTPCookieStorage.shared.cookieAcceptPolicy = .never
// 從響應中獲取`cookie`. guard let url = URL(string: "http://localhost:3001/api/J1/getJ1List") else { return } let request = URLRequest(url: url) let session = URLSession.shared let dataTask = session.dataTask(with: request) { (data, response, err) in if err != nil { print(err.debugDescription) } else { let responseStr = String(data: data!, encoding: String.Encoding.utf8) print(responseStr!) print("mimeType: \(String(describing: response?.mimeType))") } if let response = response as? HTTPURLResponse { print("statusCode: \(response.statusCode)") 1 // get cookie from response let cookies = HTTPCookie.cookies(withResponseHeaderFields: response.allHeaderFields as! [String : String], for: url) 1 for cookie in cookies { print("Cookie: \(cookie)") } 1 for (tab, result) in response.allHeaderFields { print("\(tab.description) - \(result)") } if response.statusCode == 200 { print(response) } } } dataTask.resume()
// 刪除cookie. func deleteCookie(cookieName:String, url:URL) { let jar = HTTPCookieStorage.shared guard let storedcookies = jar.cookies(for: url) else { return } for cookie in storedcookies { jar.deleteCookie(cookie) } }
// 創建cookie. guard let url = URL(string: "http://localhost:3001/api/J1/getJ1List") else { return } let properties = [HTTPCookiePropertyKey.name : "FOO", HTTPCookiePropertyKey.value : "This is foo", HTTPCookiePropertyKey.path : "/", HTTPCookiePropertyKey.originURL : "url"] guard let cookie = HTTPCookie.init(properties: properties) else { return } var request = URLRequest(url: url) var newCookies: [HTTPCookie] = [cookie] var newHeaders = HTTPCookie.requestHeaderFields(with: newCookies) request.allHTTPHeaderFields = newHeaders 1 let dataTask = session.dataTask(with: request) { (data, response, err) in { ... } } dataTask.resume()
cookie是可以禁止的, 而且可以通過日志分析或其他方式來實現大部分跟蹤記錄, 所以cookie自身並不是很大的安全隱患. 實際上, 可以通過提供一個標准的審查方法在遠程數據庫中保存個人信息, 並將匿名cookie作為鍵值, 來降低客戶端到服務器的敏感數據傳輸頻率.
認證
認證就是要給出一些身份信息, 當出示像護照或駕照那樣有照片的身份證件時, 就給出了一些證據, 說明你就是你所聲稱的那個人, 在自動取款機上輸入PIN碼, 或在計算機系統的對話框中輸入了密碼時, 也是在證明你就是你所聲稱的那個人.
HTTP提供了一個原生的質詢 / 響應(challenge / response)框架, 簡化了對用戶的認證過程.
iOS URLProtectionSpace
_ = URLProtectionSpace(host: "localhost", port: 3001, protocol: NSURLProtectionSpaceHTTP, realm: "moblie", authenticationMethod: NSURLAuthenticationMethodDefault)
最佳實踐是使用URLProtectionSpace驗證手機銀行應用的用戶與安全的銀行服務器進行通信, 特別是在發出的請求會操縱後端數據時更是如此. URLProtectionSpace是要認證的服務器或域, 是多有進來的URLAuthenticationChallenges的一個屬性.
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { let defaultSpace = URLProtectionSpace(host: "localhost", port: 3001, protocol: NSURLProtectionSpaceHTTP, realm: "mobile", authenticationMethod: NSURLAuthenticationMethodDefault) let trustSpace = URLProtectionSpace(host: "localhost", port: 3001, protocol: NSURLAuthenticationMethodDefault, realm: "mobile", authenticationMethod: NSURLAuthenticationMethodClientCertificate) let validSpaces = [defaultSpace, trustSpace] if !validSpaces.contains(challenge.protectionSpace) { let msg = "We're unable to establish a secure connection. Please check your network connection and try again" DispatchQueue.main.async { let alert = UIAlertController(title: "Unsecure Connection", message: msg, preferredStyle: .alert) alert .addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) self.present(alert, animated: true, completion: nil) } challenge.sender?.cancel(challenge) } }
上述代碼片段添加了額外的保護空間, 這位後端提供了一些靈活性. 當確定要支持的保護控件後, 請創建它們, 然後將它們添加到數組中以便與進來的認證挑戰相比較. 實際上, 你應該定義有效的保護控件作為模型層的一部分, 這樣就可以在所有網絡中重用它們了, 如果認證挑戰的保護控件與所有支持的空間不匹配, 那麼你應該通知用戶取消認證質詢.
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic { if challenge.previousFailureCount == 0 { let creds = URLCredential(user: "Castie!", password: "******", persistence: URLCredential.Persistence.forSession) challenge.sender?.use(creds, for: challenge) } else { challenge.sender?.cancel(challenge) DispatchQueue.main.async { let alert = UIAlertController(title: "Unsecure Connection", message: msg, preferredStyle: .alert) alert .addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) self.present(alert, animated: true, completion: nil) } } } }
在確定質詢是針對HTTP Basic或另一種支持的質詢類型後, 應該確保沒有失敗, 並使用用戶輸入的用戶名與密碼創建URLCredential對象. 如果質詢失敗, 那就警告用戶並取消質詢.
URLCredentialStorage.shared.set(creds, for: protectionSpace)
使用URLCredentialStorage可以處理證書數據的認證響應.
優化
iOS 用戶都希望應用能夠立刻響應每個請求, 移動產業都有這樣一條原則, 即屏幕越小, 用戶越沒耐心. 提供讓用戶樂於使用的應用意味著要珍惜用戶的時間, 就像珍惜你自己的時間一樣. 通過壓縮請求和響應來優化應用所使用的帶寬, 通過管道化請求避免不必要的延遲, 甚至通過緩存響應來避免冗余的網絡請求都會加速應用並改進用戶體驗.
壓縮請求和響應
在默認情況下, URLSession 會為每個請求添加Accept-Encoding: gzip, deflate 告知服務器, 客戶端可以接收使用gzip或DEFLATE壓縮的負載, 不過服務器可以自己選擇是否壓縮響應. 這樣, 通過響應負載壓縮來提升性能的關鍵在於配置服務器以支持壓縮.
如果想要禁用負載壓縮, 應用可以通過清除自動設定的Accept-Encoding首部來實現.
var request = URLRequest(url: URL(string: "http://localhost:3000")!, cachePolicy: URLRequest.CachePolicy.useProtocolCachePolicy, timeoutInterval: 20) request.addValue("", forHTTPHeaderField: "Accept-Encoding")
HTTP管道
HTTP管道是重用現有TCP連接的一種方式, 它使得HTTP客戶端能夠在對第一個請求的響應返回前在相同的TCP Socket上發送第二個請求, 響應返回的順序與請求發起的順序保持一致.
var request = URLRequest(url: URL(string: "http://localhost:3000")!) request.httpShouldUsePipelining = true
重定義緩存
除了上述提到的緩存策略, iOS還提供了重新定義默認的緩存, 並指定了更大的內存容量和持久化儲存, 以便在應用重啟後依然可以使用.
let cache = URLCache(memoryCapacity: 1024 * 1024, diskCapacity: 1024 * 1024 * 20, diskPath: "URLCache") URLCache.shared = cache
最後
其實HTTP是一個很復雜的工程, 包括很多的首部定義及各種代理網關, DNS及均衡負載等知識, 不過這些一般都是由服務端進行完成, 不過好在現在BAAS這種雲平台的出現極大的便利了我們這些前端開發者, 但基礎的知識還是需要了解的, 你說呢?
作者:Castie1
鏈接:http://www.jianshu.com/p/f1aadc9f5dc3
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。