1. 程式人生 > >【Java】WebSocket協議與 SpringMVC整合WebSocket demo

【Java】WebSocket協議與 SpringMVC整合WebSocket demo

WebSocket協議

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

WebSocket通訊協議於2011年被IETF定為標準RFC 6455,並被RFC7936所補充規範。

WebSocket協議支援(在受控環境中執行不受信任的程式碼的)客戶端與(選擇加入該程式碼的通訊的)遠端主機之間進行全雙工通訊。用於此的安全模型是Web瀏覽器常用的基於原始的安全模式。 協議包括一個開放的握手以及隨後的TCP層上的訊息幀。 該技術的目標是為基於瀏覽器的、需要和伺服器進行雙向通訊的(伺服器不能依賴於開啟多個HTTP連線(例如,使用XMLHttpRequest或<iframe>和長輪詢))應用程式提供一種通訊機制。

長久以來, 建立實現客戶端和使用者端之間雙工通訊的web app都會造成HTTP輪詢的濫用: 客戶端向主機不斷髮送不同的HTTP呼叫來進行詢問。 這會導致一系列的問題: 1.伺服器被迫為每個客戶端使用許多不同的底層TCP連線:一個用於向客戶端傳送資訊,其它用於接收每個傳入訊息。 2.有些協議有很高的開銷,每一個客戶端和伺服器之間都有HTTP頭。 3.客戶端指令碼被迫維護從傳出連線到傳入連線的對映來追蹤回覆。 一個更簡單的解決方案是使用單個TCP連線雙向通訊。 這就是WebSocket協議所提供的功能。 結合WebSocket API ,WebSocket協議提供了一個用來替代HTTP輪詢實現網頁到遠端主機的雙向通訊的方法。 WebSocket協議被設計來取代用HTTP作為傳輸層的雙向通訊技術,這些技術只能犧牲效率和可依賴性其中一方來提高另一方,因為HTTP最初的目的不是為了雙向通訊。(獲得更多關於此的討論可查閱RFC6202)

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

  1. Header 互相溝通的Header是很小的-大概只有 2 Bytes
  2. Server Push 伺服器的推送,伺服器不再被動的接收到瀏覽器的請求之後才返回資料,而是在有新資料時就主動推送給瀏覽器。

(上面資料來自百度)

Websocket是一個持久化的協議,相對於HTTP這種非持久的協議來說。簡單的舉個例子吧,用目前應用比較廣泛的PHP生命週期來解釋。

HTTP的生命週期通過 Request 來界定,也就是一個 Request 一個 Response ,那麼在 HTTP1.0 中,這次HTTP請求就結束了。

在HTTP1.1中進行了改進,使得有一個keep-alive,也就是說,在一個HTTP連線中,可以傳送多個Request,接收多個Response。但是請記住 Request = Response , 在HTTP中永遠是這樣,也就是說一個request只能有一個response。而且這個response也是被動的,不能主動發起。

特點:

事件驅動

非同步

使用ws或者wss協議的客戶端socket

能夠實現真正意義上的推送功能

缺點:

少部分瀏覽器不支援,瀏覽器支援的程度與方式有區別。

Websocket的使用場景
ajax輪詢

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

long poll

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

從上面可以看出其實這兩種方式,都是在不斷地建立HTTP連線,然後等待服務端處理,可以體現HTTP協議的另外一個特點,被動性。 何為被動性呢,其實就是,服務端不能主動聯絡客戶端,只能有客戶端發起。

沒有其他技術能夠像WebSocket一樣提供真正的雙向通訊,許多web開發者仍然是依賴於ajax的長輪詢來實現。 決定手頭的工作是否需要使用WebSocket技術的方法很簡單:

你的應用提供多個使用者相互交流嗎?
你的應用是展示伺服器端經常變動的資料嗎?

如果你的回答是肯定的,那麼請考慮使用WebSocket。如果你仍然不確定,並想要更多的靈感,這有一些殺手鐗的案例。

1.社交訂閱

對社交類的應用的一個裨益之處就是能夠即時的知道你的朋友正在做什麼。雖然聽起來有點可怕,但是我們都喜歡這樣做。你不會想要在數分鐘之後才能知道一個家庭成員在餡餅製作大賽獲勝或者一個朋友訂婚的訊息。你是線上的,所以你的訂閱的更新應該是實時的。 2.多玩家遊戲 網路正在迅速轉變為遊戲平臺。在不使用外掛(我指的是Flash)的情況下,網路開發者現在可以在瀏覽器中實現和體驗高效能的遊戲。無論你是在處理DOM元素、CSS動畫,HTML5的canvas或者嘗試使用WebGL,玩家之間的互動效率是至關重要的。我不想在我扣動扳機之後,我的對手卻已經移動位置。

3.協同編輯/程式設計

我們生活在分散式開發團隊的時代。平時使用一個文件的副本就滿足工作需求了,但是你最終需要有一個方式來合併所有的編輯副本。版本控制系統,比如Git能夠幫助處理某些檔案,但是當Git發現一個它不能解決的衝突時,你仍然需要去跟蹤人們的修改歷史。通過一個協同解決方案,比如WebSocket,我們能夠工作在同一個文件,從而省去所有的合併版本。這樣會很容易看出誰在編輯什麼或者你在和誰同時在修改文件的同一部分。

4.點選流資料

分析使用者與你網站的互動是提升你的網站的關鍵。HTTP的開銷讓我們只能優先考慮和收集最重要的資料部分。然後,經過六個月的線下分析,我們意識到我們應該收集一個不同的判斷標準——一個看起來不是那麼重要但是現在卻影響了一個關鍵的決定。與HTTP請求的開銷方式相比,使用Websocket,你可以由客戶端傳送不受限制的資料。想要在除頁面載入之外跟蹤滑鼠的移動?只需要通過WebSocket連線傳送這些資料到伺服器,並存儲在你喜歡的NoSQL資料庫中就可以了(MongoDB是適合記錄這樣的事件的)。現在你可以通過回放使用者在頁面的動作來清楚的知道發生了什麼。

5.股票基金報價

金融界瞬息萬變——幾乎是每毫秒都在變化。我們人類的大腦不能持續以那樣的速度處理那麼多的資料,所以我們寫了一些演算法來幫我們處理這些事情。雖然你不一定是在處理高頻的交易,但是,過時的資訊也只能導致損失。當你有一個顯示盤來跟蹤你感興趣的公司時,你肯定想要隨時知道他們的價值,而不是10秒前的資料。使用WebSocket可以流式更新這些資料變化而不需要等待。

6.體育實況更新

現在我們開始討論一個讓人們激情澎湃的愚蠢的東西——體育。我不是運動愛好者,但是我知道運動迷們想要什麼。當愛國者在打比賽的時候,我的妹夫將會沉浸於這場比賽中而不能自拔。那是一種瘋狂痴迷的狀態,完全發自內心的。我雖然不理解這個,但是我敬佩他們與運動之間的這種強烈的聯絡,所以,最後我能做的就是給他的體驗中降低延遲。如果你在你的網站應用中包含了體育新聞,WebSocket能夠助力你的使用者獲得實時的更新。

7.多媒體聊天

視訊會議並不能代替和真人相見,但當你不能在同一個屋子裡見到你談話的物件時,視訊會議是個不錯的選擇。儘管視訊會議私有化做的“不錯”,但其使用還是很繁瑣。我可是開放式網路的粉絲,所以用WebSockets getUserMedia API和HTML5音視訊元素明顯是個不錯的選擇。WebRTC的出現順理成章的成為我剛才概括的組合體,它看起來很有希望,但其缺乏目前瀏覽器的支援,所以就取消了它成為候選人的資格。

8.基於位置的應用

越來越多的開發者借用移動裝置的GPS功能來實現他們基於位置的網路應用。如果你一直記錄使用者的位置(比如執行應用來記錄運動軌跡),你可以收集到更加細緻化的資料。如果你想實時的更新網路資料儀表盤(可以說是一個監視運動員的教練),HTTP協議顯得有些笨拙。借用WebSocket TCP連結可以讓資料飛起來。

9.線上教育

上學花費越來越貴了,但網際網路變得更快和更便宜。線上教育是學習的不錯方式,尤其是你可以和老師以及其他同學一起交流。很自然,WebSockets是個不錯的選擇,可以多媒體聊天、文字聊天以及其它優勢如與別人合作一起在公共數字黑板上畫畫...

WebSocket方法介紹:
websocket允許通過JavaScript建立與遠端伺服器的連線,從而實現客戶端與伺服器間雙向的通訊。在websocket中有兩個方法:  
    1send() 向遠端伺服器傳送資料
    2close() 關閉該websocket連結
  websocket同時還定義了幾個監聽函式    
    1、onopen 當網路連線建立時觸發該事件
    2、onerror 當網路發生錯誤時觸發該事件
    3、onclose 當websocket被關閉時觸發該事件
    4、onmessage 當websocket接收到伺服器發來的訊息的時觸發的事件,也是通訊中最重要的一個監聽事件。msg.data
  websocket還定義了一個readyState屬性,這個屬性可以返回websocket所處的狀態:
    1CONNECTING(0) websocket正嘗試與伺服器建立連線
    2OPEN(1) websocket與伺服器已經建立連線
    3CLOSING(2) websocket正在關閉與伺服器的連線
    4CLOSED(3) websocket已經關閉了與伺服器的連線

  websocket的url開頭是ws,如果需要ssl加密可以使用wss,當我們呼叫websocket的構造方法構建一個websocket物件(new WebSocket(url))的之後,就可以進行即時通訊了。
SpringMVC專案整合WebSocket案例:
1.pom.xml配置
<!-- websocket協議 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-websocket</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
   <groupId>javax.websocket</groupId>
   <artifactId>javax.websocket-api</artifactId>
   <version>1.1</version>
   <scope>provided</scope>
</dependency>
<!-- websocket協議  end-->
2.程式碼編寫
chat.jsp頁面
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>

    <head lang="en">
        <meta charset="UTF-8">
        <script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
        <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css">
        <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">
        <script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
        <script src="//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
        <title>webSocket-測試使用者</title>
        <script type="text/javascript">
            $(function() {
                var websocket;
                if('WebSocket' in window) {
                                        console.log("此瀏覽器支援websocket");
                    websocket = new WebSocket("ws://127.0.0.1:8080/chat/12345");
                } else if('MozWebSocket' in window) {
                    alert("此瀏覽器只支援MozWebSocket");
                } else {
                    alert("此瀏覽器只支援SockJS");
                }
                websocket.onopen = function(evnt) {
                    $("#tou").html("連結伺服器成功!")
                };
                websocket.onmessage = function(evnt) {
                    $("#msg").html($("#msg").html() + "<br/>" + evnt.data);
                };
                websocket.onerror = function(evnt) {};
                websocket.onclose = function(evnt) {
                    $("#tou").html("與伺服器斷開了連結!")
                }
                
                
                
                
                $('#send').click(function(){
                	send();
                });

                function send() {
                    if(websocket != null) {
                        var message = document.getElementById('message').value;
                        websocket.send(message);
                    } else {
                        alert('未與伺服器連結.');
                    }
                }
            });
        </script>
    </head>

    <body>
        <div class="page-header" id="tou">
            webSocket多終端聊天測試
        </div>
        <div class="well" id="msg"></div>
        <div class="col-lg">
            <div class="input-group">
                <input type="text" class="form-control" placeholder="傳送資訊..." id="message">
                <span class="input-group-btn">
                    <button class="btn btn-default" type="button" id="send" >傳送</button>
                </span>
            </div>
        </div>
    </body>

</html>
後臺程式碼
package com.naton.controller;

import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

import org.apache.log4j.Logger;
import org.springframework.web.socket.server.standard.SpringConfigurator;


//websocket連線URL地址和可被呼叫配置
@ServerEndpoint(value="/chat/{userId}",configurator = SpringConfigurator.class)
public class WebSocketChat {
	
	private static Logger logger = Logger.getRootLogger();
    //靜態變數,用來記錄當前線上連線數。應該把它設計成執行緒安全的。
    private static int onlineCount = 0;
   
    //記錄每個使用者下多個終端的連線
    private static Map<String, Set<WebSocketChat>> userSocket = new HashMap<>();
 
    //需要session來對使用者傳送資料, 獲取連線特徵userId
    private Session session;
    private String userId;
   
    /**
     * @Title: onOpen
     * @Description: websocekt連線建立時的操作
     * @param @param userId 使用者id
     * @param @param session websocket連線的session屬性
     * @param @throws IOException
     */
    @OnOpen
    public void onOpen(@PathParam("userId") String userId,Session session) throws IOException{
        this.session = session;
        this.userId = userId;
        onlineCount++;
        //根據該使用者當前是否已經在別的終端登入進行新增操作
        if (userSocket.containsKey(this.userId)) {
            logger.info("當前使用者id:{"+this.userId+"}已有其他終端登入");
            userSocket.get(this.userId).add(this); //增加該使用者set中的連線例項
        }else {
            logger.info("當前使用者id:{"+this.userId+"}第一個終端登入");
            Set<WebSocketChat> addUserSet = new HashSet<>();
            addUserSet.add(this);
            userSocket.put(this.userId, addUserSet);
        }
        logger.info("使用者{"+userId+"}登入的終端個數是為{"+userSocket.get(this.userId).size()+"}");
        logger.info("當前線上使用者數為:{"+userSocket.size()+"},所有終端個數為:{"+onlineCount+"}");
    }
   
    /**
     * @Title: onClose
     * @Description: 連線關閉的操作
     */
    @OnClose
    public void onClose(){
        //移除當前使用者終端登入的websocket資訊,如果該使用者的所有終端都下線了,則刪除該使用者的記錄
        if (userSocket.get(this.userId).size() == 0) {
            userSocket.remove(this.userId);
        }else{
            userSocket.get(this.userId).remove(this);
        }
        logger.info("使用者{"+this.userId+"}登入的終端個數是為{"+userSocket.get(this.userId).size()+"}");
        logger.info("當前線上使用者數為:{"+userSocket.size()+"},所有終端個數為:{"+onlineCount+"}");
    }
   
    /**
     * @Title: onMessage
     * @Description: 收到訊息後的操作
     * @param @param message 收到的訊息
     * @param @param session 該連線的session屬性
     */
    @OnMessage
    public void onMessage(String message, Session session) {    
        logger.info("收到來自使用者id為:{"+this.userId+"}的訊息:{"+message+"}");
        if(session ==null)  logger.info("session null");
        //測試向客戶端傳送訊息傳送
        sendMessageToUser(this.userId,"伺服器收到你的訊息:"+ message);
    }
   
    /**
     * @Title: onError
     * @Description: 連線發生錯誤時候的操作
     * @param @param session 該連線的session
     * @param @param error 發生的錯誤
     */
    @OnError
    public void onError(Session session, Throwable error){
        logger.info("使用者id為:{"+this.userId+"}的連線傳送錯誤");
        error.printStackTrace();
    }
   
  /**
   * @Title: sendMessageToUser
   * @Description: 傳送訊息給使用者下的所有終端
   * @param @param userId 使用者id
   * @param @param message 傳送的訊息
   * @param @return 傳送成功返回true,反則返回false
   */
    public Boolean sendMessageToUser(String userId,String message){
        if (userSocket.containsKey(userId)) {
            logger.info(" 給使用者id為:{"+userId+"}的所有終端傳送訊息:{"+message+"}");
            for (WebSocketChat WS : userSocket.get(userId)) {
                logger.info("sessionId為:{"+WS.session.getId()+"}");
                try {
                    WS.session.getBasicRemote().sendText(message);
                } catch (IOException e) {
                    e.printStackTrace();
                    logger.info(" 給使用者id為:{"+userId+"}傳送訊息失敗");
                    return false;
                }
            }
            return true;
        }
        logger.info("傳送錯誤:當前連線不包含id為:{"+userId+"}的使用者");
        return false;
    }
  
}

WSMessageService類

package com.naton.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import com.naton.controller.WebSocketChat;


@Service("webSocketMessageService")
public class WSMessageService {
    private Logger logger = LoggerFactory.getLogger(WSMessageService.class);
    //宣告websocket連線類
    private WebSocketChat webSocketChat = new WebSocketChat();

    /**
     * @Title: sendToAllTerminal
     * @Description: 呼叫websocket類給使用者下的所有終端傳送訊息
     * @param @param userId 使用者id
     * @param @param message 訊息
     * @param @return 傳送成功返回true,否則返回false
     */
    public Boolean sendToAllTerminal(String userId,String message){   
        logger.info("向用戶{}的訊息:{}",userId,message);
        if(webSocketChat.sendMessageToUser(userId,message)){
            return true;
        }else{
            return false;
        }   
    }           
}

MessageController類

package com.naton.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import com.naton.service.WSMessageService;

@Controller
@RequestMapping("/message")
public class MessageController {
	
    //websocket服務層呼叫類
    @Autowired
    private WSMessageService wsMessageService;

  //請求入口
    @RequestMapping(value="/TestWS",method=RequestMethod.GET)
    @ResponseBody
    public String TestWS(@RequestParam(value="userId",required=true) String userId,
        @RequestParam(value="message",required=true) String message){