1. 程式人生 > >實時通訊技術之websocket

實時通訊技術之websocket

本文章即從4個方面帶大家瞭解websocket:

  1. websocket是什麼?
  2. 為什麼需要 WebSocket ?
  3. websocket的優點與缺點?
  4. websocket的相關使用(客戶端與伺服器端)?
  5. websocket的相關協議與規範?

一、websocket是什麼?

      websocket是html5提出的一種基於tcp的協議(可參考rfc6455---websocket協議)。是先通過http/https協議發起一條特殊的http請求進行握手後建立一個用於資料傳輸的tcp通道,此後服務端與客戶端通過此TCP連線進行實時通訊。它跟HTTP協議基本沒有關係,只是為了相容現有瀏覽器的握手規範而已。

二、為什麼需要websocket?

    瞭解計算機網路的人,應該都知道,http是一種無狀態的、無連線的,單向的應用層協議,預設埠為80,並且每次請求都要開啟一條連線,傳送請求--》接收資料--》關閉連線這樣一個過程,請求只能由客戶端發起,並且HTTP request 的header是非常長的,裡面包含的資料可能只是一個很小的值,這樣會佔用很多的頻寬和伺服器資源。

       這樣還有一個弊端:http協議無法實現伺服器端主動向客戶端發起訊息。

       這種單向請求的特點,註定瞭如果伺服器有連續的狀態變化,客戶端要獲知就非常麻煩。以前web server實現推送技術或者即時通訊,用的都是輪詢(polling),而輪詢可以通過ajax輪詢、long poll(長輪詢)

等方式實現,這些技術雖然可達到全雙工通訊,但是輪詢的效率低,非常浪費資源(因為必須不停連線,或者 HTTP 連線始終開啟)。

        websocket就是在這樣的環境下誕生的,它只需要一次握手,就能實現客戶端和伺服器之間進行全雙工通訊,任一方都可以通過建立的連線將資料推送到另一端。

三、websocket的優點與缺點?

        傳統輪詢方式:

  1. ajax輪詢:讓瀏覽器每隔幾秒就傳送一次請求,詢問伺服器是否有資料。
  2. long poll(長輪詢):採用阻塞模型(一直打電話,沒收到就不掛電話),傳送一次請求,如果伺服器端沒有響應,則一直請求,直到有訊息才返回,返回完之後,客戶端再次建立連線。

            這兩種方式雖然能實現雙工通訊,都是非常消耗伺服器資源的。

    websocket的優點:

  1. 只需要一次http握手,即可實現客戶端和伺服器之間進行全雙工通訊,任一方都可以通過建立的連線將資料推送到另一端。
  2. 建立在TCP 協議之上,伺服器端的實現比較容易。
  3. 與 HTTP協議有著良好的相容性。預設埠也是80443,並且握手階段採用HTTP 協議;
  4. 資料格式比較輕量,效能開銷小,通訊高效。
  5. 可以傳送文字,也可以傳送二進位制資料。
  6. 協議識別符號是ws(如果加密,則為wss),伺服器網址就是 URL

    websocket的缺點:

  1. 不相容低版本的IE;
  2. 安全性差(websocket協議與http協議不同,例如:WS訊息不包括HTTP頭,這可能會影響網路代理和防火牆的行為。WS的origin policy也與HTTP不同);

四、websocket的相關使用(客戶端與伺服器端)?

WebSocket客戶端

    客戶端 API

    以下 API 用於建立 WebSocket 物件。    

var Socket = new WebSocket(url, [protocol] );

    以上程式碼中的第一個引數 url, 指定連線的 URL。第二個引數 protocol 是可選的,指定了可接受的子協議。

  1. WebSocket 屬性

    以下是 WebSocket 物件的屬性。假定我們使用了以上程式碼建立了 Socket 物件:

    屬性 描述
    Socket.readyState 只讀屬性 readyState 表示連線狀態,可以是以下值:0 - 表示連線尚未建立。1 - 表示連線已建立,可以進行通訊。2 - 表示連線正在進行關閉。3 - 表示連線已經關閉或者連線不能開啟。
    Socket.bufferedAmount 只讀屬性 bufferedAmount 已被 send() 放入正在佇列中等待傳輸,但是還沒有發出的 UTF-8 文字位元組數。

    WebSocket 事件

    以下是 WebSocket 物件的相關事件。假定我們使用了以上程式碼建立了 Socket 物件:

    事件 事件處理程式 描述
    open Socket.onopen 連線建立時觸發
    message Socket.onmessage 客戶端接收服務端資料時觸發
    error Socket.onerror 通訊發生錯誤時觸發
    close Socket.onclose 連線關閉時觸發

    WebSocket 方法

    以下是 WebSocket 物件的相關方法。假定我們使用了以上程式碼建立了 Socket 物件:

    方法 描述
    Socket.send() 使用連線傳送資料
    Socket.close() 關閉連線

    示例

    // 初始化一個 WebSocket 物件
    var ws = new WebSocket("ws://localhost:9998/echo");
    
    // 建立 web socket 連線成功觸發事件
    ws.onopen = function () {
      // 使用 send() 方法傳送資料
      ws.send("傳送資料");
      alert("資料傳送中...");
    };
    
    // 接收服務端資料時觸發事件
    ws.onmessage = function (evt) {
      var received_msg = evt.data;
      alert("資料已接收...");
    };
    
    // 斷開 web socket 連線成功觸發事件
    ws.onclose = function () {
      alert("連線已關閉...");
    };

WebSocket 服務端

WebSocket 在服務端的實現非常豐富。Node.js、Java、C++、Python 等多種語言都有自己的解決方案。

以下,介紹我在學習 WebSocket 過程中接觸過的 WebSocket 服務端解決方案。

Node.js

常用的 Node 實現有以下三種。

Java

Java 的 web 一般都依託於 servlet 容器。

我使用過的 servlet 容器有:Tomcat、Jetty、Resin。其中Tomcat7、Jetty7及以上版本均開始支援 WebSocket(推薦較新的版本,因為隨著版本的更迭,對 WebSocket 的支援可能有變更)。

此外,Spring 框架對 WebSocket 也提供了支援。

雖然,以上應用對於 WebSocket 都有各自的實現。但是,它們都遵循RFC6455 的通訊標準,並且 Java API 統一遵循 JSR 356 - JavaTM API for WebSocket 規範。所以,在實際編碼中,API 差異不大。

Spring

Spring 對於 WebSocket 的支援基於下面的 jar 包:

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-websocket</artifactId>
  <version>${spring.version}</version>
</dependency>

在 Spring 實現 WebSocket 伺服器大概分為以下幾步:

建立 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 {

   /**
    * 建立連線後觸發的回撥
    */
   void afterConnectionEstablished(WebSocketSession session) throws Exception;

   /**
    * 收到訊息時觸發的回撥
    */
   void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception;

   /**
    * 傳輸訊息出錯時觸發的回撥
    */
   void handleTransportError(WebSocketSession session, Throwable exception) throws Exception;

   /**
    * 斷開連線後觸發的回撥
    */
   void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception;

   /**
    * 是否處理分片訊息
    */
   boolean supportsPartialMessages();

}

配置 WebSocket

配置有兩種方式:註解和 xml 。其作用就是將 WebSocket 處理器新增到註冊中心。

  1. 實現 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>

更多配置細節可以參考: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,你還需要引入它的實現包:

<!-- 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 的處理器。

然後,你可以在這個類中使用下面的註解來表明所修飾的方法是觸發事件的回撥

// 收到訊息觸發事件
@OnMessage
public void onMessage(String message, Session session) throws IOException, InterruptedException {
    ...
}

// 開啟連線觸發事件
@OnOpen
public void onOpen(Session session, EndpointConfig config, @PathParam("id") String id) {
    ...
}

// 關閉連線觸發事件
@OnClose
public void onClose(Session session, CloseReason closeReason) {
    ...
}

// 傳輸訊息錯誤觸發事件
@OnError
public 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 代理

如果把 WebSocket 的通訊看成是電話連線,Nginx 的角色則像是電話接線員,負責將發起電話連線的電話轉接到指定的客服。

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;
  }
}

更多配置細節可以參考:Nginx 官方的 websocket 文件

FAQ

HTTP 和 WebSocket 有什麼關係?

Websocket 其實是一個新協議,跟 HTTP 協議基本沒有關係,只是為了相容現有瀏覽器的握手規範而已,也就是說它是 HTTP 協議上的一種補充。

Html 和 HTTP 有什麼關係?

Html 是超文字標記語言,是一種用於建立網頁的標準標記語言。它是一種技術標準。Html5 是它的最新版本。

Http 是一種網路通訊協議。其本身和 Html 沒有直接關係。

完整示例

如果需要完整示例程式碼,可以參考我的 Github 程式碼:

資料

本文章感謝靜默虛空作者文章支援,文章地址: