從產品上線前的接口開發和調試,到上線后的 bug 定位、性能優化,網絡編程知識貫穿著一個互聯網產品的整個生命周期。不論你是前后端的開發崗位,還是 SQA、運維等其他技術崗位,掌握網絡編程知識均是崗位的基礎要求,即使是產品、設計等非技術崗位,在灰度環境體驗產品時也需要理解頁面緩存、Host 切換等網絡基礎概念。
「貓哥網絡編程系列」一直是我想沉淀的一個技術知識點,因為我認為:網絡編程相關知識(尤其是 HTTP 協議),是互聯網產品開發當中最重要的基礎知識(沒有之一)。掌握這方面的基礎知識,對一個新手程序員來說至關重要。本系列將會在我的微信公眾號「貓哥學前班」上連載,并在 Github 上維護更新。
使用「詳解 BAT 面試題」作為本文的副標題,是為了吸引更多的技術新人瀏覽此文,然而本文并非標題黨。掌握本文所提到的知識點必將大幅提升程序員的面試成功率,因為「網絡編程」方面的基礎知識,是 BAT 面試的必考項目。
從 BAT 面試題說起
2009 年我在支付寶做前端開發時,參與草擬了一份非正式的前端崗位招聘要求。
這里有:
國內最大的第三方支付舞臺,體驗億萬資金穿梭代碼的快感;
一群熱愛前端技術的伙伴,最快的成長經歷;
持續的培訓體系,完善的項目開發環境,最具潛力的UED團隊。
你需要:
熱愛前端,熱愛設計,對新鮮事物充滿好奇心,喜歡搗鼓各種互聯網應用;(興趣、學習能力、創新能力)
自我管理能力強,健康的創業心態,樂于分享與溝通;(心態、分享、性格)
具備基本的前端素質,了解WEB標準化、性能優化方法,了解可用性、可訪問性;(基本技能)
能和設計師談產品設計,和后端開發研討技術實現方案,制定服務接口,崇尚團隊合作;(向前向后能力)
當時在面試時最流行問的前端技術問題是:Web 標準化、AJAX 與 YSlow。
- 問:一個 AJAX 請求從開始創建到最后的響應階段,在其整個生命周期中,使用到了哪些 JavaScript 對象與方法?
- 問:YSlow 的 34 條性能優化建議中,哪些與 HTTP 協議相關,請盡可能多的列舉出來,并說說你的理解?
2011 年我開始成為騰訊的前端開發面試官,負責騰訊電商的前端開發(網頁重構方向)筆試出題與面試工作。在 2012 年的校招過程中,我發現不論是我出的筆試題,還是其他面試官出的題目,HTTP 協議相關的知識都是必考項。例如,
- 問:HTTP 協議中與緩存相關的 HTTP Header 有哪些?
- 問:列舉出你所知道的 HTTP 狀態碼,并描述它們的含義與發生的場景?
后來我在學習百度 FIS 框架的過程中,無意間看到百度 FEX 團隊的這份開源前端開發面試題,不出所料,同樣有一道與網絡編程相關的題目:
一個頁面從輸入 URL 到頁面加載完的過程中都發生了什么事情?越詳細越好
由此可見,網絡編程相關知識的確是 BAT 前端面試題中的必考項,而對于負責輸出 API 接口的后臺開發崗位來說,更是如此:
- 問:一個 POST 請求的 Content-Type 有多少種,傳輸的數據格式有何區別?
- 問:什么是 RESTful API,如何設計一個 Open API 的接口?
解題思路
接下來我們一起探討下具體的解題思路,瀏覽完本文之后,你將會首先掌握 HTTP 協議相關的前后端基礎知識。
要掌握 HTTP ,就需要先看到 HTTP 到底長什么樣?(不了解「網絡七層協議模型」和 TCP 的同學先不著急,本系列的后面幾篇會涉及到。)
1、安裝 HTTP 抓包工具
在 Chrome 開發者工具下我們可以看到,打開一個網頁后,瀏覽器會發起許多 HTTP 的請求(HTTP Request),這些請求經過服務器端處理后會返回對應的數據(HTTP Response),瀏覽器會按照這些數據的類型將它們渲染出來。
Chrome 中看到的 Request/Response Header 是其格式化之后的形式,要看到它們的原始模樣(Raw Source),我們需要借助兩個 HTTP 接口調試利器。
其中 Windows 系統下使用 Fiddler,Mac 系統下使用 Charles。Fiddler 具體的安裝與使用教程,請自行百度(安裝 Fiddler4 還需同時安裝 .NET Framework 4),Charles 相關教程,推薦參考 iOS 大神唐巧的《Charles 從入門到精通》。使用 Linux 系統的說明已經是網絡編程方面的大牛了,不需要繼續往下看 :P
2、查看 HTTP 詳細報文
運行 Fiddler(或 Charles) 之后,使用 Chrome 瀏覽器打開「貓哥學前班」的新浪微博主頁:http://weibo.com/mgxqb
在 Fiddler 左側面板下選中該條 HTTP 請求,再將右側面板的請求部分和響應部分都切換到 Raw 標簽頁。如下圖所示:
Charles 下的操作與 Fiddler 類似:
HTTP 協議規范由 W3C 制定,與具體的抓包工具無關,接下來我們主要以 Charles 為例,詳細講解下 HTTP 的報文格式,這對理解基于 HTTP 的 API 接口設計和網頁性能優化有很大幫助。
我們先看一下請求頭的源碼(Request Raw),為了防止隱私泄露,我已刪除部分 Cookie 信息:
GET /mgxqb HTTP/1.1 Host: weibo.com Cache-Control: max-age=0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.73 Safari/537.36 Accept-Encoding: gzip, deflate, sdch Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh;q=0.4 Cookie: YF-Page-G0=f70469e0b5607cacf38b47457e34254f; _s_tentry=passport.weibo.com
仔細觀察以上源碼,我們能大概總結出 HTTP 協議的格式規范:
- 第一行定義了請求類型(
GET
)、請求路徑(/mgxqb
)與協議類型及其版本號(HTTP/1.1
),使用一個半角空格間隔這三塊信息; - 示例源碼的最后是兩個空行。由于 HTTP 規范中要求一個合法的 HTTP 報文至少包含有一個空行,其中第一個空行用來間隔報文的頭部信息(HTTP Request/Response Header)和主體信息(HTTP Request/Response Body)。在空行的下一行是報文的主體信息,由于本例為 GET 類型請求,其主體(Body)信息通常為空,這便是第二個空行的含義;
- 余下的部分有著相同的格式,即 「HTTP Header 字段名+半角冒號+半角空格+值」,我們可以把它看成YAML 格式的簡易版。其中 HTTP Header 在規范中有著明確的定義,具體參見 HTTP頭字段列表。
這便是一個 HTTP 協議報文的源碼格式,以下我們簡單講解下最常見的 HTTP header 的含義。
3、常見 HTTP header
User-Agent:客戶端身份標識
User-Agent (以下簡稱 UA)字段記錄了訪問當前網頁的用戶瀏覽器的類型與版本、操作系統類型與版本。根據不同的 UA 信息,提供不同的站點內容是使用 UA 的常見場景。例如,如果用戶使用手機訪問魅族官網www.meizu.com,瀏覽器會自動跳轉至魅族手機官網 m.meizu.com。這種跳轉實現既可以由前端 JavaScript 完成,也可以通過后端返回 302 重定向來完成。
JavaScript 訪問 window.navigator.userAgent
屬性即可獲取該信息。雖然該屬性是只讀的,但有很多前端手段可以偽造 UA 。如下圖,Chrome 開發者工具在模擬不同的手機機型時,也會改變瀏覽器 UA 值。由此可見,通過檢測 HTTP User-Agent Header 來識別是否為爬蟲程序,不是一個有效的方法。
在 PHP 中,所有的 HTTP Header 字段信息都保存在 $_SERVER
對象中,通過訪問$_SERVER['HTTP_USER_AGENT']
即可獲取 User-Agent 的值。
Cookie:用戶身份標識
由于 HTTP 協議最初被設計成一種無狀態的數據傳輸協議,服務器端無法判斷每次處理的請求相互之間以及與之前處理的請求之間的關系,Cookie 的設計就是為了解決這個問題。
用戶在瀏覽器中首次訪問一個站點時,會通過請求響應頭或頁面JS腳本生成一些用于標識用戶身份的 Cookie 信息,這些信息會按照域名分類,存放在瀏覽器本地緩存文件當中。例如 Windows 系統下通過訪問 「C:\Users<用戶名>\AppData\Local\Microsoft\Windows\Temporary Internet Files」 目錄可以查看到 IE 瀏覽器保存在本地的 Cookie 文件。當用戶再次訪問該站點時,這些 Cookie 信息會被瀏覽器自動添加到 HTTP Request Header 的 Cookie 字段中,服務器通過讀取這些信息,來區分當前請求的用戶身份與狀態。
瀏覽器可以通過讀寫 document.cookie 屬性來添加或刪除 Cookie 信息,服務器端可以通過 HTTP Response Header(響應頭)中的 Set-Cookie 來改寫客戶端的 Cookie 信息。每一條 Cookie 屬性通常都會設置一個過期時間,過期之后的 Cookie 瀏覽器將會自動清理它們,不會再被攜帶在 HTTP Request Header(請求頭)中。
例如,以下 PHP 語句可以通過設置 Cookie 過期時間為前一個小時來觸發客戶端 Cookie 過期,達到刪除 Cookie 的目的:
setcookie('key', '', time() - 3600, '/');
由于 Cookie 通常用于記錄用戶「帳號信息」和用戶的「操作記錄」,所以泄露 Cookie 會帶來個人帳號與隱私泄露的風險。這也是為什么你在百度上搜索「貸款」的關鍵詞之后,訪問其他網站時就能看到相關的推薦廣告,甚至第二天就會有各種放貸電話找上門來。
又由于 Cookie 可以隨意被客戶端修改(通過修改 document.cookie 屬性),因此瀏覽器廠商們一起制定了HttpOnly 的 Cookie 機制。服務器端在 setcookie 時,通過設置 HttpOnly 的標識,可以防止客戶端通過 JavaScript 修改 Cookie 的信息。不過這種方法對于基于 HTTP 協議進行篡改的方法來說無法防范,在之后的貓哥網絡編程系列中,我將會介紹如何通過監控 Wi-Fi 流量來截取、偽造用戶身份。
在 YSlow 性能優化最佳實踐中,有兩條與 Cookie 相關的建議:
雖然瀏覽器對 Cookie 的大小與數量有著較為嚴格的限制,但很多網站(尤其是包含登錄態的)的 Cookie 信息量通常比其他所有 HTTP Header 加起來的還要多。為了減少不必要的 HTTP 數據傳輸量,YSlow 給出了以上兩條優化建議。由于網頁的靜態資源(圖片、CSS、JS)文件無需記錄用戶狀態,因此通常會使用一個額外的域名(Cookie 是按域名來分類存儲)來存放靜態資源文件。
我們使用 Chrome 開發者工具查看「貓哥學前班」新浪微博主頁,可以看到新浪微博使用了 img.t.sinajs.cn的域名來存放它的 CSS 文件,這個域名發起的 HTTP Request Header 中沒有自動帶上 Cookie 字段信息 (因為前后端腳本都沒有在這個域名上設置 Cookie,而是設置在了 weibo.com 域名上):
這里還需要引申一個知識點:Session,它和 Cookie 有什么關系?由于 Session 與本文所講的 HTTP 協議關系不大,相關知識點請自行百度。
Cache-Control:瀏覽器資源緩存標識
網站性能優化中,最為關鍵的是緩存機制(又是沒有之一)。在服務器端通常會使用 Memcached、Redis 等服務來緩存經常訪問的數據。例如在一個電商網站中,用戶經常訪問的熱賣商品數據會被緩存在內存中,用戶在一定時間內訪問商品詳情頁時,后臺程序直接從緩存服務中獲取這段數據,這種方法可以大幅降低數據庫的訪問壓力。
在用戶端,瀏覽器會有一系列機制通過緩存來提升頁面加載速度。例如 IE/Chrome 都會緩存 GET 類型的 AJAX 請求,IE 甚至會緩存 POST 類型的請求,需要通過增加時間戳參數的方式來強制清除緩存。對于所有的靜態資源文件來說,最佳實踐是為它們增加一個 「Never Expires」(永不過期)的強(長)緩存,以下是一個強緩存靜態資源服務器的 Nginx 配置示例:
server { listen 80; server_name yekai.net; root /var/www/yekai.net; location / { index index.html index.htm; } location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|js|css)$ { expires 365d; } }
通過配置 「expires 365d」,HTTP Response Header(響應頭)中會返回 「Cache-Control: max-age=31536000」 的頭字段,配合 Last-Modified 頭字段。瀏覽器便可以自動完成資源的強緩存。
Cache-Control 是瀏覽器緩存機制中最為重要的一個配置,以下是瀏覽器加載靜態資源文件時的緩存檢查機制流程:
由此可見,靜態資源緩存優化的最佳狀態是:直接從本地緩存中讀取 > 304 狀態 > 200 狀態。關于 HTTP 狀態碼,與網站性能優化有關的主要是以下幾個。
- 盡量減少 200 狀態碼的請求。200 表示是一個正常的請求返回,此條優化規則要求盡可能多的減少頁面的 HTTP Request 數量。常見的方法有:合并打包靜態資源、使用 CSS Sprite 雪碧圖合并、緩存 AJAX、使用 LocalStorage/UserData/Manifest 等本地緩存技術。
- 清理返回 301/302 狀態碼的入口鏈接。301 表示永久重定向,302 表示臨時重定向。服務器端使用重定向返回通常是為了兼容一個舊的入口鏈接。我們能做的優化是,將調用舊入口的場景進行清理,直接調用重定向之后的新 URL 地址。
- 304 表示靜態資源未更新,瀏覽器可直接使用本地緩存文件。通常 304 的產生與瀏覽器的處理機制以及服務器緩存頭配置有一定的關系。304 雖然未傳輸文件主體內容,但 HTTP 請求的建立依然是一個可以避免的性能損耗。騰訊 KM(內部知識分享平臺)上有一篇文章通過在真實海量業務場景(沒記錯的話是 Qzone 業務)中,正交驗證 HTTP 1.0 與 1.1 協議中與緩存相關的 HTTP Header 配置,結合日志分析得出了一個最佳實踐:關閉 Etag 配置,只啟用 Cache-Control 與 Last-Modified 響應頭。為了兼容老瀏覽器,可保留 Expires。因為 Etag 的緩存方案,在經過 CDN 及網關代理服務器后,會導致緩存命中率下降。從以上「瀏覽器緩存檢查機制流程」圖上可以看出,使用強緩存(Cache-Control max-age 設置為一年)后瀏覽器在資源過期前不會發起 HTTP 請求,那如何保證靜態資源在服務器上更新后本地的緩存也能同步更新呢?可參考百度 FIS 的「文件指紋」方案。
- 清理返回 404 狀態碼的入口鏈接。靜態資源文件的 404 調用需嚴格避免,而入口頁面的 404 則在所難免。通過在全站 404 頁面進行產品引導與體驗優化,并結合數據上報記錄來源頁(HTTP Referer Header 或
document.referrer
),可以找到并清理 404 來源入口。對于由搜索引擎進入的來源,可通過主動提交新索引至搜索引擎,或使用 301/302 重定向的方式,有效利用起這些「被浪費的流量」。 - 502 服務器出錯。如果是 Nginx + FastCGI 的常見架構,通常是由于 Nginx 緩沖區溢出或服務器資源被耗盡引起,針對不同的業務場景進行 Nginx 的配置優化能顯著提升服務器抗壓性能。
如果你對上文提及的「網絡性能優化」的知識點十分感興趣,建議你通讀 Steve Souders 的《高性能網站建設指南》與《高性能網站建設進階指南》,Steve Souders 的個人網站上積累了很多性能優化的方法與案例。
如果你能看到這里,相信你已經知道如何解答前文提到的幾道 BAT 網絡編程面試題中,關于 「HTTP 協議、狀態碼、緩存與性能優化」相關的問題。
在下一章節中,我們會繼續 HTTP 協議的話題,并詳細講解 HTTP POST 相關的網絡編程細節與調試技巧,相信看完之后你將能輕松定位并解決所有接口聯調的問題與 Bug:)
文章列表