WebSocket實現羣發和單聊--Springboot實現

一:WebSocket原理

1、要談WebSocket就不得不提起HTTP連接

    WebSocket是HTML5出的東西(協議,就是大家一起約定好的東西),也就是說HTTP協議沒有變化,或者說沒關係,但HTTP是不支持持久連接的(長連接,循環連接的不算)首先HTTP有1.1和1.0之說,也就是所謂的keep-alive,把多個HTTP請求合併爲一個,但是Websocket其實是一個新協議,跟HTTP協議基本沒有關係,只是爲了兼容現有瀏覽器的握手規範而已,也就是說它是HTTP協議上的一種補充。有交集,但是並不是全部。當然他們都屬於網絡的7層協議中的應用層。

    另外Html5是指的一系列新的API,或者說新規範,新技術。Http協議本身只有1.0和1.1,而且跟Html本身沒有直接關係。
    通俗來說,你可以用HTTP協議傳輸非Html數據,就是這樣。再簡單來說,層級不一樣。

    此外,ws協議是全雙工協議,意味着不光服務器向客戶端推送,客戶端也可以像服務器發送請求。

2、Websocket是什麼樣的協議,具體有什麼優點

    首先,Websocket是一個持久化的協議,相對於HTTP這種非持久的協議來說。
    1) HTTP的生命週期通過Request來界定,也就是一個Request 一個Response,那麼在HTTP1.0中,這次HTTP請求就結束了。
    在HTTP1.1中進行了改進,使得有一個keep-alive,也就是說,在一個HTTP連接中,可以發送多個Request,接收多個Response。
    但是請記住 Request = Response , 在HTTP中永遠是這樣,也就是說一個request只能有一個response。而且這個response也是被動的,不能主動發起。
    2)首先Websocket是基於HTTP協議的,或者說借用了HTTP的協議來完成一部分握手。在握手階段是一樣的。

    在三次握手進行通信的過程中,Websocket協議要比HTTP協議的握手請求中,多了幾個東西。

    

這個就是Websocket的核心了,告訴Apache、Nginx等後端服務器:發起的是Websocket協議,即協議是ws:// 而不是http://。

之後,Sec-WebSocket-Key 是一個Base64 encode的值,這個是瀏覽器隨機生成的,告訴服務器:我要驗證你是不是真的是Websocket。
然後,Sec_WebSocket-Protocol 是一個用戶定義的字符串,用來區分同URL下,不同的服務所需要的協議。
最後,Sec-WebSocket-Version 是告訴服務器所使用的Websocket Draft(協議版本),在最初的時候,Websocket協議還在 Draft 階段,各種奇奇怪怪的協議都有,而且還有很多期奇奇怪怪不同的東西,什麼Firefox和Chrome用的不是一個版本之類的,當初Websocket協議太多可是一個大難題。不過現在還好,已經定下來啦~大家都使用的一個東西。
3、Websocket的作用

在講Websocket之前,我就順帶着講下 long poll 和 ajax輪詢 的原理。
首先是 ajax輪詢 ,ajax輪詢 的原理非常簡單,讓瀏覽器隔個幾秒就發送一次請求,詢問服務器是否有新信息。相當於java代碼的while循環,不段的發起請求,如果客戶端連接數量過多的話,對服務器壓力無疑是個重大的考驗。

其次是 long poll,其實原理跟 ajax輪詢 差不多,都是採用輪詢的方式,不過採取的是阻塞模型(就是請求到服務器後,一直不返回相應,也不超時),直到有消息才返回,返回完之後,客戶端再次建立連接,週而復始。

通過上面這個例子,我們可以看出,這兩種方式都不是最好的方式,需要很多資源。
一種需要更快的速度,一種需要更多的'電話'。這兩種都會導致'電話'的需求越來越高。
HTTP還是一個無狀態協議。通俗的說就是,服務器因爲每天要接待太多客戶了,是個健忘鬼,你一掛電話,他就把你的東西全忘光了,把你的東西全丟掉了。你第二次還得再告訴服務器一遍。
所以在這種情況下出現了,Websocket出現了。他解決了HTTP的這幾個難題。

首先,被動性,當服務器完成協議升級後(HTTP->Websocket),服務端就可以主動推送信息給客戶端啦。所以我們其實一直建立者連接,一旦有消息了,服務器就會推送給客戶端。

只需要經過一次HTTP請求,就可以做到源源不斷的信息傳送了。(在程序設計中,這種設計叫做回調,即:你有信息了再來通知我,而不是我傻乎乎的每次跑來問你)
這樣的協議解決了上面同步有延遲,而且還非常消耗資源的這種情況。

那麼爲什麼他會解決服務器上消耗資源的問題呢?
其實我們所用的程序是要經過兩層代理的,即HTTP協議在Nginx等服務器的解析下,然後再傳送給相應的Handler(JAVA等)來處理。
簡單地說,我們有一個非常快速的接線員(Nginx),他負責把問題轉交給相應的客服(Handler)。
本身接線員基本上速度是足夠的,但是每次都卡在客服(Handler)了,老有客服處理速度太慢。,導致客服不夠。
Websocket就解決了這樣一個難題,建立後,可以直接跟接線員建立持久連接,有信息的時候客服想辦法通知接線員,然後接線員在統一轉交給客戶。
這樣就可以解決客服處理速度過慢的問題了。
同時,在傳統的方式上,要不斷的建立,關閉HTTP協議,由於HTTP是非狀態性的,每次都要重新傳輸identity info(鑑別信息),來告訴服務端你是誰。
雖然接線員很快速,但是每次都要聽這麼一堆,效率也會有所下降的,同時還得不斷把這些信息轉交給客服,不但浪費客服的處理時間,而且還會在網路傳輸中消耗過多的流量/時間。
但是Websocket只需要一次HTTP握手,所以說整個通訊過程是建立在一次連接/狀態中,也就避免了HTTP的非狀態性,服務端會一直知道你的信息,直到你關閉請求,這樣就解決了接線員要反覆解析HTTP協議,還要查看identity info的信息。
同時由客戶主動詢問,轉換爲服務器(推送)有信息的時候就發送(當然客戶端還是等主動發送信息過來的。。),沒有信息的時候就交給接線員(Nginx),不需要佔用本身速度就慢的客服(Handler)了。

至於怎麼在不支持Websocket的客戶端上使用Websocket。。答案是:不能。
但是可以通過上面說的 long poll 和 ajax 輪詢來模擬出類似的效果。

二:代碼實現

    首先是jar包引入,springboot可以很容易的實現websocket協議的配置(springboot版本2.0以上)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

    接下來就是配置啓動文件了

package cn.chinotan.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * @program: test
 * @description: WebSocketConfig啓動配置
 * @author: xingcheng
 * @create: 2019-05-30 19:33
 **/
@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

    緊接着就是業務邏輯處理

package cn.chinotan.websocket;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @program: test
 * @description: WebSocketServer 服務器
 * @author: xingcheng
 * @create: 2019-05-30 19:34
 **/
@ServerEndpoint("/websocket/{sid}")
@Component
@Slf4j
public class WebSocketServer {

    /**
     * 靜態變量,用來記錄當前在線連接數。線程安全。
     */
    private static AtomicInteger onlineCount = new AtomicInteger(0);
    /**
     * concurrent包的線程安全Set,用來存放每個客戶端對應的MyWebSocket對象。
     */
    private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();

    /**
     * 與某個客戶端的連接會話,需要通過它來給客戶端發送數據
     */
    private Session session;

    /**
     * 接收sid
     */
    private String sid = "";

    /**
     * 連接建立成功調用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
        this.session = session;
        // 加入set中
        webSocketSet.add(this);
        // 在線數加1
        addOnlineCount();          
        log.info("有新窗口開始監聽:" + sid + ",當前在線人數爲" + getOnlineCount());
        this.sid = sid;
        sendMessage("連接成功: " + sid);
    }

    /**
     * 連接關閉調用的方法
     */
    @OnClose
    public void onClose() {
        //從set中刪除
        webSocketSet.remove(this);
        //在線數減1
        subOnlineCount();
        log.info("有一連接關閉!當前在線人數爲" + getOnlineCount());
    }

    /**
     * 收到客戶端消息後調用的方法
     *
     * @param message 客戶端發送過來的消息
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("收到來自窗口" + sid + "的信息:" + message);
        //羣發消息
        for (WebSocketServer item : webSocketSet) {
            item.sendMessage(message);
        }
    }

    /**
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("發生錯誤");
        error.printStackTrace();
    }

    /**
     * 實現服務器主動推送
     */
    public void sendMessage(String message) {
        try {
            this.session.getBasicRemote().sendText(message);
        } catch (IOException e) {
            log.error("消息推送失敗");
            e.printStackTrace();
        }
    }


    /**
     * 羣發自定義消息
     */
    public static void sendInfo(String message, String sid) {
        log.info("推送消息到窗口" + sid + ",推送內容:" + message);
        for (WebSocketServer item : webSocketSet) {
            //這裏可以設定只推送給這個sid的,爲null則全部推送
            if (sid == null) {
                item.sendMessage(message);
            } else if (item.sid.equals(sid)) {
                item.sendMessage(message);
            }
        }
    }

    public static int getOnlineCount() {
        return onlineCount.get();
    }

    public static void addOnlineCount() {
        WebSocketServer.onlineCount.addAndGet(1);
    }

    public static void subOnlineCount() {
        WebSocketServer.onlineCount.decrementAndGet();
    }
}

可以看到,通過springboot的註解,可以很容易的實現ws協議的編寫,類似於controller,並且還有對應的生命週期監聽註解

最後服務器端,我們配置一個推送請求,通過平常的http協議編寫,模擬服務器想客戶端推送過程

package cn.chinotan.controller;

import cn.chinotan.websocket.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.util.List;

/**
 * @program: test
 * @description: websocket控制器
 * @author: xingcheng
 * @create: 2019-06-01 15:13
 **/
@RestController
@RequestMapping("/webSocket")
public class WebSocketController {
    
    @Autowired
    WebSocketServer webSocketServer;

    @GetMapping("/server/push/{sid}")
    public void push(@PathVariable("sid") Integer sid) {
        if (0 == sid) {
            WebSocketServer.sendInfo(sid + ":全員注意啦!", null);
        } else {
            WebSocketServer.sendInfo(sid + ":注意啦! -> " + (sid + 1), String.valueOf(sid + 1));
        }
    }
    
}

最後,我們編寫客戶端的代碼,採用html

<html>

<head>
<title>WebSocket測試頁面</title>
</head>

<body>
    <script> 
    var socket;  
    if(typeof(WebSocket) == "undefined") {  
        console.log("您的瀏覽器不支持WebSocket");  
    }else{  
        console.log("您的瀏覽器支持WebSocket");
        let sid = getUrlParam("sid");
        //實現化WebSocket對象,指定要連接的服務器地址與端口  建立連接  
        if (!sid) {
            alert("sid不存在")
        };
        socket = new WebSocket("ws://localhost:9000/websocket/" + sid);  
        //打開事件  
        socket.onopen = function() {  
            console.log("Socket 已打開");  
            socket.send("這是來自客戶端的消息" + location.href + new Date());  
        };  
        //獲得消息事件  
        socket.onmessage = function(msg) {  
            console.log(msg.data);  
            //發現消息進入    開始處理前端觸發邏輯
        };  
        //關閉事件  
        socket.onclose = function() {  
            console.log("Socket已關閉");  
        };  
        //發生了錯誤事件  
        socket.onerror = function() {  
            alert("Socket發生了錯誤");  
            //此時可以嘗試刷新頁面
        }  
        //離開頁面時,關閉socket
        //jquery1.8中已經被廢棄,3.0中已經移除
        // $(window).unload(function(){  
        //     socket.close();  
        //});  
    }

    function getUrlParam(paraName) {
    var url = document.location.toString();
    var arrObj = url.split("?");

    if (arrObj.length > 1) {
      var arrPara = arrObj[1].split("&");
      var arr;

      for (var i = 0; i < arrPara.length; i++) {
        arr = arrPara[i].split("=");

        if (arr != null && arr[0] == paraName) {
          return arr[1];
        }
      }
      return "";
    }
    else {
      return "";
    }
  }
    </script> 
</body>
</html>

接下來就可以進行測試了:

首先,我們啓動在谷歌瀏覽器上啓動三個窗口,連接地址分別是file:///Users/xingcheng/Downloads/websocket.html?sid=21,file:///Users/xingcheng/Downloads/websocket.html?sid=22,file:///Users/xingcheng/Downloads/websocket.html?sid=23,代表3個不同的客戶端

來看服務器和客戶端的交互過程

服務器:

客戶端:

可以看到,連接的過程全部監聽到了

接下來模擬服務器的推送過程:羣聊,訪問連接:http://localhost:9000/webSocket/server/push/0

服務器:

客戶端:

最後,驗證單聊:22客戶端通過服務器給23發送消息,推送地址:http://localhost:9000/webSocket/server/push/22

服務器:

客戶端:

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章