WebSocket是基於TCP的應用層協議,用於在C/S架構的應用中實現雙向通訊,關於WebSocket協議的詳細規範和定義參見rfc6455。
須要特別注意的是:雖然WebSocket協議在創建鏈接時會使用HTTP協議,但這並意味着WebSocket協議是基於HTTP協議實現的。
javascript
實際上,WebSocket協議與Http協議有着本質的區別:
1.通訊方式不一樣
WebSocket是雙向通訊模式,客戶端與服務器之間只有在握手階段是使用HTTP協議的「請求-響應」模式交互,而一旦鏈接創建以後的通訊則使用雙向模式交互,不管是客戶端仍是服務端均可以隨時將數據發送給對方;而HTTP協議則至始至終都採用「請求-響應」模式進行通訊。也正由於如此,HTTP協議的通訊效率沒有WebSocket高。
html
2.協議格式不一樣
WebSocket與HTTP的協議格式是徹底不一樣的,具體來說:
(1)HTTP協議(參見:rfc2616)比較臃腫,而WebSocket協議比較輕量。
(2)對於HTTP協議來說,一個數據包就是一條完整的消息;而WebSocket客戶端與服務端通訊的最小單位是幀(frame),由1個或多個幀組成一條完整的消息(message)。即:發送端將消息切割成多個幀,併發送給服務端;服務端接收消息幀,並將關聯的幀從新組裝成完整的消息。
WebSocket協議格式:前端
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+
HTTP請求消息格式:java
Request-LineCRLF general-headerCRLF request-headerCRLF entity-headerCRLF CRLF [ message-body ]
HTTP響應消息格式:node
Status-LineCRLF general-headerCRLF response-headerCRLF entity-headerCRLF CRLF [ message-body ]
雖然WebSocket和HTTP是不一樣應用協議,但rfc6455規定:「WebSocket設計爲經過80和443端口工做,以及支持HTTP代理和中介」,從而使其與HTTP協議兼容。爲了實現兼容性,WebSocket握手時使用HTTP Upgrade頭從HTTP協議更改成WebSocket協議,參考:WebSocket維基百科 。nginx
隨着Web應用的發展,特別是動態網頁的普及,愈來愈多的場景須要實現數據動態刷新。
在早期的時候,實現數據刷新的方式一般有以下3種:
1.客戶端定時查詢
客戶端定時查詢(如:每隔10秒鐘查詢一次)是最原始也是最簡單的實現數據刷新的方法,服務端不用作任何改動,只須要在客戶端添加一個定時器便可。可是這種方式的缺點也很明顯:大量的定時請求都是無效的,由於服務端的數據並無更新,相應地也致使了大量的帶寬浪費。git
2.長輪訓機制
長輪訓機制是對客戶端定時查詢的一種改進,即:客戶端依舊保持定時發送請求給服務端,可是服務端並不當即響應,而是等到真正有數據更新的時候才發送給客戶端。實際上,並非當沒有數據更新時服務端就永遠都不響應客戶端,而是須要在等待一個超時時間以後結束該次長輪訓請求。相對於客戶端定時查詢方式而言,當數據更新頻率不肯定時長輪訓機制可以很明顯地減小請求數。可是,在數據更新比較頻繁的場景下,長輪訓方式的優點就沒那麼明顯了。
在Web開發中使用得最爲廣泛的長輪訓實現方案爲Comet(Comet (web技術)),Tomcat和Jetty都有對應的實現支持,詳見:WhatIsComet,Why Asynchronous Servlets。github
3.HTTP Streaming
不管是長輪訓機制仍是傳統的客戶端定時查詢方式,都須要客戶端不斷地發送請求以獲取數據更新,而HTTP Streaming則試圖改變這種方式,其實現機制爲:客戶端發送獲取數據更新請求到服務端時,服務端將保持該請求的響應數據流一直打開,只要有數據更新就實時地發送給客戶端。
雖然這個設想是很是美好的,但這帶來了新的問題:
(1)HTTP Streaming的實現機制違背了HTTP協議自己的語義,使得客戶端與服務端再也不是「請求-響應」的交互方式,而是直接在兩者創建起了一個單向的「通訊管道」。
(2)在HTTP Streaming模式下,服務端只要獲得數據更新就發送給客戶端,那麼就須要客戶端與服務端協商如何區分每個更新數據包的開始和結尾,不然就可能出現解析數據錯誤的狀況。
(3)另外,處於客戶端與服務端的網絡中介(如:代理)可能會緩存響應數據流,這可能會致使客戶端沒法真正獲取到服務端的更新數據,這實際上與HTTP Streaming的本意是相違背的。
鑑於上述緣由,在實際應用中HTTP Streaming並無真正流行起來,反之使用得最多的是長輪訓機制。web
顯然,上述幾種實現數據動態刷新的方式都是基於HTTP協議實現的,或多或少地存在這樣那樣的問題和缺陷;而WebSocket是一個全新的應用層協議,專門用於Web應用中須要實現動態刷新的場景。
相比起HTTP協議,WebSocket具有以下特色:spring
在Web應用的網頁中使用WebSocket,WebSocket對象提供了用於建立和管理WebSocket鏈接,以及能夠經過該鏈接發送和接收數據的API。
1.構造函數
可使用WebSocket類的構造函數(WebSocket(url[, protocols]))實例化一個對象,如:
var url = "ws://host:port/endpoint"; var ws = new WebSocket(url);
執行上述語句以後,瀏覽器將與服務端創建一個WebSocket鏈接,同時返回一個WebSocket實例對象ws。
2.對象屬性
WebSocket實例對象具有以下屬性:
3.對象方法
WebSocket定義了2個方法:
(1)WebSocket.send(data):向服務器發送數據,將須要經過WebSocket鏈接傳輸至服務器的數據排入隊列,並根據所須要傳輸的數據字節的大小來增長屬性bufferedAmount的值 。若數據沒法傳輸(例如數據須要緩存而緩衝區已滿)時,套接字會自行關閉。
參數data爲傳輸至服務器的數據,它必須是如下類型之一:
(2)WebSocket.close([code[, reason]]):關閉當前鏈接,若是鏈接已經關閉,則此方法不執行任何操做。
參數:
異常:
更多WebSockete API的詳細內容參見W3C的定義:The WebSocket API。
以下爲在網頁中使用原生WebSocket的實現方式。
var url = "ws://localhost:8080/websocket/text"; var ws = new WebSocket(url); ws.onopen = function(event) { console.log("websocket connection open."); console.log(event); }; ws.onmessage = function(event) { console.log("websocket message received.") console.log(event.data); }; ws.onclose = function (event) { console.log("websocket connection close."); console.log(event.code); }; ws.onerror = function(event) { console.log("websocket connection error."); console.log(event); };
在Web網頁中使用WebSocket須要瀏覽器支持,不一樣瀏覽器軟件版本對WebSocket的支持狀況詳見瀏覽器兼容性。
另外,WebSocket客戶端除了能夠在網頁中使用,目前還存在一些獨立的客戶端組件,如:
1.Jetty WebSocket Client API
2.websockets-api-java-spring-client
3.Java-WebSocket
在服務端使用WebSocket須要服務器組件支持,以下以在Tomcat 8.5.41(Tomcat 7以後才支持WebSocket)中使用原生WebSocket爲例。
因爲在服務端使用WebSocket須要使用到WebSocket的API,所以須要添加API依賴管理:
<dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-websocket-api</artifactId> <version>8.5.41</version> </dependency>
使用註解方式編寫WebSocket服務端:
@ServerEndpoint(value="/websocket/text") public class WebSocketTest { private static final Logger logger = LoggerFactory.getLogger(WsChatAnnotation.class); private static final AtomicInteger counter = new AtomicInteger(0); // 客戶端計數器 private static final Set<WsChatAnnotation> connections = new CopyOnWriteArraySet<WsChatAnnotation>(); // 客戶端websocket鏈接集合 private Session session = null; // WebSocket會話對象 private Integer number = 0; // 客戶端編號 public WsChatAnnotation() { number = counter.incrementAndGet(); } /** * 客戶端創建websocket鏈接 * @param session */ @OnOpen public void start(Session session) { logger.info("on open"); this.session = session; connections.add(this); try { session.getBasicRemote().sendText(new StringBuffer().append("Hello: ").append(number).toString()); } catch (IOException e) { e.printStackTrace(); } } /** * 客戶端斷開websocket鏈接 */ @OnClose public void close() { logger.info("session close"); try { this.session.close(); } catch (IOException e) { e.printStackTrace(); } finally { connections.remove(this); } } /** * 接收客戶端發送的消息 * @param message */ @OnMessage public void message(String message) { logger.info("message: {}", message); for(WsChatAnnotation client : connections) { synchronized (client) { try { client.session.getBasicRemote().sendText(message); } catch (IOException e) { e.printStackTrace(); } } } } @OnError public void error(Throwable t) { logger.error("client: {} error", number, t.getMessage()); } }
當下的Web應用架構一般都是集羣化部署,前端使用反向代理或者直接部署負載均衡器,這就要求反向代理或者負載均衡器必須支持WebSocket協議。
目前Nginx,Haporxy都已經支持WebSocket協議。
以下爲在使用nginx做爲反向代理的場景下,配置nginx代理websocket協議。
# add websocket proxy location ~ /ws { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_pass http://8080; }
【參考】
https://spring.io/blog/2012/05/08/spring-mvc-3-2-preview-techniques-for-real-time-updates/ Spring MVC 3.2 Preview: Techniques for Real-time Updates
https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket#%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0 WebSocket
https://www.cnblogs.com/chyingp/p/websocket-deep-in.html WebSocket協議:5分鐘從入門到精通
http://www.ruanyifeng.com/blog/2017/05/websocket.html WebSocket 教程
https://blog.csdn.net/chszs/article/details/26369257 Nginx擔當WebSockets代理
http://blog.fens.me/nodejs-websocket-nginx/ Nginx反向代理Websocket