1. 程式人生 > >(轉)WebSocket學習

(轉)WebSocket學習

石墨文件:https://shimo.im/docs/3UkyOPJvmj4f9EAP/

(二期)17、即時通訊技術websocket

【課程17】java We...實現.xmind0.1MB

【課程17】spirngbo...cket.xmind0.1MB

【課程17】webso...簡介.xmind0.2MB

【課程17】websoc...過程.xmind0.2MB

【課程17預習】即...cket.xmind0.1MB

  百度百科:websocket

WebSocket協議是基於TCP的一種新的網路協議。它實現了瀏覽器與伺服器全雙工(full-duplex)通訊——允許伺服器主動傳送資訊給客戶端。

http的不足,websocket的出現 websocket背景

瞭解計算機網路協議的人,應該都知道:HTTP 協議是一種無狀態的、無連線的、單向的應用層協議。它採用了請求/響應模型。通訊請求只能由客戶端發起,服務端對請求做出應答處理。

這種通訊模型有一個弊端:HTTP 協議無法實現伺服器主動向客戶端發起訊息。

這種單向請求的特點,註定瞭如果伺服器有連續的狀態變化,客戶端要獲知就非常麻煩。大多數 Web 應用程式將通過頻繁的非同步JavaScript和XML(AJAX)請求實現長輪詢。輪詢的效率低,非常浪費資源(因為必須不停連線,或者 HTTP 連線始終開啟)。

 

因此,工程師們一直在思考,有沒有更好的方法。WebSocket 就是這樣發明的。WebSocket 連線允許客戶端和伺服器之間進行全雙工通訊,以便任一方都可以通過建立的連線將資料推送到另一端。WebSocket 只需要建立一次連線,就可以一直保持連線狀態。這相比於輪詢方式的不停建立連線顯然效率要大大提高。

 

http解決雙工常用方法:

長久以來, 建立實現客戶端和使用者端之間雙工通訊的web app都會造成HTTP輪詢的濫用: 客戶端向主機不斷髮送不同的HTTP呼叫來進行詢問。

橋樑技術 -- ajax輪詢

ajax輪詢 的原理非常簡單,讓瀏覽器隔個幾秒就傳送一次請求,詢問伺服器是否有新資訊。

場景再現:

客戶端:啦啦啦,有沒有新資訊(Request)

服務端:沒有(Response)

客戶端:啦啦啦,有沒有新資訊(Request)

服務端:沒有。。(Response)

客戶端:啦啦啦,有沒有新資訊(Request)

服務端:你好煩啊,沒有啊。。(Response)

客戶端:啦啦啦,有沒有新訊息(Request)

服務端:好啦好啦,有啦給你。(Response)

客戶端:啦啦啦,有沒有新訊息(Request)

服務端:。。。。。沒。。。。沒。。。沒有(Response) ---- loop

橋樑技術 -- 長輪詢

long poll 其實原理跟 ajax輪詢 差不多,都是採用輪詢的方式,不過採取的是阻塞模型(一直打電話,沒收到就不掛電話),也就是說,客戶端發起連線後,如果沒訊息,就一直不返回Response給客戶端。直到有訊息才返回,返回完之後,客戶端再次建立連線,周而復始。

場景再現

客戶端:啦啦啦,有沒有新資訊,沒有的話就等有了才返回給我吧(Request)

服務端:額。。 等待到有訊息的時候。。來 給你(Response)

客戶端:啦啦啦,有沒有新資訊,沒有的話就等有了才返回給我吧(Request) -loop

 

websocket特點

  1. 伺服器可以主動向客戶端推送資訊,客戶端也可以主動向伺服器傳送資訊,是真正的雙向平等對話,屬於伺服器推送技術的一種。
  1. 建立在 TCP 協議之上,伺服器端的實現比較容易。
  1. 與 HTTP 協議有著良好的相容性。預設埠也是80和443,並且握手階段採用 HTTP 協議,因此握手時不容易遮蔽,能通過各種 HTTP 代理伺服器。
  1. 資料格式比較輕量,效能開銷小,通訊高效。
  1. 可以傳送文字,也可以傳送二進位制資料。
  1. 沒有同源限制,客戶端可以與任意伺服器通訊。
  1. 協議識別符號是ws(如果加密,則為wss),伺服器網址就是 URL。
ws://example.com:80/some/path
websocket實現原理

在實現websocket連線過程中,需要通過瀏覽器發出websocket連線請求,然後伺服器發出迴應,這個過程通常稱為“握手” 。在 WebSocket API,瀏覽器和伺服器只需要做一個握手的動作,然後,瀏覽器和伺服器之間就形成了一條快速通道。兩者之間就直接可以資料互相傳送。在此WebSocket 協議中,為我們實現即時服務帶來了兩大好處:

  • 1. Header

互相溝通的Header是很小的-大概只有 2 Bytes

  • 2. Server Push

伺服器的推送,伺服器不再被動的接收到瀏覽器的請求之後才返回資料,而是在有新資料時就主動推送給瀏覽器。

websocket與http的關係

首先Websocket是基於HTTP協議的,或者說借用了HTTP的協議來完成一部分握手。

我們來看個典型的 Websocket 握手(借用Wikipedia的。。)

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

熟悉HTTP的童鞋可能發現了,這段類似HTTP協議的握手請求中,多了幾個東西。我會順便講解下作用。

Upgrade: websocket
Connection: Upgrade

這個就是Websocket的核心了,告訴 Apache 、 Nginx 等伺服器:注意啦,我發起的是Websocket協議,快點幫我找到對應的助理處理~不是那個老土的HTTP。

Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

首先, Sec-WebSocket-Key 是一個 Base64 encode 的值,這個是瀏覽器隨機生成的,告訴伺服器:泥煤,不要忽悠窩,我要驗證尼是不是真的是Websocket助理。與後面服務端響應首部的 Sec-WebSocket-Accept 是配套的,提供基本的防護,比如惡意的連線,或者無意的連線。

然後, Sec_WebSocket-Protocol 是一個使用者定義的字串,用來區分同URL下,不同的服務所需要的協議。簡單理解:今晚我要服務A,別搞錯啦~

最後, Sec-WebSocket-Version 是告訴伺服器所使用的 Websocket Draft(協議版本),在最初的時候,Websocket協議還在 Draft 階段,各種奇奇怪怪的協議都有,而且還有很多期奇奇怪怪不同的東西,什麼Firefox和Chrome用的不是一個版本之類的,當初Websocket協議太多可是一個大難題。。不過現在還好,已經定下來啦~大家都使用的一個東西~ 脫水: 服務員,我要的是13歲的噢→_→

然後伺服器會返回下列東西,表示已經接受到請求, 成功建立Websocket啦!

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

這裡開始就是HTTP最後負責的區域了,告訴客戶,我已經成功切換協議啦~

Upgrade: websocket
Connection: Upgrade

依然是固定的,告訴客戶端即將升級的是 Websocket 協議,而不是mozillasocket,lurnarsocket或者shitsocket。

 

然後, Sec-WebSocket-Accept 這個則是經過伺服器確認,並且加密過後的 Sec-WebSocket-Key 。 

Sec-WebSocket-Accept 的計算方法:

  • 將 Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接;
  • 通過 SHA1 計算出摘要,並轉成 base64 字串。

作用:

  1. 避免服務端收到非法的websocket連線(比如http客戶端不小心請求連線websocket服務,此時服務端可以直接拒絕連線)
  1. 確保服務端理解websocket連線。因為ws握手階段採用的是http協議,因此可能ws連線是被一個http伺服器處理並返回的,此時客戶端可以通過Sec-WebSocket-Key來確保服務端認識ws協議。(並非百分百保險,比如總是存在那麼些無聊的http伺服器,光處理Sec-WebSocket-Key,但並沒有實現ws協議。。。)
  1. Sec-WebSocket-Key主要目的並不是確保資料的安全性,因為Sec-WebSocket-Key、Sec-WebSocket-Accept的轉換計算公式是公開的,而且非常簡單,最主要的作用是預防一些常見的意外情況(非故意的)。

 

後面的, Sec-WebSocket-Protocol 則是表示最終使用的協議。

至此,HTTP已經完成它所有工作了,接下來就是完全按照Websocket協議進行了。

 

連線成功的狀態碼是101。

 

java WebSocket實現

在maven中新增websocket庫的程式碼如下:

<dependency> 
  <groupId>javax.websocket</groupId> 
  <artifactId>javax.websocket-api</artifactId> 
  <version>1.1</version>
  <scope>provided</scope>
</dependency>
註解、成員資料介紹

@ServerEndpoint

宣告websocket地址類似Spring MVC中的@controller註解類似,websocket使用@ServerEndpoint來進行宣告介面:@ServerEndpoint(value="/websocket/{paraName}") ; 其中 “ { } ”用來表示帶引數的連線,如果需要獲取{}中的引數在引數列表中增加:@PathParam("paraName") Integer userId 。

 

[email protected]

public void onOpen(Session session) throws IOException{ }-------有連線時的觸發函式。 我們可以在使用者連線時記錄使用者的連線帶的引數,只需在引數列表中增加引數:@PathParam("paraName") String paraName。

 

[email protected]

public void onClose(){ }------連線關閉時的呼叫方法。

 

[email protected]

public void onMessage(String message, Session session) { }-------收到訊息時呼叫的函式,其中Session是每個websocket特有的資料成員

 

4.Session----每個Session代表了兩個web socket斷點的會話;當websocket握手成功後,websocket就會提供一個開啟的Session,可以通過這個Session來對另一個端點發送資料;如果Session關閉後傳送資料將會報錯。

 

5.Session.getBasicRemote().sendText("message")-------向該Session連線的使用者傳送字串資料。

 

[email protected]

public void onError(Session session, Throwable error) { }--------發生意外錯誤時呼叫的函式。

 

websocket demo git:

springboot + websocket專案實現

Websocket 是通過一個socket來實現雙工非同步通訊的能力。但是直接使用WebSocket協議開發程式顯得特別煩瑣,我門會使用它的子協議STOMP,它是一個更高階級別的協議,STOMP協議使用一個基於幀(frame)的格式來定義資訊。

SockJS

正如我們所知,websocket協議雖然已經被制定,當時還有很多版本的瀏覽器或瀏覽器廠商還沒有支援的很好。

 

所以,SockJS,可以理解為是websocket的一個備選方案。

 

那它如何規定備選方案的呢?

它大概支援這樣幾個方案:

  • Websockets
  • Streaming
  • Polling

當然,開啟並使用SockJS後,它會優先選用websocket協議作為傳輸協議,如果瀏覽器不支援websocket協議,則會在其他方案中,選擇一個較好的協議進行通訊。

此圖來源於 github: sockjs-client

 

所以,如果使用SockJS進行通訊,它將在使用上保持一致,底層由它自己去選擇相應的協議。

可以認為SockJS是websocket通訊層上的上層協議。底層對於開發者來說是透明的。

STOMP

STOMP 中文為: 面向訊息的簡單文字協議

 

websocket定義了兩種傳輸資訊型別: 文字資訊 和 二進位制資訊 ( text and binary )。型別雖然被確定,但是他們的傳輸體是沒有規定的。

 

當然你可以自己來寫傳輸體,來規定傳輸內容。(當然,這樣的複雜度是很高的)

所以,需要用一種簡單的文字傳輸型別來規定傳輸內容,它可以作為通訊中的文字傳輸協議,即互動中的高階協議來定義互動資訊。

STOMP本身可以支援流型別的網路傳輸協議: websocket協議和tcp協議。

 

stomp是一個用於client之間進行非同步訊息傳輸的簡單文字協議, 全稱是Simple Text Oriented Messaging Protocol.

對於stomp協議來說, client分為消費者client與生產者client兩種. server是指broker, 也就是訊息佇列的管理者.

 

stomp協議並不是為websocket設計的, 它是屬於訊息佇列的一種協議, 和amqp, jms平級. 

只不過由於它的簡單性恰巧可以用於定義websocket的訊息體格式. 

 

stomp協議很多mq都已支援, 比如rabbitmq, activemq. 很多語言也都有stomp協議的解析client庫.

可以這麼理解, websocket結合stomp相當於一個面向公網對使用者比較友好的一種訊息佇列.

 

stomp協議中的client分為兩角色:

  • 生產者: 通過SEND命令給某個目的地址(destination)傳送訊息.
  • 消費者: 通過SUBSCRIBE命令訂閱某個目的地址(destination), 當生產者傳送訊息到目的地址後, 訂閱此目的地址的消費者會即時收到訊息.

 

它的格式為:

springboot 基於子協議STOMP開發的websocket 後端技術方案選型

websocket服務端選型:spring websocket

支援SockJS,開啟SockJS後,可應對不同瀏覽器的通訊支援

支援STOMP傳輸協議,可無縫對接STOMP協議下的訊息代理器(如:RabbitMQ, ActiveMQ)

前端技術方案選型

前端選型: stomp.js,sockjs.js

後端開啟SOMP和SockJS支援後,前對應有對應的js庫進行支援.

所以選用此兩個庫.

技術選型總結

上述所用技術,是這樣的邏輯:

  • 開啟socktJS:

如果有瀏覽器不支援websocket協議,可以在其他兩種協議中進行選擇,但是對於應用層來講,使用起來是一樣的。

這是為了支援瀏覽器不支援websocket協議的一種備選方案

  • 使用STOMP:

使用STOMP進行互動,前端可以使用stomp.js類庫進行互動,訊息一STOMP協議格式進行傳輸,這樣就規定了訊息傳輸格式。

訊息進入後端以後,可以將訊息與實現STOMP格式的代理器進行整合。

這是為了訊息統一管理,進行機器擴容時,可進行負載均衡部署

  • 使用spring websocket:

使用spring websocket,是因為他提供了STOMP的傳輸自協議的同時,還提供了StockJS的支援。

當然,除此之外,spring websocket還提供了許可權整合的功能,還有自帶天生與spring家族等相關框架進行無縫整合。

websocket資訊流

 

  • Message在應用中的流動是這樣一個流程,如上圖。若destination是以/app開始則會通過request channel交給註解方法來處理,處理完畢根據預設的路徑轉發給SimpleBroker處理(若不使用預設路徑可以用@SendTo來指定路徑),處理完畢後交由response channel返回連線的客戶端。 
  • 若destination是以/topic開頭則直接交給SimpleBroker處理。

 

 

第一步:匯入spirngboot與websocket整合的pom座標

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!--新增jsp支援-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
    <scope>provided</scope>
</dependency>
 
  

第二步:自定義websocket配置,配置內容包括開啟子協議STOMP,配置服務端點,字首,等資訊。

@Configuration
@EnableWebSocketMessageBroker//註解表示開啟使用STOMP協議來傳輸基於代理的訊息,Broker就是代理的意思。
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
 
  
    /***
     * 註冊 Stomp的端點
     * addEndpoint:新增STOMP協議的端點。提供WebSocket或SockJS客戶端訪問的地址
     * withSockJS:使用SockJS協議
     * @param registry
     */
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/endpointWisely")
                .withSockJS() ;
 
  
        registry.addEndpoint("/websocket")
                .setAllowedOrigins("*")//新增允許跨域訪問
                .withSockJS() ;
    }
 
  
    /**
     * 配置訊息代理
     * 啟動Broker,訊息的傳送的地址符合配置的字首來的訊息才傳送到這個broker
     */
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/api/v1/socket/send","/user/", "/topic");//推送訊息字首
        registry.setApplicationDestinationPrefixes("/api/v1/socket/req");//應用請求字首
        registry.setUserDestinationPrefix("/user");//推送使用者字首
    }
 
  
}

Spring框架提供基於websocket的STOMP支援,需要使用spring-messaging和spring-websocket模組。 

下面的配置中,註冊了一個字首為/endpointWisely的stomp終端,客戶端可以使用該url來建立websocket連線。 

 

Message的destination如果是以/app開頭,則會轉發給響應的訊息處理方法(如使用@MessageMapping註解的方法),

 

如果是以/topic,/queue開頭則會被轉發給訊息代理(broker),由broker廣播給連線的客戶端。

 

 

第三步:上面配置完了之後,就可以開始編寫內容了。這裡表示伺服器傳送地址對映到/welcomeTopic,然後所有訂閱了/topic/getResponse路徑的都可以收到廣播訊息。

@MessageMapping("/welcomeTopic")//瀏覽器傳送請求通過@messageMapping 對映/welcome 這個地址。
@SendTo("/topic/getResponse")//伺服器端有訊息時,會訂閱@SendTo 中的路徑的瀏覽器傳送訊息。
public ResponseMessage say(RequestMessage message) throws Exception {
    System.out.println("傳送資訊-----------------------" + message.getMessage());
 
  
    return new ResponseMessage("Welcome, " + message.getMessage() + "!");
}

 

第四步:由於使用了sockjs,因此前端需要匯入相關的js檔案。

sockjs.js0.2MB

stomp.js16.7KB

jquery.js0.3MB

 

頁面如下:

topic.jsp2.6KB

 

 

常用註解
  • @EnableWebSocketMessageBroker

通過EnableWebSocketMessageBroker 開啟使用STOMP協議來傳輸基於代理(message broker)的訊息,此時瀏覽器支援使用@MessageMapping 就像支援@RequestMapping一樣。

  • @MessageMapping

配置中定義的config.setApplicationDestinationPrefixes("/app");表示如果連結以/app開頭,則會轉發給對應具有@MessageMapping對應連結的註解方法處理。如連結是/app/welcome則會找到@MessageMapping("/welcome")註解對應的方法。

 

  • @SendTo

可以把訊息廣播到路徑上去,例如上面可以把訊息廣播到”/topic/greetings”,如果客戶端在這個路徑訂閱訊息,則可以接收到訊息。

  • @SendToUser

訊息目的地有UserDestinationMessageHandler來處理,會將訊息路由到傳送者對應的目的地。預設該註解字首為/user。如:使用者訂閱/user/hi ,在@SendToUser('/hi')查詢目的地時,會將目的地的轉化為/user/{name}/hi, 這個name就是principal的name值,該操作是認為使用者登入並且授權認證,使用principal的name作為目的地標識。發給訊息來源的那個使用者。(就是誰請求給誰,不會發給所有使用者,區分就是依照principal-name來區分的)。

 

spring websocket基於註解的@SendTo和@SendToUser雖然方便,但是有侷限性,例如我這樣子的需求,我想手動的把訊息推送給某個人,或者特定一組人,怎麼辦,@SendTo只能推送給所有人,@SendToUser只能推送給請求訊息的那個人,這時,我們可以利用SimpMessagingTemplate這個類。

 

SimpMessagingTemplate有倆個推送的方法

  1. convertAndSend(destination, payload); //將訊息廣播到特定訂閱路徑中,類似@SendTo 
  1. convertAndSendToUser(user, destination, payload);//將訊息推送到固定的使用者訂閱路徑中,類似@SendToUser

 

  • @DestinationVariable

這個註解用於動態監聽路徑,很想rest中的@PathVariable:

@MessageMapping("/queue/chat/{uid}")
public void chat(@Payload @Validated Message message, @DestinationVariable("uid") String uid, Principal principal) {
    String msg = "傳送人: " + principal.getName() + " chat ";
    simpMessagingTemplate.convertAndSendToUser(uid,"/queue/chat",msg);
}
通訊層設計 – 1-1 && 1-n

1-n topic:

此方式,上述訊息模型章節已經講過,此處不再贅述

1-1 queue:

客服-使用者溝通為1-1使用者互動的案例

前端:

stompClient.subscribe('/user/queue/chat',function(greeting){
    showGreeting(greeting.body);
});

後端:

@MessageMapping("/queue/chat/{uid}")
public void chat(@Payload @Validated Message message, @DestinationVariable("uid") String uid, Principal principal) {
    String msg = "傳送人: " + principal.getName() + " chat ";
    simpMessagingTemplate.convertAndSendToUser(uid,"/queue/chat",msg);
}

傳送端:

function chat(uid) {
    stompClient.send("/app/queue/chat/"+uid,{},JSON.stringify({'title':'hello','content':'message content'}));
}

上述的轉化,看上去沒有topic那樣1-n的廣播要流暢,因為程式碼中採用約定的方式進行開發,當然這是由spring約定的。

 

 

專案git地址:

https://gitee.com/java-mindmap/training-camp-project-demo/tree/master/springboot-websocket