WebSocket協議入門介紹

目錄

WebSocket協議是什麼

WebSocket是應用層協議

WebSocket是基於TCP的應用層協議,用於在C/S架構的應用中實現雙向通訊,關於WebSocket協議的詳細規範和定義參見rfc6455
須要特別注意的是:雖然WebSocket協議在創建鏈接時會使用HTTP協議,但這並意味着WebSocket協議是基於HTTP協議實現的。
WebSocket屬於應用層協議javascript

WebSocket與Http的區別

實際上,WebSocket協議與Http協議有着本質的區別:
1.通訊方式不一樣
WebSocket是雙向通訊模式,客戶端與服務器之間只有在握手階段是使用HTTP協議的「請求-響應」模式交互,而一旦鏈接創建以後的通訊則使用雙向模式交互,不管是客戶端仍是服務端均可以隨時將數據發送給對方;而HTTP協議則至始至終都採用「請求-響應」模式進行通訊。也正由於如此,HTTP協議的通訊效率沒有WebSocket高。
WebSocket與Http協議的交互方式對比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

爲何要使用WebSocket

隨着Web應用的發展,特別是動態網頁的普及,愈來愈多的場景須要實現數據動態刷新。
在早期的時候,實現數據刷新的方式一般有以下3種:
1.客戶端定時查詢
客戶端定時查詢(如:每隔10秒鐘查詢一次)是最原始也是最簡單的實現數據刷新的方法,服務端不用作任何改動,只須要在客戶端添加一個定時器便可。可是這種方式的缺點也很明顯:大量的定時請求都是無效的,由於服務端的數據並無更新,相應地也致使了大量的帶寬浪費。git

2.長輪訓機制
長輪訓機制是對客戶端定時查詢的一種改進,即:客戶端依舊保持定時發送請求給服務端,可是服務端並不當即響應,而是等到真正有數據更新的時候才發送給客戶端。實際上,並非當沒有數據更新時服務端就永遠都不響應客戶端,而是須要在等待一個超時時間以後結束該次長輪訓請求。相對於客戶端定時查詢方式而言,當數據更新頻率不肯定時長輪訓機制可以很明顯地減小請求數。可是,在數據更新比較頻繁的場景下,長輪訓方式的優點就沒那麼明顯了。
在Web開發中使用得最爲廣泛的長輪訓實現方案爲Comet(Comet (web技術)),Tomcat和Jetty都有對應的實現支持,詳見:WhatIsCometWhy Asynchronous Servletsgithub

3.HTTP Streaming
不管是長輪訓機制仍是傳統的客戶端定時查詢方式,都須要客戶端不斷地發送請求以獲取數據更新,而HTTP Streaming則試圖改變這種方式,其實現機制爲:客戶端發送獲取數據更新請求到服務端時,服務端將保持該請求的響應數據流一直打開,只要有數據更新就實時地發送給客戶端。
雖然這個設想是很是美好的,但這帶來了新的問題:
(1)HTTP Streaming的實現機制違背了HTTP協議自己的語義,使得客戶端與服務端再也不是「請求-響應」的交互方式,而是直接在兩者創建起了一個單向的「通訊管道」。
(2)在HTTP Streaming模式下,服務端只要獲得數據更新就發送給客戶端,那麼就須要客戶端與服務端協商如何區分每個更新數據包的開始和結尾,不然就可能出現解析數據錯誤的狀況。
(3)另外,處於客戶端與服務端的網絡中介(如:代理)可能會緩存響應數據流,這可能會致使客戶端沒法真正獲取到服務端的更新數據,這實際上與HTTP Streaming的本意是相違背的。
鑑於上述緣由,在實際應用中HTTP Streaming並無真正流行起來,反之使用得最多的是長輪訓機制。web

顯然,上述幾種實現數據動態刷新的方式都是基於HTTP協議實現的,或多或少地存在這樣那樣的問題和缺陷;而WebSocket是一個全新的應用層協議,專門用於Web應用中須要實現動態刷新的場景。
相比起HTTP協議,WebSocket具有以下特色:spring

  1. 支持雙向通訊,實時性更強。
  2. 更好的二進制支持。
  3. 較少的控制開銷:鏈接建立後,WebSockete客戶端、服務端進行數據交換時,協議控制的數據包頭部較小。
  4. 支持擴展。

如何使用WebSocket

客戶端API

在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實例對象具有以下屬性:

  • WebSocket.binaryType: 返回websocket鏈接所傳輸二進制數據的類型。
  • WebSocket.bufferedAmount:只讀屬性,用於返回已經被send()方法放入隊列中但尚未被髮送到網絡中的數據的字節數。一旦隊列中的全部數據被髮送至網絡,則該屬性值將被重置爲0。可是,若在發送過程當中鏈接被關閉,則屬性值不會重置爲0。若是你不斷地調用send(),則該屬性值會持續增加。
  • WebSocket.extensions:只讀屬性,返回服務器已選擇的擴展值。目前,連接能夠協定的擴展值只有空字符串或者一個擴展列表。
  • WebSocket.protocol:只讀屬性,用於返回服務器端選中的子協議的名字;這是一個在建立WebSocket對象時,在參數protocols中指定的字符串。
  • WebSocket.readyState:只讀屬性,返回當前WebSocket對象的連接狀態,可能的值爲WebSocket中定義的常量:WebSocket.CONNECTING,WebSocket.OPEN,WebSocket.CLOSING,WebSocket.CLOSED。
  • WebSocket.url:只讀屬性,返回值爲當構造函數建立WebSocket實例對象時URL的絕對路徑。
  • WebSocket.onopen:用於指定鏈接成功後的回調函數,當WebSocket的鏈接狀態readyState變爲「OPEN」時調用;這意味着當前鏈接已經準備好發送和接受數據,這個事件處理程序經過事件(創建鏈接時)觸發。
  • WebSocket.onclose:用於指定鏈接關閉後的回調函數,當WebSocket的鏈接狀態readyState變爲「CLOSED」時被調用,它接收一個名字爲「close」的CloseEvent事件對象。
  • WebSocket.onmessage:用於指定當從服務器接受到信息時的回調函數,當從服務器收到一條消息時,該回調函數將被調用,在函數中接受一命名爲「message」的MessageEvent事件對象。
  • WebSocket.onerror:用於指定鏈接失敗後的回調函數,定義一個發生錯誤時執行的回調函數,此事件的事件名爲"error"。

3.對象方法
WebSocket定義了2個方法:
(1)WebSocket.send(data):向服務器發送數據,將須要經過WebSocket鏈接傳輸至服務器的數據排入隊列,並根據所須要傳輸的數據字節的大小來增長屬性bufferedAmount的值 。若數據沒法傳輸(例如數據須要緩存而緩衝區已滿)時,套接字會自行關閉。
參數data爲傳輸至服務器的數據,它必須是如下類型之一:

  • USVString:文本字符串。字符串將以UTF-8格式添加到緩衝區,而且屬性bufferedAmount將加上該字符串以UTF-8格式編碼時的字節數的值。
  • ArrayBuffer:您可使用一個有類型的數組對象發送底層二進制數據,其二進制數據內存將被緩存於緩衝區,屬性bufferedAmount將加上所需字節數的值。
  • Blob:Blob類型將隊列blob中的原始數據以二進制傳輸,屬性bufferedAmount將加上原始數據的字節數的值。
  • ArrayBufferView:以二進制幀的形式發送任何JavaScript類數組對象,其二進制數據內容將被隊列於緩衝區中,屬性bufferedAmount將加上對應字節數的值。

(2)WebSocket.close([code[, reason]]):關閉當前鏈接,若是鏈接已經關閉,則此方法不執行任何操做。
參數:

  • code:可選,爲一個數字狀態碼,它解釋了鏈接關閉的緣由。若是沒有傳這個參數,默認使用1005。CloseEvent的容許的狀態碼見狀態碼列表
  • reason:可選,一我的類可讀的字符串,它解釋了鏈接關閉的緣由,這個UTF-8編碼的字符串不能超過123個字節。

異常:

  • INVALID_ACCESS_ERR:一個無效的code。
  • SYNTAX_ERR:reason字符串太長(超過123字節)。

更多WebSockete API的詳細內容參見W3C的定義:The WebSocket API

在客戶端使用WebSocket

以下爲在網頁中使用原生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

在服務端使用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());
    }
}

反向代理對WebSocket的支持

當下的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

相關文章
相關標籤/搜索