概述
WebSocket 是什么?
WebSocket 是一種網(wǎng)絡(luò)通信協(xié)議。RFC6455 定義了它的通信標(biāo)準(zhǔn)。
WebSocket 是 HTML5 開(kāi)始提供的一種在單個(gè) TCP 連接上進(jìn)行全雙工通訊的協(xié)議。
為什么需要 WebSocket 著隆?
了解計(jì)算機(jī)網(wǎng)絡(luò)協(xié)議的人,應(yīng)該都知道:HTTP 協(xié)議是一種無(wú)狀態(tài)的甘改、無(wú)連接的旅东、單向的應(yīng)用層協(xié)議。它采用了請(qǐng)求/響應(yīng)模型十艾。通信請(qǐng)求只能由客戶端發(fā)起,服務(wù)端對(duì)請(qǐng)求做出應(yīng)答處理腾节。
這種通信模型有一個(gè)弊端:HTTP 協(xié)議無(wú)法實(shí)現(xiàn)服務(wù)器主動(dòng)向客戶端發(fā)起消息忘嫉。
這種單向請(qǐng)求的特點(diǎn),注定了如果服務(wù)器有連續(xù)的狀態(tài)變化案腺,客戶端要獲知就非常麻煩庆冕。大多數(shù) Web 應(yīng)用程序?qū)⑼ㄟ^(guò)頻繁的異步JavaScript和XML(AJAX)請(qǐng)求實(shí)現(xiàn)長(zhǎng)輪詢。輪詢的效率低劈榨,非常浪費(fèi)資源(因?yàn)楸仨毑煌_B接访递,或者 HTTP 連接始終打開(kāi))。
因此同辣,工程師們一直在思考拷姿,有沒(méi)有更好的方法惭载。WebSocket 就是這樣發(fā)明的。WebSocket 連接允許客戶端和服務(wù)器之間進(jìn)行全雙工通信响巢,以便任一方都可以通過(guò)建立的連接將數(shù)據(jù)推送到另一端描滔。WebSocket 只需要建立一次連接,就可以一直保持連接狀態(tài)踪古。這相比于輪詢方式的不停建立連接顯然效率要大大提高含长。
WebSocket 如何工作?
Web瀏覽器和服務(wù)器都必須實(shí)現(xiàn) WebSockets 協(xié)議來(lái)建立和維護(hù)連接伏穆。由于 WebSockets 連接長(zhǎng)期存在拘泞,與典型的HTTP連接不同,對(duì)服務(wù)器有重要的影響枕扫。
基于多線程或多進(jìn)程的服務(wù)器無(wú)法適用于 WebSockets田弥,因?yàn)樗荚诖蜷_(kāi)連接,盡可能快地處理請(qǐng)求铡原,然后關(guān)閉連接偷厦。任何實(shí)際的 WebSockets 服務(wù)器端實(shí)現(xiàn)都需要一個(gè)異步服務(wù)器。
WebSocket 客戶端
在客戶端燕刻,沒(méi)有必要為 WebSockets 使用 JavaScript 庫(kù)只泼。實(shí)現(xiàn) WebSockets 的 Web 瀏覽器將通過(guò) WebSockets 對(duì)象公開(kāi)所有必需的客戶端功能(主要指支持 Html5 的瀏覽器)。
客戶端 API
以下 API 用于創(chuàng)建 WebSocket 對(duì)象卵洗。
var Socket = new WebSocket(url, [protocol] );
以上代碼中的第一個(gè)參數(shù) url, 指定連接的 URL请唱。第二個(gè)參數(shù) protocol 是可選的,指定了可接受的子協(xié)議过蹂。
WebSocket 屬性
以下是 WebSocket 對(duì)象的屬性十绑。假定我們使用了以上代碼創(chuàng)建了 Socket 對(duì)象:
屬性 | 描述 |
---|---|
Socket.readyState | 只讀屬性 readyState 表示連接狀態(tài),可以是以下值:0 - 表示連接尚未建立酷勺。1 - 表示連接已建立本橙,可以進(jìn)行通信。2 - 表示連接正在進(jìn)行關(guān)閉脆诉。3 - 表示連接已經(jīng)關(guān)閉或者連接不能打開(kāi)甚亭。 |
Socket.bufferedAmount | 只讀屬性 bufferedAmount 已被 send() 放入正在隊(duì)列中等待傳輸,但是還沒(méi)有發(fā)出的 UTF-8 文本字節(jié)數(shù)击胜。 |
WebSocket 事件
以下是 WebSocket 對(duì)象的相關(guān)事件亏狰。假定我們使用了以上代碼創(chuàng)建了 Socket 對(duì)象:
事件 | 事件處理程序 | 描述 |
---|---|---|
open | Socket.onopen | 連接建立時(shí)觸發(fā) |
message | Socket.onmessage | 客戶端接收服務(wù)端數(shù)據(jù)時(shí)觸發(fā) |
error | Socket.onerror | 通信發(fā)生錯(cuò)誤時(shí)觸發(fā) |
close | Socket.onclose | 連接關(guān)閉時(shí)觸發(fā) |
WebSocket 方法
以下是 WebSocket 對(duì)象的相關(guān)方法。假定我們使用了以上代碼創(chuàng)建了 Socket 對(duì)象:
方法 | 描述 |
---|---|
Socket.send() | 使用連接發(fā)送數(shù)據(jù) |
Socket.close() | 關(guān)閉連接 |
示例
// 初始化一個(gè) WebSocket 對(duì)象
var ws = new WebSocket("ws://localhost:9998/echo");
// 建立 web socket 連接成功觸發(fā)事件
ws.onopen = function () {
// 使用 send() 方法發(fā)送數(shù)據(jù)
ws.send("發(fā)送數(shù)據(jù)");
alert("數(shù)據(jù)發(fā)送中...");
};
// 接收服務(wù)端數(shù)據(jù)時(shí)觸發(fā)事件
ws.onmessage = function (evt) {
var received_msg = evt.data;
alert("數(shù)據(jù)已接收...");
};
// 斷開(kāi) web socket 連接成功觸發(fā)事件
ws.onclose = function () {
alert("連接已關(guān)閉...");
};
WebSocket 服務(wù)端
WebSocket 在服務(wù)端的實(shí)現(xiàn)非常豐富偶摔。Node.js暇唾、Java、C++、Python 等多種語(yǔ)言都有自己的解決方案策州。
以下瘸味,介紹我在學(xué)習(xí) WebSocket 過(guò)程中接觸過(guò)的 WebSocket 服務(wù)端解決方案。
Node.js
常用的 Node 實(shí)現(xiàn)有以下三種抽活。
Java
Java 的 web 一般都依托于 servlet 容器硫戈。
我使用過(guò)的 servlet 容器有:Tomcat、Jetty下硕、Resin丁逝。其中Tomcat7、Jetty7及以上版本均開(kāi)始支持 WebSocket(推薦較新的版本梭姓,因?yàn)殡S著版本的更迭霜幼,對(duì) WebSocket 的支持可能有變更)。
此外誉尖,Spring 框架對(duì) WebSocket 也提供了支持罪既。
雖然,以上應(yīng)用對(duì)于 WebSocket 都有各自的實(shí)現(xiàn)铡恕。但是琢感,它們都遵循RFC6455 的通信標(biāo)準(zhǔn),并且 Java API 統(tǒng)一遵循 JSR 356 - JavaTM API for WebSocket 規(guī)范探熔。所以驹针,在實(shí)際編碼中,API 差異不大诀艰。
Spring
Spring 對(duì)于 WebSocket 的支持基于下面的 jar 包:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${spring.version}</version>
</dependency>
在 Spring 實(shí)現(xiàn) WebSocket 服務(wù)器大概分為以下幾步:
創(chuàng)建 WebSocket 處理器
擴(kuò)展 TextWebSocketHandler
或 BinaryWebSocketHandler
柬甥,你可以覆寫(xiě)指定的方法。Spring 在收到 WebSocket 事件時(shí)其垄,會(huì)自動(dòng)調(diào)用事件對(duì)應(yīng)的方法苛蒲。
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ā)的回調(diào)
*/
void afterConnectionEstablished(WebSocketSession session) throws Exception;
/**
* 收到消息時(shí)觸發(fā)的回調(diào)
*/
void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception;
/**
* 傳輸消息出錯(cuò)時(shí)觸發(fā)的回調(diào)
*/
void handleTransportError(WebSocketSession session, Throwable exception) throws Exception;
/**
* 斷開(kāi)連接后觸發(fā)的回調(diào)
*/
void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception;
/**
* 是否處理分片消息
*/
boolean supportsPartialMessages();
}
配置 WebSocket
配置有兩種方式:注解和 xml 绿满。其作用就是將 WebSocket 處理器添加到注冊(cè)中心臂外。
- 實(shí)現(xiàn)
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>
更多配置細(xì)節(jié)可以參考:Spring WebSocket 文檔
javax.websocket
如果不想使用 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无牵,你還需要引入它的實(shí)現(xiàn)包:
<!-- 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
這個(gè)注解用來(lái)標(biāo)記一個(gè)類(lèi)是 WebSocket 的處理器。
然后厂抖,你可以在這個(gè)類(lèi)中使用下面的注解來(lái)表明所修飾的方法是觸發(fā)事件的回調(diào)
// 收到消息觸發(fā)事件
@OnMessage
public void onMessage(String message, Session session) throws IOException, InterruptedException {
...
}
// 打開(kāi)連接觸發(fā)事件
@OnOpen
public void onOpen(Session session, EndpointConfig config, @PathParam("id") String id) {
...
}
// 關(guān)閉連接觸發(fā)事件
@OnClose
public void onClose(Session session, CloseReason closeReason) {
...
}
// 傳輸消息錯(cuò)誤觸發(fā)事件
@OnError
public void onError(Throwable error) {
...
}
ServerEndpointConfig.Configurator
編寫(xiě)完處理器茎毁,你需要擴(kuò)展 ServerEndpointConfig.Configurator 類(lèi)完成配置:
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);
}
}
然后就沒(méi)有然后了,就是這么簡(jiǎn)單。
WebSocket 代理
如果把 WebSocket 的通信看成是電話連接七蜘,Nginx 的角色則像是電話接線員谭溉,負(fù)責(zé)將發(fā)起電話連接的電話轉(zhuǎn)接到指定的客服。
Nginx 從 1.3 版開(kāi)始正式支持 WebSocket 代理橡卤。如果你的 web 應(yīng)用使用了代理服務(wù)器 Nginx扮念,那么你還需要為 Nginx 做一些配置,使得它開(kāi)啟 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;
}
}
更多配置細(xì)節(jié)可以參考:Nginx 官方的 websocket 文檔
FAQ
HTTP 和 WebSocket 有什么關(guān)系柜与?
Websocket 其實(shí)是一個(gè)新協(xié)議,跟 HTTP 協(xié)議基本沒(méi)有關(guān)系嵌灰,只是為了兼容現(xiàn)有瀏覽器的握手規(guī)范而已弄匕,也就是說(shuō)它是 HTTP 協(xié)議上的一種補(bǔ)充。
Html 和 HTTP 有什么關(guān)系沽瞭?
Html 是超文本標(biāo)記語(yǔ)言迁匠,是一種用于創(chuàng)建網(wǎng)頁(yè)的標(biāo)準(zhǔn)標(biāo)記語(yǔ)言。它是一種技術(shù)標(biāo)準(zhǔn)驹溃。Html5 是它的最新版本城丧。
Http 是一種網(wǎng)絡(luò)通信協(xié)議。其本身和 Html 沒(méi)有直接關(guān)系豌鹤。
完整示例
如果需要完整示例代碼亡哄,可以參考我的 Github 代碼:
-
嵌入式 Jetty 服務(wù)器的 WebSocket 示例
spring-websocket 和 jetty 9.3 版本似乎存在兼容性問(wèn)題,Tomcat則木有問(wèn)題傍药。
我嘗試了好幾次磺平,沒(méi)有找到解決方案,只好使用 Jetty 官方的嵌入式示例在 Jetty 中使用 WebSocket 拐辽。
資料
-
知乎高票答案——WebSocket是什么原理 by Ovear
對(duì) WebSocket 原理的闡述簡(jiǎn)單易懂拣挪。
-
WebSocket 教程 by ruanyf
阮一峰大神的科普一如既往的淺顯易懂。
WebSockets by fullstackpython