深入學習WebSockets概念和實踐

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"WebSocket 協議爲 Internet 通信創造了新的可能性,併爲真正的實時通訊打開了大門。本文將介紹 WebSockets 的發展史,並深入學習 WebSockets 是如何產生的、它們是什麼、如何工作,以及 WebSockets 如何在實際應用程序中工作的。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"WebSockets:背景","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"WebSockets 於 2008 年首次被提出,自 2010 年左右開始獲得瀏覽器廠商的廣泛支持。在 WebSockets 出現之前,“實時”通訊已經存在,但它很難實現,通訊速度比較慢,而且是通過入侵現有的web技術來實現的,而這些技術並不是爲實時應用而設計的。這是因爲web是建立在HTTP協議的基礎上的,而HTTP協議最初完全是作爲一種請求-響應機制設計的。打開一個連接,描述想要的,返回一個響應,然後關閉連接。這在 Web 的早期是很好的,因爲在那時的場景中,只需要處理一個文本文檔和一些額外的資源即可(通常是圖像)。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"使用 JavaScript 編寫 Web 腳本","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1995 年,","attrs":{}},{"type":"link","attrs":{"href":"https://en.wikipedia.org/wiki/Netscape","title":"","type":null},"content":[{"type":"text","text":"Netscape Communications","attrs":{}}]},{"type":"text","text":" 聘請了 Brendan Eich,目標是將腳本功能嵌入其 Netscape Navigator 瀏覽器中,於是 JavaScript 誕生了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最初,JavaScript 設計的有點奇怪,不能做很多事情(尤其是在 JavaScript 可以使用的瀏覽器 DOM 極其有限的情況下),但它對一些事情很有用,例如在表單提交之前對輸入字段進行簡單的驗證後再將表單數據發送到服務器。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"微軟很快就憑藉 Internet Explorer 進入了瀏覽器領域,這是早期瀏覽器戰爭真正開始的地方。兩家公司都在爭奪最好的瀏覽器,因此不可避免地會定期向 Netscape 和 Internet Explorer 添加特性和功能。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/99/99c9581c6e6e63716c9df593ff9297bf.jpeg","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"XMLHttpRequest 和 AJAX 的誕生","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當時引入的兩個最重要的功能是將 Java applet 嵌入到頁面中的能力,以及微軟提供的ActiveX控件。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這些本質上是預編譯的組件,可以選擇在網頁中呈現自己的嵌入式用戶界面。更重要的是,除了當時 JavaScript 微薄的腳本功能套件之外,它們還提供了大量其他可能性。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然通過 Java 提供了一些類似的網絡功能,但最重要的後臺通信功能首次出現在 1999 年,使用Microsoft XMLHTTP ActiveXObject接口。它在 Internet Explorer 5.0 中原生可用,無需安裝插件,可以用一行 JavaScript 實例化,並且在處理 Java applet 時不會帶來任何的不兼容。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"XMLHTTP對象使向服務器發出請求並接收響應成爲可能——所有這些都無需重新加載頁面或以其他方式中斷用戶體驗。然後JavaScript代碼可以解析響應並在不刷新頁面的情況下修改頁面,從而將大量豐富的體驗集成到網站中。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"常見的早期用例包括允許下拉框填充基於用戶先前輸入的選項,以及在填寫用戶註冊表單時對用戶名可用性的“即時”驗證。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面是實例化 XMLHTTP 對象的示例 JavaScript 代碼:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"var xmlhttp = new ActiveXObject(\"Microsoft.XMLHTTP\");\nxmlhttp.open(\"GET\", \"/api/data\", true);\nxmlhttp.onreadystatechange = function () {\n if (xmlhttp.readyState === 4) {\n console.log(xmlhttp.responseText);\n }\n};\nxmlhttp.send(null);\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於被其他瀏覽器採用,XMLHTTP後來成爲XMLHttpRequest事實上的標準。這也是術語“AJAX”被創造出來的時間,即“異步 JavaScript 和 XML”。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"後來出現了JSON標準並使一切變得更好,但AJAX中的“X”(更不用說 XMLHttpRequest 中的“XML”)從未真正消失,儘管實際的 XML 格式數據已經從標準消息傳遞中基本消失了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"XMLHttpRequest對象現代用法的代碼示例:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"const req = new XMLHttpRequest();\nreq.addEventListener(\"load\", () => console.log(this.responseText));\nreq.open(\"GET\", \"/api/data\");\nreq.send();\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"充滿新可能性的世界","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在通常情況下,XMLHttpRequest 仍然遵循用於檢索原始 HTML 文檔的相同的 HTTP 請求-響應模型。沒有真正的概念允許服務器主動聯繫用戶,或者爲更復雜的用例建立任何類型的通用雙向連接。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然而,JavaScript 一直在獲得新的特性和功能,瀏覽器也在增強文檔對象模型 (DOM)。使得 JavaScript 在豐富用戶與網頁交互的體驗方面具有越來越大的潛力。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/1d/1da154282f81e8f65323dc9c8342b6e2.jpeg","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"隨着充滿活力的體驗的潛能開始變得明顯,開發人員自然而然地傾向於直接在瀏覽器中實現客戶端-服務器應用程序的想法。在此之前,任何重要工作的標準範例都是構建一個專用的軟件應用程序,將其與安裝程序打包,讓用戶下載安裝程序,然後在他們的機器上安裝它。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不用說,這對不懂 IT 技術的用戶來說都是一個相當大的進入障礙,讓應用程序保持更新,並進行修復和增強功能就是一項具有挑戰的事情。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此,很容易理解,能夠構建一個應用程序是多麼誘人,該應用程序既不需要安裝就可以訪問,也不需要用戶培訓和反饋來迭代軟件的實現。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"孵化實時WEB","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"“創新就是把兩個已經存在的東西以一種新的方式組合在一起。——湯姆·弗雷斯頓","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當某些事情在技術上似乎是可行的,並且潛在的回報值得付出努力時,我們通常會竭盡全力將可用的東西拼裝成滿足我們需求的形狀。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此,開發人員使用 XMLHttpRequest 並濫用它來模擬 web 頁面和服務器之間的實時交互通信。實現這一點的技術都變得很普遍——甚至是“標準的”。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這些技術中最常見的可能是長輪詢,這涉及打開到服務器的 XMLHttpRequest 連接,並將其保持打開狀態,直到不再需要進行通信爲止。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在正常情況下,當發出HTTP請求時,服務器的響應將通過發出請求的連接流返回給客戶端。其目的是允許瀏覽器在等待服務器響應的下一部分時開始呈現HTML頁面。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於長輪詢,讓 HTTP 連接保持打開狀態意味着只要連接保持打開狀態,服務器就可以繼續持續響應數據。沒有技術要求數據採用一種或另一種格式,或者在向客戶端發送數據後關閉請求。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這同樣適用於客戶端發送的 HTTP 請求負載,服務器可能會在客戶端的請求數據全部到達之前開始傳遞其響應,並且在客戶端選擇停止發送請求數據之前,並不嚴格要求它停止發送請求數據。這就意味着,就像服務器可以在連接的生命週期內繼續傳遞響應數據一樣的原理,客戶端也可以這樣做,最終的結果就實現了服務器和客戶端之間事實上的雙向通信。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e8/e8c2295f344437f7713d187712475557.jpeg","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在缺乏其它合適工具的情況下,長輪詢對於 web 應用程序開發人員來說是可行的,但是正確地執行長輪詢是很棘手的,並且充滿了必須處理的可能意想不到的複雜情況。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"總而言之,長輪詢實際上只是對可用工具進行重新利用的一種情況,以便完成它們並非真正設計用來做的事情。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"需要一個真正的解決方案 - 可以使開發人員在 Web 環境中具有適當的 TCP/IP 套接字風格能力的東西。此類解決方案需要爲 Web 構建,並且需要解決在 Web 環境中操作時出現的所有問題。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"WebSockets 誕生","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在2008年年中左右,開發人員 Michael Carter 和 Ian Hickson 特別強烈地感受到使用 Comet 實現任何真正健壯的東西所帶來的痛苦和侷限性。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過在IRC和W3C郵件列表上的合作,他們制定了一項計劃,爲現代實時、雙向的WEB通信引入一個新標準,於是“WebSocket”這個名字就應運而生了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個設想很快就進入了W3C HTML草案標準,不久之後,Michael Carter 在 Comet 社區發佈一篇介紹 WebSockets的文章。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2010 年,Google Chrome 4 是第一個全面支持 WebSockets 的瀏覽器。在接下來的幾年裏,其他瀏覽器供應商也紛紛效仿。2011 年,","attrs":{}},{"type":"link","attrs":{"href":"https://tools.ietf.org/html/rfc6455","title":"","type":null},"content":[{"type":"text","text":"RFC 6455(WebSocket 協議)","attrs":{}}]},{"type":"text","text":"發佈到 IETF 網站。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"到今天,所有主流瀏覽器都完全支持 WebSockets,包括 Internet Explorer 10 和 11。此外,自2013年以來,iOS和Android上的瀏覽器都支持WebSocket,這意味着WebSocket支持的現代前景是非常健康的。很多“物聯網”或 IoT 也可以在某些版本的Android上運行,所以截至2018年,在其他類型的設備上支持WebSocket也相當普遍。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"什麼是 WebSockets?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"簡而言之,WebSockets 是建立在設備TCP/IP堆棧之上的一個微型的傳輸層。其目的是爲 web 應用程序開發人員提供一個基本的儘可能接近原生的 TCP 通信層,同時添加一些抽象來消除某些在 Web 工作方式方面可能存在的兼容性。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它們還迎合了這樣一個事實,即 Web 具有額外的安全考慮因素,必須考慮這些因素以保護消費者和服務提供商。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"WebSockets 是傳輸還是協議?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可能聽說過 WebSockets 同時被稱爲“傳輸”和“協議”。前者更準確,因爲雖然它們是一種必須遵守建立通信和封裝傳輸數據的嚴格規則集的協議,該標準對實際數據有效載荷在外部消息信封內的結構不採取任何措施。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"事實上,該規範的一部分包含了讓客戶端和服務器就傳輸數據的格式化和解析協議達成一致的選項。該標準將這些稱爲“子協議”,以避免在命名中產生歧義。子協議的例子有 JSON、XML、MQTT、WAMP。這些可以確保不僅在數據的結構方式上達成一致,而且在通信必須開始、繼續和最終終止的方式上達成一致。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"WebSocket 僅提供一個傳輸層,在該層上可以實現消息傳遞過程,這就是爲什麼大多數常見的子協議並不僅限於基於WebSocket的通信。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"WebSocket認證和授權是如何工作的?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"WebSockets 是一個建立在 TCP/IP 之上的薄層,因此除了基本的握手和消息幀規範之外的任何事情實際上都需要在每個應用程序或每個庫的基礎上處理。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"“該協議沒有規定服務器可以在 WebSocket 握手期間對客戶端進行身份驗證的任何特定方式。WebSocket 服務器可以使用通用 HTTP 服務器可用的任何客戶端身份驗證機制,例如 cookie、HTTP 身份驗證或 TLS 身份驗證”。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"簡而言之,使用的基於 HTTP 的身份驗證方法,或者使用子協議,例如MQTT或WAMP,它們都提供了 WebSocket 身份驗證和授權的方法。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"拋棄 HTTP:WebSockets 和重新設計的 TCP 套接字","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在發出 HTTP 請求和接收響應時,實際涉及的雙向網絡通信發生在活動的 TCP/IP 連接上。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"正如我們現在所知,WebSockets 也是建立在 TCP 堆棧之上的,這意味着我們需要的是一種讓客戶端和服務器共同同意保持 TCP 連接打開並將其重新用於持續通信的方法。如果這樣做,那麼就沒有技術上的理由爲什麼他們不能繼續使用套接字傳輸任何類型的任意數據,只要他們都同意應該如何解析發送和接收的二進制數據。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"要開始爲 WebSocket 通信重新利用 TCP 連接的過程,客戶端可以包含一組專門爲此類用例發明的標準 HTTP 請求標頭:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"GET /index.html HTTP/1.1\nHost: www.example.com\nConnection: Upgrade\nUpgrade: websocket\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/5a/5a6df4851113413571b3c7b56d5dec4b.jpeg","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"該 Connection 頭告訴客戶端想協商中所使用的插座的方式發生變化的服務器。隨附的值 Upgrade 表示當前通過 TCP 使用的傳輸協議應該更改。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"既然服務器知道客戶端想要升級當前在活動 TCP 套接字上使用的協議,服務器知道查找相應的 Upgrade 標頭,這將告訴它客戶端想要在剩餘的生命週期中使用哪種傳輸協議通訊。一旦服務器看到標頭 WebSocket 的值 Upgrade ,它就知道 WebSocket 握手過程已經開始。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"在瀏覽器中使用 WebSockets","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"WebSocket API 是在 WHATWG HTML Living Standard 中定義的, 實際上使用起來非常簡單。構建一個 WebSocket 只需要一行代碼:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"const ws = new WebSocket('ws://example.org');\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意 ws 在擁有該 http 通訊的地方使用,還可以選擇 wss 在使用 https 的地方。這些協議是與 WebSocket 規範一起引入的 ,旨在表示 HTTP 連接,其中包括升級連接以使用 WebSockets 的請求。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"創建 WebSocket 對象本身並沒不需要做很多事情,連接是異步建立的,因此需要在發送任何消息之前偵聽握手的完成情況,並且還包括從服務器接收的消息的偵聽器:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"ws.addEventListener(\"open\", () => {\n console.log(\"websocket open\");\n ws.send(\"hello\");\n});\n\nws.addEventListener(\"close\", () => {\n console.log(\"websocket close\");\n});\n\nws.addEventListener(\"error\", () => {\n console.log(\"websocket error\");\n});\n\nws.addEventListener(\"message\", (event) => {\n console.log(\"Received:\", event.data);\n});\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當連接終止時,WebSockets 不會自動恢復,這是需要自己實現的東西,也是存在許多客戶端庫的部分原因。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常爲了保持連接狀態,需要增加心跳機制。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"WebSocket 心跳機制","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"WebSockets 心跳機制是每隔一段時間會向服務器發送一個數據包,告訴服務器自己還活着,同時客戶端會確認服務器端是否還活着,如果還活着的話,就會回傳一個數據包給客戶端來確定服務器端也還活着,否則的話,有可能是網絡斷開連接了。就需要建立重連機制。主要在一些長時間連接的應用場景需要考慮心跳機制及重連機制,以保證長時間的連接及數據交互。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/0f/0f63fc0d3099ecf9b03595ecd4d107de.jpeg","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"開源 WebSocket 庫","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"WebSocket 庫有兩個主要類:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實現協議的 WebSocket 庫,剩下的交給開發人員","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"WebSocket 庫建立在協議之上,具有實時消息傳遞應用程序通常需要的各種附加功能。這可能包括恢復丟失的連接、發佈/訂閱和頻道、身份驗證、授權等。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"後一種類型通常需要在客戶端使用它們自己的庫,而不僅僅是使用瀏覽器提供的原始 WebSocket API。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以下推薦的庫都是開源的。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"WS","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://github.com/websockets/ws","title":"","type":null},"content":[{"type":"text","text":"ws","attrs":{}}]},{"type":"text","text":" 是“使用簡單、速度極快且經過全面測試的 Node.js 的 WebSocket 客戶端和服務器”。它絕對是一個準系統實現,旨在完成實現協議的所有艱苦工作。但是,諸如連接恢復、發佈/訂閱等附加功能是您必須自己管理的問題。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"倉庫地址:","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/websockets/ws","title":"","type":null},"content":[{"type":"text","text":"https://github.com/websockets/ws","attrs":{}}]}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"客戶端","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"const WebSocket = require(\"ws\");\n\nconst ws = new WebSocket(\"ws://www.host.com/path\");\n\nws.on(\"open\", function open() {\n ws.send(\"something\");\n});\n\nws.on(\"message\", function incoming(data) {\n console.log(data);\n});\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"服務端","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"const WebSocket = require(\"ws\");\n\nconst wss = new WebSocket.Server({ port: 8080 });\n\nwss.on(\"connection\", function connection(ws) {\n ws.on(\"message\", function incoming(message) {\n console.log(\"received: %s\", message);\n });\n\n ws.send(\"something\");\n});\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Socket.io","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://github.com/socketio/socket.io","title":"","type":null},"content":[{"type":"text","text":"Socket.IO","attrs":{}}]},{"type":"text","text":" 已經存在一段時間了,可以被認爲是 WebSockets 的“jQuery”。它使用長輪詢和 WebSockets 進行傳輸,默認情況下從長輪詢開始,然後升級到 WebSockets(如果可用)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"倉庫地址:https://github.com/socketio/socket.io","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"鑑於長輪詢的相關性逐漸減弱,如今 Socket.IO 的主要吸引力在於其其他功能,例如恢復斷開的連接、自動支持 JSON 和“命名空間”,它們本質上是多路複用在同一客戶端連接上的隔離消息通道。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Socket.IO 實際上不能與通用的 WebSockets 解決方案互換——無論是在服務器端還是在客戶端——並且嘗試連接到 Socket.io 客戶端或服務器以外的其他東西將會失敗。它有自己的附加握手協議,以及每條消息中包含的一些附加元數據。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"應該使用 Socket.IO 嗎?","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從好的方面來說,除了啓動和運行的簡單之外,它還很完善,如果遇到使用上的問題,擁有大量的學習資料。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"像 jQuery 一樣,Socket.IO 很大程度上是過去時代的產物,對於新項目,最好使用更現代的東西。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"客戶端","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"const io = require(\"socket.io-client\");\nconst socket = io();\nsocket.emit(\"chat message\", \"Hello there\");\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"服務端","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"const app = require(\"express\")();\nconst http = require(\"http\").Server(app);\nconst io = require(\"socket.io\")(http);\n\napp.get(\"/\", function (req, res) {\n res.sendFile(__dirname + \"/index.html\");\n});\n\nio.on(\"connection\", function (socket) {\n console.log(\"a user connected\");\n});\n\nhttp.listen(3000, function () {\n console.log(\"listening on *:3000\");\n});\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"μWebSockets","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"μWebSockets 是 ws 的直接替代品,實施時特別注重性能和穩定性。據我所知,μWebSockets 是最快的 WebSocket 服務器實現。它實際上是由 SocketCluster 在幕後使用的,我將在下面討論。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"倉庫地址:","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/uNetworking/uWebSockets","title":"","type":null},"content":[{"type":"text","text":"https://github.com/uNetworking/uWebSockets","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"const WebSocketServer = require(\"uws\").Server;\nconst wss = new WebSocketServer({ port: 3000 });\n\nconst onMessage = (message) => {\n console.log(`received: ${message}`);\n};\n\nwss.on(\"connection\", function (ws) {\n ws.on(\"message\", onMessage);\n ws.send(\"something\");\n});\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"faye-websocket","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://github.com/faye/faye-websocket-node","title":"","type":null},"content":[{"type":"text","text":"faye-websocket","attrs":{}}]},{"type":"text","text":" 是客戶端和服務器端符合標準的 WebSocket 實現,起源於 Ruby-on-Rails 社區,是Faye 項目的一部分。根據 Github,項目:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"“它本身不提供服務器,而是可以輕鬆處理現有 Node 應用程序中的 WebSocket 連接。除了標準 WebSocket API 之外,它不提供任何抽象。”","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"倉庫地址:https://github.com/faye/faye-websocket-node","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在下面的服務器示例代碼中,您可以看到處理連接升級和從入站套接字緩衝區轉換消息幀的所有工作都由WebSocket庫提供的類處理。與其他最小的解決方案一樣,這是一個簡單的實現——您需要自己處理特定於應用程序的問題。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"客戶端","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"const WebSocket = require(\"faye-websocket\");\nconst ws = new WebSocket.Client(\"ws://www.example.com/\");\n\nws.on(\"open\", function (event) {\n console.log(\"open\");\n ws.send(\"Hello, world!\");\n});\n\nws.on(\"message\", function (event) {\n console.log(\"message\", event.data);\n});\n\nws.on(\"close\", function (event) {\n console.log(\"close\", event.code, event.reason);\n ws = null;\n});\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"服務端","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"const WebSocket = require(\"faye-websocket\");\nconst http = require(\"http\");\n\nvar server = http.createServer();\n\nserver.on(\"upgrade\", function (request, socket, body) {\n if (WebSocket.isWebSocket(request)) {\n var ws = new WebSocket(request, socket, body);\n\n ws.on(\"message\", function (event) {\n ws.send(event.data);\n });\n\n ws.on(\"close\", function (event) {\n console.log(\"close\", event.code, event.reason);\n ws = null;\n });\n }\n});\n\nserver.listen(8000);\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"SocketCluster","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://socketcluster.io/","title":"","type":null},"content":[{"type":"text","text":"SocketCluster","attrs":{}}]},{"type":"text","text":" 是一個全功能的客戶端-服務器消息框架,完全圍繞 WebSockets 構建,並在底層使用 μWebSockets。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"“SocketCluster 是 Node.js 的開源實時框架。它支持直接的客戶端-服務器通信和通過發佈/訂閱通道進行的組通信。它旨在輕鬆擴展到任意數量的進程/主機,非常適合構建聊天系統。請參閱用於聊天的 SocketCluster 設計模式”。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"與 Socket.IO 等更簡單的解決方案相比,SocketCluster 需要稍多的安裝,但通常很容易啓動和運行。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"客戶端","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"const socket = socketCluster.create();\nsocket.emit(\"sampleClientEvent\", {\n message: \"This is an object with a message property\",\n});\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"服務端","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"const SocketCluster = require(\"socketcluster\");\nconst socketCluster = new SocketCluster({\n port: 8000, // 服務端口\n appName: \"devpoint\",\n wsEngine: \"ws\",\n workerController: __dirname + \"/worker.js\",\n brokerController: __dirname + \"/broker.js\",\n\n // 是否在worker崩潰時重新啓動(默認值爲true)\n rebootWorkerOnCrash: true,\n});\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"總結","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"服務器可以處理的併發連接數少將成爲服務器負載的瓶頸。大多數性能還可以的 WebSocket 服務器可以支持數千個併發連接。","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章