天天看點

Spring Boot 系列:Vue+Sping Boot +WebSocket實作前後端消息推送

目錄

1.需求

2.原理

2.1握手協定:

2.2優點

3.步驟

3.1後端springboot內建websocket

3.2建立配置類, 開啟WebSocket支援

3.3建立WebSocketServer服務端

3.4前端

3.5編寫通路接口模仿服務端消息推送

3.6服務端推送對象資料(WebSocket-發送對象-自定義Encoder)

3.7結果

4異常

1.需求

前後端實作資料實時傳輸,采用長連接配接的模式 websocket

前端vue項目,後端Springboot

2.原理

        WebSocket是一種在單個TCP連接配接上進行全雙工通信的協定。WebSocket通信協定于2011年被IETF定為标準RFC 6455,并由RFC7936補充規範。WebSocketAPI也被W3C定為标準。

        WebSocket使得用戶端和伺服器之間的資料交換變得更加簡單,允許服務端主動向用戶端推送資料。在WebSocket API中,浏覽器和伺服器隻需要完成一次握手,兩者之間就直接可以建立持久性的連接配接,并進行雙向資料傳輸。

2.1握手協定:

       WebSocket 是獨立的、建立在 TCP 上的協定。

       Websocket 通過HTTP/1.1 協定的101狀态碼進行握手。

      為了建立Websocket連接配接,需要通過浏覽器送出請求,之後伺服器進行回應,這個過程通常稱為“握手”(handshaking)。

2.2優點

優點:

  • 較少的控制開銷。在連接配接建立後,伺服器和用戶端之間交換資料時,用于協定控制的資料標頭部相對較小。在不包含擴充的情況下,對于伺服器到用戶端的内容,此頭部大小隻有2至10位元組(和資料包長度有關);對于用戶端到伺服器的内容,此頭部還需要加上額外的4位元組的掩碼。相對于HTTP請求每次都要攜帶完整的頭部,此項開銷顯著減少了。
  • 更強的實時性。由于協定是全雙工的,是以伺服器可以随時主動給用戶端下發資料。相對于HTTP請求需要等待用戶端發起請求服務端才能響應,延遲明顯更少;即使是和Comet等類似的長輪詢比較,其也能在短時間内更多次地傳遞資料。
  • 保持連接配接狀态。與HTTP不同的是,Websocket需要先建立連接配接,這就使得其成為一種有狀态的協定,之後通信時可以省略部分狀态資訊。而HTTP請求可能需要在每個請求都攜帶狀态資訊(如身份認證等)。
  • 更好的二進制支援。Websocket定義了二進制幀,相對HTTP,可以更輕松地處理二進制内容。
  • 可以支援擴充。Websocket定義了擴充,使用者可以擴充協定、實作部分自定義的子協定。如部分浏覽器支援壓縮等。
  • 更好的壓縮效果。相對于HTTP壓縮,Websocket在适當的擴充支援下,可以沿用之前内容的上下文,在傳遞類似的資料時,可以顯著地提高壓縮率。

3.步驟

3.1後端springboot內建websocket

gradle中內建websocket

implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-websocket'

           

3.2建立配置類, 開啟WebSocket支援

WebSocketConfig.java

package com.trgis.config;

import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * 開啟WebSocket支援
 **/
@Configuration
@ConditionalOnWebApplication
public class WebSocketConfig  {

    //使用boot内置tomcat時需要注入此bean
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

           

3.3建立WebSocketServer服務端

WebSocket.java

@ServerEndpoint("/websocket")
@Component
@Slf4j
public class WebSocket {
    //與某個用戶端的連接配接會話,需要通過它來給用戶端發送資料
    private Session session;
 
    //concurrent包的線程安全Set,用來存放每個用戶端對應的WebSocket對象。
    private static  CopyOnWriteArraySet<WebSocket> webSocketSet=new CopyOnWriteArraySet<>();
 
    /**
     *  建立連接配接成功
     * @param session
     */
    @OnOpen
    public void onOpen(Session session){
        this.session=session;
        webSocketSet.add(this);
      log.info("【websocket消息】 有新的連接配接,總數{}",webSocketSet.size());
    }
 
    /**
     * 連接配接關閉
     */
    @OnClose
    public void onClose(){
        this.session=session;
        webSocketSet.remove(this);
        log.info("【websocket消息】 連接配接斷開,總數{}",webSocketSet.size());
    }
 
    /**
     * 接收用戶端消息
     * @param message
     */
    @OnMessage
    public void onMessage(String message){
        log.info("【websocket消息】 收到用戶端發來的消息:{}",message);
    }
 
    /**
     * 發送消息
     * @param message
     */
    public void sendMessage(String message){
        log.info("【websocket消息】 發送消息:{}",message);
        for (WebSocket webSocket:webSocketSet){
            try {
                webSocket.session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
           

@ServerEndpoint 注解是一個類層次的注解,它的功能主要是将目前的類定義成一個websocket伺服器端。注解的值将被用于監聽使用者連接配接的終端通路URL位址。

onOpen 和 onClose 方法分别被@OnOpen和@OnClose 所注解。他們定義了當一個新使用者連接配接和斷開的時候所調用的方法。

onMessage 方法被@OnMessage所注解。這個注解定義了當伺服器接收到用戶端發送的消息時所調用的方法。

用onMessage()接收前端使用者發來的消息。

用sendMessage()給前端使用者發送消息。

注意@ServerEndpoint("/websocket")是你連接配接時的url,如果後端為192.168.1.88:9997,那麼前端websocket連接配接url寫為: ws:http://192.168.1.88:9997/websocket

3.4前端

<template>
    <div class="Task">
        <div class="" style="height: 100px;width: 100px;color: #fff;" >
            <button @click="open()">打開連接配接</button>
            <button @click="close()">關閉連接配接</button>
            <input type="text" id="name"  v-model="message"/><button @click="send()">發送消息</button>
        </div>
      

    </div>
</template>

<script>   
    import {baseURL} from 'src/const/config'
    export default {
        name: "Task",
        data(){
            return{
                baseURL,
                websock: null,
                message: "",
             
            }
        },
        mounted() {
            this.initWebSocket()
              
        },
        methods:{
            initWebSocket(){ //初始化weosocket

                const wsuri = 'ws://192.168.1.88:9997/websocket';//ws位址
                this.websock = new WebSocket(wsuri);
                this.websock.onopen = this.websocketonopen;

                this.websock.onerror = this.websocketonerror;

                this.websock.onmessage = this.websocketonmessage;
                this.websock.onclose = this.websocketclose;
            },

            websocketonopen() {
                console.log("WebSocket連接配接成功");
                websocket.send(""WebSocket連接配接成功");//發送消息
            },
            websocketonerror(e) { //錯誤
                console.log("WebSocket連接配接發生錯誤");
            },
            websocketonmessage(e){ //資料接收
                const redata = JSON.parse(e.data);//接收對象的

                //注意:長連接配接我們是背景直接1秒推送一條資料,
                //但是點選某個清單時,會發送給背景一個辨別,背景根據此辨別傳回相對應的資料,
                //這個時候資料就隻能從一個出口出,是以讓背景加了一個鍵,例如鍵為1時,是每隔1秒推送的資料,為2時是發送辨別後再推送的資料,以作區分
                console.log(redata.total,1111);
            },

            websocketclose(e){ //關閉
                console.log("connection closed");
            },
            close(){
                this.websock.close();
            },
            send(){
                if(this.websock && this.websock.readyState==1){
                    this.websock.send("S1");
                }else{
                    console.log("連接配接已關閉")
                }

            },
            open(){
                this.initWebSocket();
            }

        },
        destroyed: function() {
            //頁面銷毀時關閉長連接配接
            this.websocketclose();
        },
    }
</script>

<style scoped >
   
</style>
           

3.5編寫通路接口模仿服務端消息推送

/**
     * 發送場景模拟
     * @param msg
     * @return
     */
    @GetMapping("/send")
    @ResponseBody
    public String sendMessage(String msg) {
        //如果通路的位址中msg參數不為空值,發送msg的值給前端
        if (!StringUtils.isEmpty(msg)) {
            webSocket.sendMessage(msg);
            return "服務端發送消息:" + msg;
        }
        return "服務端未發送消息:" + msg;
    }
           

3.6服務端推送對象資料(WebSocket-發送對象-自定義Encoder)

Websocket發送對象,通過Encoder 自定義規則(轉換為JSON字元串),前端收到後再轉換為JSON對象

3.6.1自定義Encoder

package com.trgis.config;
import com.alibaba.fastjson.JSON;
import com.trgis.vo.SocketVO;

import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;
import java.util.Map;

public class WebSocketCustomEncoding implements Encoder.Text<SocketVO> {
    @Override
    public String encode(SocketVO vo) {
        assert vo!=null;
        return JSON.toJSONString(vo);
    }

    @Override
    public void init(EndpointConfig endpointConfig) {

    }
    @Override
    public void destroy() {

    }

}
           

3.6.2 SocketVo

package com.trgis.vo;

import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.ArrayList;

/**
 * @PackageName: com.trgis.vo
 * @ClassName: socketVO
 * @Author: zoe
 * @Date: 2021/4/9 0013 11:02
 * @Description:  socket對象 傳給前端
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SocketVO {
    @ApiModelProperty("名稱")
    private String name;

    @ApiModelProperty("總計條數")
    private Integer total;

    @ApiModelProperty("x")
    private  ArrayList<String>  x;

    @ApiModelProperty("y")
    private ArrayList<String>  y;
}
           

3.6.3 Websocket發送自定義對象

在@ServerEndpoint 指定endocers

修改剛剛的webSocket.java中的WebSocket中的@ServerEndpoint 并增加sendMessage(SocketVO vo)方法 代表傳回對象

@ServerEndpoint(value = "/websocket",encoders = WebSocketCustomEncoding.class)
@Component
@Slf4j
public class WebSocket {

    /**
     * 新增 發送消息 對象模式
     * @param vo
     */
    public void sendMessage(SocketVO vo){
        log.info("【websocket消息】 發送消息:{}",vo);
        for (WebSocket webSocket:webSocketSet){
            try {
                webSocket.session.getBasicRemote().sendObject(vo);
            } catch (IOException | EncodeException e) {
                e.printStackTrace();
            }
        }
    }
}
           

如果不在 @ServerEndpoint 指定endocers,直接通過sendObject(Object o)發送對象,

會報javax.websocket.EncodeException: No encoder specified for object of class xxxx異常

3.6.4Controll中調用修改

/**
     * 發送場景模拟
     * @param msg
     * @return
     */
    @GetMapping("/send")
    @ResponseBody
    public String sendMessage(String msg) {
        //如果通路的位址中msg參數不為空值,發送msg的值給前端
        if (!StringUtils.isEmpty(msg)) {
            //webSocket.sendMessage(msg);
          SocketVO vo = new SocketVO();
          vo.setX("x");
          vo.setY("y");
          vo.setName(msg);
          vo.setTotal(10);
          socket.sendMessage(vo);
            return "服務端發送消息:" + msg;
        }
        

        return "服務端未發送消息:" + msg;
    }
           

3.7結果

Spring Boot 系列:Vue+Sping Boot +WebSocket實作前後端消息推送
Spring Boot 系列:Vue+Sping Boot +WebSocket實作前後端消息推送

4異常

可能遇到的錯誤及注意事項:

檢查new WebSocket("ws://localhost:9997/websocket");的路徑是否正确、是否以ws://開頭,端口是否對應正确

url是否和後端配置的一緻,單詞是否拼寫正确,導包是否正确

前後端端口号是否重複占用

gradle引包是否正确

tomcat是否使用7以上版本,建議使用tomcat8以及較新的springboot版本

後端配置檔案是否注入spring

是否設定了攔截器