WebSocket協議入門介紹

2Simple發表於2019-05-30

目錄

WebSocket協議是什麼

WebSocket是應用層協議

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

WebSocket與Http的區別

實際上,WebSocket協議與Http協議有著本質的區別:
1.通訊方式不同
WebSocket是雙向通訊模式,客戶端與伺服器之間只有在握手階段是使用HTTP協議的“請求-響應”模式互動,而一旦連線建立之後的通訊則使用雙向模式互動,不論是客戶端還是服務端都可以隨時將資料傳送給對方;而HTTP協議則至始至終都採用“請求-響應”模式進行通訊。也正因為如此,HTTP協議的通訊效率沒有WebSocket高。
WebSocket與Http協議的互動方式對比

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請求訊息格式:

Request-LineCRLF
general-headerCRLF
request-headerCRLF
entity-headerCRLF
CRLF
[ message-body ]

HTTP響應訊息格式:

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維基百科

為什麼要使用WebSocket

隨著Web應用的發展,特別是動態網頁的普及,越來越多的場景需要實現資料動態重新整理。
在早期的時候,實現資料重新整理的方式通常有如下3種:
1.客戶端定時查詢
客戶端定時查詢(如:每隔10秒鐘查詢一次)是最原始也是最簡單的實現資料重新整理的方法,服務端不用做任何改動,只需要在客戶端新增一個定時器即可。但是這種方式的缺點也很明顯:大量的定時請求都是無效的,因為服務端的資料並沒有更新,相應地也導致了大量的頻寬浪費。

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

3.HTTP Streaming
不論是長輪訓機制還是傳統的客戶端定時查詢方式,都需要客戶端不斷地傳送請求以獲取資料更新,而HTTP Streaming則試圖改變這種方式,其實現機制為:客戶端傳送獲取資料更新請求到服務端時,服務端將保持該請求的響應資料流一直開啟,只要有資料更新就實時地傳送給客戶端。
雖然這個設想是非常美好的,但這帶來了新的問題:
(1)HTTP Streaming的實現機制違背了HTTP協議本身的語義,使得客戶端與服務端不再是“請求-響應”的互動方式,而是直接在二者建立起了一個單向的“通訊管道”。
(2)在HTTP Streaming模式下,服務端只要得到資料更新就傳送給客戶端,那麼就需要客戶端與服務端協商如何區分每一個更新資料包的開始和結尾,否則就可能出現解析資料錯誤的情況。
(3)另外,處於客戶端與服務端的網路中介(如:代理)可能會快取響應資料流,這可能會導致客戶端無法真正獲取到服務端的更新資料,這實際上與HTTP Streaming的本意是相違背的。
鑑於上述原因,在實際應用中HTTP Streaming並沒有真正流行起來,反之使用得最多的是長輪訓機制。

顯然,上述幾種實現資料動態重新整理的方式都是基於HTTP協議實現的,或多或少地存在這樣那樣的問題和缺陷;而WebSocket是一個全新的應用層協議,專門用於Web應用中需要實現動態重新整理的場景。
相比起HTTP協議,WebSocket具備如下特點:

  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

相關文章