WebSocket 教程

概述

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))。

ajax-long-polling.png

因此同辣,工程師們一直在思考拷姿,有沒(méi)有更好的方法惭载。WebSocket 就是這樣發(fā)明的。WebSocket 連接允許客戶端和服務(wù)器之間進(jìn)行全雙工通信响巢,以便任一方都可以通過(guò)建立的連接將數(shù)據(jù)推送到另一端描滔。WebSocket 只需要建立一次連接,就可以一直保持連接狀態(tài)踪古。這相比于輪詢方式的不停建立連接顯然效率要大大提高含长。

websockets-flow.png

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ò)展 TextWebSocketHandlerBinaryWebSocketHandler 柬甥,你可以覆寫(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è)中心臂外。

  1. 實(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();
    }

}
  1. 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 代碼:

資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末俱诸,一起剝皮案震驚了整個(gè)濱河市菠劝,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌睁搭,老刑警劉巖赶诊,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異园骆,居然都是意外死亡舔痪,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén)锌唾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)锄码,“玉大人夺英,你說(shuō)我怎么就攤上這事∽檀罚” “怎么了痛悯?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)重窟。 經(jīng)常有香客問(wèn)我载萌,道長(zhǎng),這世上最難降的妖魔是什么巡扇? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任扭仁,我火速辦了婚禮,結(jié)果婚禮上霎迫,老公的妹妹穿的比我還像新娘斋枢。我一直安慰自己,他們只是感情好知给,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布瓤帚。 她就那樣靜靜地躺著,像睡著了一般涩赢。 火紅的嫁衣襯著肌膚如雪戈次。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天筒扒,我揣著相機(jī)與錄音怯邪,去河邊找鬼。 笑死花墩,一個(gè)胖子當(dāng)著我的面吹牛悬秉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播冰蘑,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼和泌,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了祠肥?” 一聲冷哼從身側(cè)響起武氓,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎仇箱,沒(méi)想到半個(gè)月后县恕,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡剂桥,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年忠烛,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片权逗。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡况木,死狀恐怖垒拢,靈堂內(nèi)的尸體忽然破棺而出旬迹,到底是詐尸還是另有隱情火惊,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布奔垦,位于F島的核電站屹耐,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏椿猎。R本人自食惡果不足惜惶岭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望犯眠。 院中可真熱鬧按灶,春花似錦、人聲如沸筐咧。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)量蕊。三九已至铺罢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間残炮,已是汗流浹背韭赘。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留势就,地道東北人泉瞻。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像苞冯,于是被迫代替她去往敵國(guó)和親袖牙。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容