WebSocket 是一種網絡通信協(xié)議,很多高級功能都需要它。RFC6455 定義了它的通信標準。
WebSocket 是 HTML5 開始提供的一種在單個 TCP 連接上進行全雙工通訊的協(xié)議。
了解計算機網絡協(xié)議的人,應該都知道:HTTP 協(xié)議是一種無狀態(tài)的、無連接的、單向的應用層協(xié)議。它采用了請求/響應模型。通信請求只能由客戶端發(fā)起,服務端對請求做出應答處理。
初次接觸 WebSocket 的人,都會問同樣的問題:我們已經有了 HTTP 協(xié)議,為什么還需要另一個協(xié)議?它能帶來什么好處?
答案很簡單,因為 HTTP 協(xié)議有一個缺陷:通信只能由客戶端發(fā)起。
舉例來說,我們想了解今天的天氣,只能是客戶端向服務器發(fā)出請求,服務器返回查詢結果。HTTP 協(xié)議做不到服務器主動向客戶端推送信息。
這種通信模型有一個弊端:HTTP 協(xié)議無法實現服務器主動向客戶端發(fā)起消息。
這種單向請求的特點,注定了如果服務器有連續(xù)的狀態(tài)變化,客戶端要獲知就非常麻煩。大多數 Web 應用程序將通過頻繁的異步JavaScript和XML(AJAX)請求實現長輪詢。輪詢的效率低,非常浪費資源(因為必須不停連接,或者 HTTP 連接始終打開)。
因此,工程師們一直在思考,有沒有更好的方法。WebSocket 就是這樣發(fā)明的。WebSocket 連接允許客戶端和服務器之間進行全雙工通信,以便任一方都可以通過建立的連接將數據推送到另一端。WebSocket 只需要建立一次連接,就可以一直保持連接狀態(tài)。這相比于輪詢方式的不停建立連接顯然效率要大大提高。
WebSocket 協(xié)議在2008年誕生,2011年成為國際標準。所有瀏覽器都已經支持了。
它的最大特點就是,服務器可以主動向客戶端推送信息,客戶端也可以主動向服務器發(fā)送信息,是真正的雙向平等對話,屬于服務器推送技術的一種。
其他特點包括:
(1)建立在 TCP 協(xié)議之上,服務器端的實現比較容易。
(2)與 HTTP 協(xié)議有著良好的兼容性。默認端口也是80和443,并且握手階段采用 HTTP 協(xié)議,因此握手時不容易屏蔽,能通過各種 HTTP 代理服務器。
(3)數據格式比較輕量,性能開銷小,通信高效。
(4)可以發(fā)送文本,也可以發(fā)送二進制數據。
(5)沒有同源限制,客戶端可以與任意服務器通信。
(6)協(xié)議標識符是ws(如果加密,則為wss),服務器網址就是 URL。
ws://example.com:80/some/path
Web瀏覽器和服務器都必須實現 WebSockets 協(xié)議來建立和維護連接。由于 WebSockets 連接長期存在,與典型的HTTP連接不同,對服務器有重要的影響。
基于多線程或多進程的服務器無法適用于 WebSockets,因為它旨在打開連接,盡可能快地處理請求,然后關閉連接。任何實際的 WebSockets 服務器端實現都需要一個異步服務器。
WebSocket 的用法相當簡單。
下面是一個網頁腳本的例子(點擊這里看運行結果),基本上一眼就能明白。
var ws = new WebSocket("wss://echo.websocket.org"); ws.onopen = function(evt) { console.log("Connection open ..."); ws.send("Hello WebSockets!"); }; ws.onmessage = function(evt) { console.log( "Received Message: " + evt.data); ws.close(); }; ws.onclose = function(evt) { console.log("Connection closed."); };
在客戶端,沒有必要為 WebSockets 使用 JavaScript 庫。實現 WebSockets 的 Web 瀏覽器將通過 WebSockets 對象公開所有必需的客戶端功能(主要指支持 Html5 的瀏覽器)。
以下 API 用于創(chuàng)建 WebSocket 對象。
var Socket = new WebSocket(url, [protocol] );
以上代碼中的第一個參數 url, 指定連接的 URL。第二個參數 protocol 是可選的,指定了可接受的子協(xié)議。
以下是 WebSocket 對象的屬性。假定我們使用了以上代碼創(chuàng)建了 Socket 對象:
屬性 | 描述 |
---|---|
Socket.readyState | 只讀屬性 readyState 表示連接狀態(tài),可以是以下值:0 - 表示連接尚未建立。1 - 表示連接已建立,可以進行通信。2 - 表示連接正在進行關閉。3 - 表示連接已經關閉或者連接不能打開。 |
Socket.bufferedAmount | 只讀屬性 bufferedAmount 已被 send() 放入正在隊列中等待傳輸,但是還沒有發(fā)出的 UTF-8 文本字節(jié)數。 |
以下是 WebSocket 對象的相關事件。假定我們使用了以上代碼創(chuàng)建了 Socket 對象:
事件 | 事件處理程序 | 描述 |
---|---|---|
open | Socket.onopen | 連接建立時觸發(fā) |
message | Socket.onmessage | 客戶端接收服務端數據時觸發(fā) |
error | Socket.onerror | 通信發(fā)生錯誤時觸發(fā) |
close | Socket.onclose | 連接關閉時觸發(fā) |
以下是 WebSocket 對象的相關方法。假定我們使用了以上代碼創(chuàng)建了 Socket 對象:
方法 | 描述 |
---|---|
Socket.send() | 使用連接發(fā)送數據 |
Socket.close() | 關閉連接 |
示例
// 初始化一個 WebSocket 對象 var ws = new WebSocket("ws://localhost:9998/echo"); // 建立 web socket 連接成功觸發(fā)事件 ws.onopen = function () { // 使用 send() 方法發(fā)送數據 ws.send("發(fā)送數據"); alert("數據發(fā)送中..."); }; // 接收服務端數據時觸發(fā)事件 ws.onmessage = function (evt) { var received_msg = evt.data; alert("數據已接收..."); }; // 斷開 web socket 連接成功觸發(fā)事件 ws.onclose = function () { alert("連接已關閉..."); };
WebSocket 在服務端的實現非常豐富。Node.js、Java、C++、Python 等多種語言都有自己的解決方案。
以下,介紹我在學習 WebSocket 過程中接觸過的 WebSocket 服務端解決方案。
常用的 Node 實現有以下三種。
Java 的 web 一般都依托于 servlet 容器。
我使用過的 servlet 容器有:Tomcat、Jetty、Resin。其中Tomcat7、Jetty7及以上版本均開始支持 WebSocket(推薦較新的版本,因為隨著版本的更迭,對 WebSocket 的支持可能有變更)。
此外,Spring 框架對 WebSocket 也提供了支持。
雖然,以上應用對于 WebSocket 都有各自的實現。但是,它們都遵循RFC6455 的通信標準,并且 Java API 統(tǒng)一遵循 JSR 356 - JavaTM API for WebSocket 規(guī)范。所以,在實際編碼中,API 差異不大。
Spring 對于 WebSocket 的支持基于下面的 jar 包:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-websocket</artifactId> <version>${spring.version}</version></dependency>
在 Spring 實現 WebSocket 服務器大概分為以下幾步:
創(chuàng)建 WebSocket 處理器
擴展 TextWebSocketHandler
或 BinaryWebSocketHandler
,你可以覆寫指定的方法。Spring 在收到 WebSocket 事件時,會自動調用事件對應的方法。
import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.TextMessage; public class MyHandler extends TextWebSocketHandler { @Override public void handleTextMessage(WebSocketSession session, TextMessage message) { // ... } }
WebSocketHandler
源碼如下,這意味著你的處理器大概可以處理哪些 WebSocket 事件:
public interface WebSocketHandler { /** * 建立連接后觸發(fā)的回調 */ void afterConnectionEstablished(WebSocketSession session) throws Exception; /** * 收到消息時觸發(fā)的回調 */ void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception; /** * 傳輸消息出錯時觸發(fā)的回調 */ void handleTransportError(WebSocketSession session, Throwable exception) throws Exception; /** * 斷開連接后觸發(fā)的回調 */ void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception; /** * 是否處理分片消息 */ boolean supportsPartialMessages(); }
配置 WebSocket
配置有兩種方式:注解和 xml 。其作用就是將 WebSocket 處理器添加到注冊中心。
實現 WebSocketConfigurer
import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myHandler(), "/myHandler"); } @Bean public WebSocketHandler myHandler() { return new MyHandler(); } }
xml 方式
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd"> <websocket:handlers> <websocket:mapping path="/myHandler" handler="myHandler"/> </websocket:handlers> <bean id="myHandler" class="org.springframework.samples.MyHandler"/></beans>
如果不想使用 Spring 框架的 WebSocket API,你也可以選擇基本的 javax.websocket。
首先,需要引入 API jar 包。
<!-- To write basic javax.websocket against --><dependency> <groupId>javax.websocket</groupId> <artifactId>javax.websocket-api</artifactId> <version>1.0</version></dependency>
如果使用嵌入式 jetty,你還需要引入它的實現包:
<!-- To run javax.websocket in embedded server --><dependency> <groupId>org.eclipse.jetty.websocket</groupId> <artifactId>javax-websocket-server-impl</artifactId> <version>${jetty-version}</version></dependency><!-- To run javax.websocket client --><dependency> <groupId>org.eclipse.jetty.websocket</groupId> <artifactId>javax-websocket-client-impl</artifactId> <version>${jetty-version}</version></dependency>
@ServerEndpoint
這個注解用來標記一個類是 WebSocket 的處理器。
然后,你可以在這個類中使用下面的注解來表明所修飾的方法是觸發(fā)事件的回調
// 收到消息觸發(fā)事件@OnMessagepublic void onMessage(String message, Session session) throws IOException, InterruptedException { ... }// 打開連接觸發(fā)事件@OnOpenpublic void onOpen(Session session, EndpointConfig config, @PathParam("id") String id) { ... }// 關閉連接觸發(fā)事件@OnClosepublic void onClose(Session session, CloseReason closeReason) { ... }// 傳輸消息錯誤觸發(fā)事件@OnErrorpublic void onError(Throwable error) { ... }
ServerEndpointConfig.Configurator
編寫完處理器,你需要擴展 ServerEndpointConfig.Configurator 類完成配置:
public class WebSocketServerConfigurator extends ServerEndpointConfig.Configurator { @Override public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { HttpSession httpSession = (HttpSession) request.getHttpSession(); sec.getUserProperties().put(HttpSession.class.getName(), httpSession); } }
然后就沒有然后了,就是這么簡單。
如果把 WebSocket 的通信看成是電話連接,Nginx 的角色則像是電話接線員,負責將發(fā)起電話連接的電話轉接到指定的客服。
Nginx 從 1.3 版開始正式支持 WebSocket 代理。如果你的 web 應用使用了代理服務器 Nginx,那么你還需要為 Nginx 做一些配置,使得它開啟 WebSocket 代理功能。
以下為參考配置:
server { # this section is specific to the WebSockets proxying location /socket.io { proxy_pass http://app_server_wsgiapp/socket.io; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 600; } }
更多配置細節(jié)可以參考:Nginx 官方的 websocket 文檔
Websocket 其實是一個新協(xié)議,跟 HTTP 協(xié)議基本沒有關系,只是為了兼容現有瀏覽器的握手規(guī)范而已,也就是說它是 HTTP 協(xié)議上的一種補充。
Html 是超文本標記語言,是一種用于創(chuàng)建網頁的標準標記語言。它是一種技術標準。Html5 是它的最新版本。
Http 是一種網絡通信協(xié)議。其本身和 Html 沒有直接關系。
如果需要完整示例代碼,可以參考我的 Github 代碼:
spring-websocket 和 jetty 9.3 版本似乎存在兼容性問題,Tomcat則木有問題。
我嘗試了好幾次,沒有找到解決方案,只好使用 Jetty 官方的嵌入式示例在 Jetty 中使用 WebSocket 。
如對本文有疑問,請?zhí)峤坏浇涣髡搲?,廣大熱心網友會為你解答??! 點擊進入論壇