Lightweight front-end separation simple web version chat (Spring Boot+WebSocket+Vue) Demo implementation

What is WebSocket?

In the HTTP protocol, all requests are initiated by the client and the server responds. The server cannot push messages to the client. However, in some applications that require instant communication, the server is inevitably required to send messages to the client. Push message on the end, the solution:

  1. Polling : The client continuously sends requests to the server at a fixed time interval to check whether the server has the latest data. If the server has the latest data, it will be returned to the client. If the server does not have the latest data, it will be returned. An empty JSON or XML document.  The client has to create a new HTTP request every time, and the server has to deal with a large number of invalid requests. In high concurrency scenarios, it will seriously slow down the operation efficiency of the server. At the same time, the resources of the server are greatly wasted, so this method is not feasible take.
  2. Long polling : An upgraded version of traditional polling , which simulates the server-side push (Server Push) function through delayed response. That is, Comet (server push) will first put the response in a suspended state, and then return the response when there is a content update on the server side.  This method can save network resources and server resources to a certain extent, but there are also some problems.
    • If the browser has new data to send before the server responds, it can only create a new concurrent request, or try to interrupt the current request before creating a new request.
    • Both TCP and HπP specifications have a connection timeout, so the so-called long polling cannot be continuous. The connection between the server and the client needs to be connected and closed on a regular basis.
  3.  Applet and Flash : The applet creates a Socket connection for two-way communication. This connection method eliminates many restrictions in the HTTP protocol. When the server sends a message to the client, the developer can call the JavaScript function in the Applet or Flash to display the data on the page. When the browser has data to send to the server The same is true at the time, which is delivered through Applet or Flash. 

WebSocket protocol:

       The duplex communication standard between the Web browser and the Web server. Among them, the WebSocket protocol is set as the standard by IFTF, and WebSocketAPI has the W3C positioning standard, which mainly solves the problems caused by the defects of the XMLHttpRequest in Ajax and Comet. It is a protocol for full-duplex communication on a single TCP connection ,

       Once a WebSocket protocol communication connection is established between the Web server and the client, the subsequent protocols rely on special protocols. During the communication process, data in any format such as JSON, XML, HTML, or pictures can be sent to each other. Since it is based on the HTTP-based protocol, that is, the initiator is still the client. Once the WebSocket communication connection is established, either the server or the client can directly send messages to the other party.

Features :

  • Push function : It supports the push function of pushing data from the server to the client.
  • Reduce the amount of communication : as long as a WebSocket connection is established, it is hoped that the connection will remain connected
  • Handshake request : Before realizing webSocket communication, you need to complete a handshake action, which requires HTTP. There is a Connection: Upgrade field in the request header, indicating that the client wants to upgrade the protocol, and there is also an Upgrade header field (Upgrade :websocket) to inform the server that the communication protocol has changed to achieve the purpose of handshake. The Sec-WebSocket field records the essential key values ​​during the handshake process. Sec-WebSocket-Protocol: The sub-protocol used is recorded in the field. The sub-protocol case webSocket protocol standard defines those connection names when the connection is used separately.
  • Handshake response : The field value of Sec-WebSocket-Accept is generated from the field value of Sec-WebSocket-Key in the handshake request. After the successful handshake establishes the WebSocket connection, HTTP data frames are not used for communication, but WebSocket independent data is used. frame.
  • When using WebSocket, you need to create a connection first, which makes WebSocket a stateful protocol . When the WebSocket connection is closed, a special closing message will be sent. 
  • WebSocket uses the HTTP protocol for handshake, so it can be naturally integrated into web browsers and HTTP servers without additional cost. 
  • The WebSocket connection is created on port 80 (WS) or 443 (wss) , which is the same port used by HTTP, so that basically all firewalls will not block the WebSocket connection.
  • WebSocket supports cross-domain, which can avoid the limitations of Ajax. 

Handshake request:

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

Handshake response:
 

HTTP/1.1 101 Switching Protocols
Upgrade:websocket
Connection:Upgrade
Sec-WebSocket-Accept:上pPLMBiTXaQ9kyGZZ=
Sec-WebSocket-Protocol:chet

Spring Boot integrates WebSocket

Spring Boot provides very friendly support for WebSocket, which can facilitate developers to quickly integrate WebSocket functions in their projects to achieve single chat or group chat.

You only need to introduce dependencies.


Demo

Source code:

Backend: https://github.com/LIRUILONGS/demo.git

Front end: https://github.com/LIRUILONGS/Demo_UI.git

Technology stack:

Backend: springboot+H2+general Mapper+Spring security+WebSocket

Front end: Socket+Vue(Vuex...)+Element

The implementation here is very simple and just a single chat mode.

GIF:

Icon:

 

Part of the code:

package com.liruilong.demo.controller;

import com.liruilong.model.ChatMsg;
import com.liruilong.model.Hr;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.RestController;


import java.util.Date;
import java.util.logging.Logger;

/**
 * @Description :
 * @Author: Liruilong
 * @Date: 2020/2/11 14:55
 */
@RestController
public class WsController {
    
    Logger logger = Logger.getLogger("com.liruilong.demo.controller.WsController");
    
    @Autowired
    SimpMessagingTemplate simpMessagingTemplate;
    
    
  /**
   * @param authentication
   * @param chatMsg
   * @return 
   * @description 点对点。这个为了演示方遍,把请求对象放request里面了。request用户获取当前的用户信息。
   *              chatMag为客户端发送来的消息。
   * @author Liruilong
   * @date  2020年05月09日  20:05:49
   **/
  
    // 接受消息
    @MessageMapping("/chat")
    public void handleMsg(Authentication authentication, ChatMsg chatMsg) {
        Hr hr = (Hr) authentication.getPrincipal();
        //发送点
        chatMsg.setFrom(hr.getUsername());
        //发送点名称
        chatMsg.setFromNickname(hr.getName());
        // 发送日期
        chatMsg.setDate(new Date());
        logger.info("发送的消息实体为:"+chatMsg.toString());
        // 群发消息依然使用@SendTo 注解来实现, 点对点的消息发送则使用 SimpMessagingTemplate 来实现。
        // 对消息路径做了处理默认添加/user
        simpMessagingTemplate.convertAndSendToUser(chatMsg.getTo(), "/queue/chat", chatMsg);
    }

}
package com.liruilong.demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

/**
 * @Description : 自定义类 WebSocketConfig 继承自 WebSocketMessageBrokerConfigurer 进行 WebSocket 配置
 * 通过@EnableWebSocketMessageBroker注解开启 WebSocket 消息代理
 * @Author: Liruilong
 * @Date: 2020/2/11 14:45
 */
@Configuration
// 开启WebSocket消息代理
@EnableWebSocketMessageBroker
public class WebSocketConfig  implements WebSocketMessageBrokerConfigurer {


    /**
    * @Author Liruilong
    * @Description   建立链接
    * @Date 14:50 2020/2/11
    * @Param [registry]
    * @return void
    **/

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
       //定义一个前缀为“/ws/ep”的 endPoint,并开启 sockjs 支持,
        registry.addEndpoint("/ws/ep").setAllowedOrigins("*").withSockJS();

    }
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        /*
        消息代理的前缀,即如果消息代理的前缀为指定的字符,就会将消息转发给消息代理broker
        在由消息代理将消息广播给当前的连接的客户端。
        */
        registry.enableSimpleBroker("/queue");
        /*
       前缀为“/app”的 destination 可以通过@MessageMapping 注解的方法处理,
       而其他 destination (例如“/topic”“/queue”)将被直接交给 broker 处理。
        */
        registry.setApplicationDestinationPrefixes("/ws");
    }
}

vue part:

import Vue from 'vue'
import Vuex from 'vuex'
import {Notification} from 'element-ui';
import {getRequest} from "../utils/api";
import SockJS from 'sockjs-client';
import Stomp from 'stompjs';

Vue.use(Vuex)


const store = new Vuex.Store({
    state: {
        routes: [],
        sessions: {},
        hrs: [],
        cuuentHr: [],
        currentSession: '',
        currentHr: JSON.parse(window.sessionStorage.getItem("user")),
        filterKey: '',
        stomp: null,
        isDot: {}
    },
    // 方法提交,即定义需要提交的方法
    mutations: {
        //当前用户
        INIT_CURRENTHR(state, hr) {
            state.currentHr = hr;
        },
        //新建聊天对象。
        changeCurrentSession(state, currentSession) {
            console.log("新的发送对象为:" + JSON.stringify(currentSession));
            //添加到state
            Vue.set(state.isDot, state.currentHr.username + '#' + currentSession.username, false);
            // 更新聊天对象
            state.currentSession = currentSession;
        },
        // 构建前端的消息实体
        addMessage(state, msg) {
            let mss = state.sessions[state.currentHr.username + '#' + msg.to];
            // 使用 Vue.set(object, key, value) 方法将响应属性添加到嵌套的对象上:
            if (!mss) {
              //  state.sessions[state.currentHr.username + '#' + msg.to] = [];
                Vue.set(state.sessions, state.currentHr.username + '#' + msg.to, []);
            }
            state.sessions[state.currentHr.username + '#' + msg.to].push({
                content: msg.content,
                date: new Date(),
                self: !msg.notSelf
            })
        },
        //浏览器本地的历史聊天记录可以在这里完成
        INIT_DATA(state) {
            //浏览器本地的历史聊天记录可以在这里完成
            let data = localStorage.getItem('vue-chat-session');
            if (data) {
                state.sessions = JSON.parse(data);
            }
        },
        //初始化当前用户
        INIT_HR(state, data) {
            state.hrs = data;

        }
    },
    //做异步操作,同时提交mutatons。
    actions: {
        //建立Socket连接,服务端消息订阅。
        connect(context) {
            console.log("开始建立Socket连接");
            context.state.stomp = Stomp.over(new SockJS('/ws/ep'));
            console.log("建立stomp对象")
            //建立连接,执行成功和失败的回调
            context.state.stomp.connect({},
                () => {
                    // 调用 STOMP 中的 subscribe 方法订阅服务端发送回来的消息,并将服务端发送来的消息展示出来
                    context.state.stomp.subscribe('/user/queue/chat', msg => {
                        // msg.body 固定写法
                        let receiveMsg = JSON.parse(msg.body);
                        if (!context.state.currentSession || receiveMsg.from != context.state.currentSession.username) {
                            Notification.info({
                                title: '【' + receiveMsg.fromNickname + '】发来一条消息',
                                message: receiveMsg.content.length > 10 ? receiveMsg.content.substr(0, 10) : receiveMsg.content,
                                position: 'bottom-right'
                            })
                            // 接受前端的消息实体。
                            Vue.set(context.state.isDot, context.state.currentHr.username + '#' + receiveMsg.from, true);
                        }
                        // 是否新发标识
                        receiveMsg.notSelf = true;
                        //发送人
                        receiveMsg.to = receiveMsg.from;
                       //提交
                        context.commit('addMessage', receiveMsg);
                    })
                }, () => {
                    Notification.info({
                        title: "系统讯息",
                        message: "服务器连接失败",

                    })
                })
        },
        initData(context) {
            //加载历史聊天记录
            context.commit('INIT_DATA')
            getRequest("/chat/hrs").then(resp => {
                if (resp) {
                    //获取所以的用户
                    context.commit('INIT_HR', resp);
                }
            })
        }
    }
})

store.watch(function (state) {
    return state.sessions
}, function (val) {
    localStorage.setItem('vue-chat-session', JSON.stringify(val));
}, {
    deep: true/*这个貌似是开启watch监测的判断,官方说明也比较模糊*/
})


export default store;

 

————————————————

references 

"Illustrated HTTP" Chapter 9 HTTP-based functional additional protocol

"SpringbootBoot+Vue Full Stack Development Actual Combat" Chapter 11 Spring Boot integrates WebSocket
 

Guess you like

Origin blog.csdn.net/sanhewuyang/article/details/104262273