SpringBoot整合Websocket综合示例项目

一、websocket介绍

WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC
6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket
API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

二、项目场景

项目场景是每个系统用户有多条流水线项目,每条流水线可以进行构建操作,后端模拟构建接口,实时修改当前构建流水线的状态,由初始化 -》运行中-》成功或失败,前端能够实时展示流水线状态。一般网站实现该功能都是采用轮询的方式,在一定的时间间隔内频繁获取流水线列表已此来达到"实时展示"的效果。

很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

三、项目结构

1、后端依赖

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

2、后端核心代码
客户端连接服务端时通过当前登录用户的ID和一个随机字符串生成唯一的窗口标识符,服务端websocket server通过该唯一标识维护一个标识-session的会话map,之后的消息分发都通过该map实现
BuildWebsocketServer.java【websocket服务端】

package com.swust.java.socket;

import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * @Author hqf
 * @Date Created in 2022/8/18 9:12
 * @Description 模拟构建时流水线状态变化
 * windowTag -> UserId + '-' + 'randomStr'
 */
@Component
@ServerEndpoint("/build/ws/{windowTag}")
public class BuildWebsocketServer {

    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;
    //以CopyOnWrite开头的集合类采用复制底层数组的方式来实现写操作。当线程对此类集合执行读取操作时,线程将会直接读取集合本身,无须加锁与阻塞。
    // 当线程对此类集合执行写入操作时,集合会在底层复制一份新的数组,接下来对新的数组执行写入操作。由于对集合的写入操作都是对数组的副本执行操作,因此它是线程安全的。
    //concurrent包的线程安全Set,用来存放每个客户端对应的BuildWebsocketServer对象。
    //虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。
    //  注:底下WebSocket是当前类名
    private static CopyOnWriteArraySet<BuildWebsocketServer> webSockets =new CopyOnWriteArraySet<>();
    // 用来存在线连接数
    private static Map<String, Session> sessionPool = new HashMap<>();

    /**
     * 链接成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam(value="windowTag") String windowTag) {
        try {
            this.session = session;
            webSockets.add(this);
            sessionPool.put(windowTag, session);
            System.out.println("【websocket消息】" + windowTag + "建立连接,总数为:" + webSockets.size());
        } catch (Exception e) {
        }
    }

    /**
     * 链接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        try {
            webSockets.remove(this);
            System.out.println("【websocket消息】连接断开,总数为:" + webSockets.size());
        } catch (Exception e) {
        }
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message
     */
    @OnMessage
    public void onMessage(String message) {
        System.out.println("【websocket消息】收到客户端消息:"+message);
    }

    /** 发送错误时的处理
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        System.out.println("【websocket消息】发送错误:"+error.getMessage());
        error.printStackTrace();
    }

    // 广播消息
    public void sendAllMessage(String message) {
        System.out.println("【websocket消息】广播消息:"+message);
        for(BuildWebsocketServer webSocket : webSockets) {
            try {
                if(webSocket.session.isOpen()) {
                    webSocket.session.getAsyncRemote().sendText(message);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    // 单点消息
    public void sendOneMessage(String windowTag, String message) {
        Session session = sessionPool.get(windowTag);
        if (session != null && session.isOpen()) {
            try {
                System.out.println("【websocket消息】向" + windowTag + "推送消息:"+message);
                session.getAsyncRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    // 多点消息
    public void sendMulMessage(List<String> userIds, String message) {
        for (String windowTag : sessionPool.keySet()) {
            String idStr = windowTag.split("-")[0];
            // 通过解析windowTag 得到用户ID 然后根据用户ID判断是否需要推送当前流程的构建状态信息
            if(userIds.contains(idStr)){
                sendOneMessage(windowTag,message);
            }
        }
    }
}

后端用户数据和用户流水线数据都是通过mock数据
MockCenter.java【Mock数据】

package com.swust.java.util;

import com.swust.java.entity.Pipeline;
import com.swust.java.entity.UserInfo;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @Author hqf
 * @Date Created in 2022/8/19 10:57
 * @Description
 */
@Component
public class MockCenter {

    // mock用户数据
    @Bean
    public Map<String, UserInfo> mockUser(){
        Map<String,UserInfo> map = new HashMap<>();
        map.put("zs",new UserInfo(1,"zs","123"));
        map.put("ls",new UserInfo(2,"ls","123"));
        map.put("ww",new UserInfo(3,"ww","123"));
        return map;
    }

    // mock用户-流水线关系
    @Bean
    public Map<String, List<Pipeline>> mockPipeline(){
        Pipeline p1 = new Pipeline(34,"标模测试",6);
        Pipeline p2 = new Pipeline(66,"测试环境变量",7);
        Pipeline p3 = new Pipeline(76,"单元测试",6);
        Pipeline p4 = new Pipeline(93,"集成测试",7);
        Pipeline p5 = new Pipeline(147,"界面测试",6);
        Map<String, List<Pipeline>> map = new HashMap<>();
        List<Pipeline> zsL = new ArrayList<>();
        zsL.add(p1);
        zsL.add(p2);
        zsL.add(p3);
        List<Pipeline> lsL = new ArrayList<>();
        lsL.add(p2);
        lsL.add(p3);
        lsL.add(p4);
        List<Pipeline> wwL = new ArrayList<>();
        wwL.add(p3);
        wwL.add(p4);
        wwL.add(p5);
        map.put("zs",zsL);
        map.put("ls",lsL);
        map.put("ww",wwL);
        return map;
    }
}

当用户对流水线开始构建时通过流水线ID查询该流水线参与的用户ID列表,然后根据该ID列表进行消息分发。之后通过模拟修改流水线状态来进行消息分发
BuildController.java【构建Controller】

package com.swust.java.controller;

import com.alibaba.fastjson.JSONObject;
import com.swust.java.entity.Pipeline;
import com.swust.java.entity.UserInfo;
import com.swust.java.socket.BuildWebsocketServer;
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.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * @Author hqf
 * @Date Created in 2022/8/18 8:48
 * @Description
 */
@RestController
@RequestMapping("/build")
public class BuildController {

    @Autowired
    private BuildWebsocketServer websocketServer;
    @Autowired
    private Map<String, UserInfo> mockUser;
    @Autowired
    private Map<String, List<Pipeline>> mockPipeline;
    /**
     * 开始构建
     * @param pipelineId 流水线ID
     * @return
     */
    @GetMapping("/{pipelineId}")
    public void startBuild(@PathVariable("pipelineId") int pipelineId){
        String message = String.format("流水线【%s】开始构建",pipelineId);
        System.out.println(message);
        // 查询该流程参与的用户ID列表 只给对应用户推送数据
        List<String> ids = new ArrayList<>();
        for (String key : mockPipeline.keySet()) {
            List<Pipeline> pipelines = mockPipeline.get(key);
            List<Integer> pipelineIds = new ArrayList<>();
            pipelines.forEach(pipeline -> {
                pipelineIds.add(pipeline.getId());
            });
            if(pipelineIds.contains(pipelineId)){
                UserInfo userInfo = mockUser.get(key);
                ids.add(String.valueOf(userInfo.getId()));
            }
        }
        System.out.println(String.format("流水线【%s】参与用户:%s",pipelineId,ids));
        // 初始化 -> 运行中 -> 成功或失败
        try {
            // 1、初始化
            websocketServer.sendMulMessage(ids,objStr(pipelineId,1));
            Thread.sleep(3 * 1000);
            // 2、运行中
            websocketServer.sendMulMessage(ids,objStr(pipelineId,2));
            Thread.sleep(5 * 1000);
            // 3、成功或失败
            websocketServer.sendMulMessage(ids,Math.random() * 10 > 5 ? objStr(pipelineId,6) : objStr(pipelineId,7));
        } catch (InterruptedException e) {
            e.printStackTrace();
            throw new RuntimeException("构建失败");
        }
    }

    private String objStr(int pipelineId,int status){
        JSONObject obj = new JSONObject();
        obj.put("pipelineId", pipelineId); //流水线ID
        obj.put("status", status);
        return obj.toJSONString();
    }
}

3、前端核心代码

<!DOCTYPE HTML>
<html>
<head>
    <title>流水线列表 - websocket</title>
    <link rel="stylesheet" href="./style.css">
</head>

<body>
    <div class="login-box">
        用户名:<input type="text" name="username">
        密码:<input type="password" name="password">
        <button onclick="login()">登录</button>
    </div>
    <div class="user-info"></div>
    <ul class="pipeline-list"></ul>
</body>

<script src="./jquery-3.4.1.js"></script>
<script src="./index.js"></script>
<script type="text/javascript">

    // 流水线状态映射map
    const statusMap = {
        1: "初始化",
        2: "运行中",
        3: "队列中",
        4: "取消",
        5: "停止",
        6: "成功",
        7: "失败",
        8: "超时"
    }
    // 后端服务地址
    const host = "localhost:8085";
    // 当前登录用户
    var user = {};
    // websocket连接标识
    var monitorOpen = false;

</script>
</html>

用户登录后清空用户登录表单并渲染用户所拥有的流水线列表

// 点击登录按钮
function login(){
    let loginBoxDom = $(".login-box");
    const username = loginBoxDom.find("[name='username']").val();
    const password = loginBoxDom.find("[name='password']").val();
    $.post({
        url:`http://${host}/login`,
        dataType:'json',
        data: {username,password},
        success: function(res) {
            if(res.status == 1){ // 登录成功
                loginBoxDom.remove();
                loginSuccess(res);
            }else{
                alert(res.data);
            }
        }
    });
}

// 登录成功
function loginSuccess(res){
    user = res.data.user;
    // 渲染用户信息
    $(".user-info").text(`当前登录用户:${user.username}`)
    // 根据结果渲染流水线列表
    let pipelineList = res.data.pipelines;
    // 窗口标识
    const windowTag = `${user.id}-${Math.random().toString(36).substr(2)}`;
    console.log("当前窗口标识",windowTag);
    renderList(pipelineList);
    statusMonitor(pipelineList,windowTag);
}

// 渲染页面数据
function renderList(pipelineList){
    var dom = $(".pipeline-list");
    dom.empty();
    pipelineList.forEach(item => {
        var template = $(`<li>
            ${item.name}
            【状态:<span class="font-color-${item.status}">${statusMap[item.status]}</span>】
            <button οnclick="startBuild(${item.id})">${item.status < 4 ? "构建中" : "开始构建"}</button>
        </li>`);
        dom.append(template);
    })
}

// 建立流水线状态监听的连接
function statusMonitor(pipelineList,windowTag){
    
    // 监听流水线状态
    var websocket = null;
    if ('WebSocket' in window) {
        websocket = new WebSocket(`ws://${host}/build/ws/${windowTag}`);
        console.log("尝试建立连接...");
    } else {
        alert('Not support websocket')
    }

    //连接发生错误的回调方法
    websocket.onerror = function() {
        alert("流水线状态监听器连接失败")
    };

    //连接成功建立的回调方法
    websocket.onopen = function(event) {
        console.log(`流水线状态监听器连接成功`);
        monitorOpen = true;
    }

    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function() {
        websocket.close();
    }

    //关闭连接
    function closeWebSocket() {
        websocket.close();
    }

    //接收到消息的回调方法
    websocket.onmessage = function(event) {
        const data = JSON.parse(event.data);
        console.log(`流水线状态监听器接收到的消息:`,data);
        pipelineList.forEach(item => {
            if(item.id == data.pipelineId){
                item.status = data.status;
            }
        })
        // 重新渲染页面
        renderList(pipelineList);
    }
}
// 点击开始构建按钮
function startBuild(pipelineId){
    if(monitorOpen){
        // 开始构建
        $.ajax({
            url:`http://${host}/build/${pipelineId}`,
            dataType:'json',
            type:'get'
        });
    }else{
        console.log("流水线状态监听器连接中...");
    }
}

四、项目演示

登录3个不同的用户,分别为zs、ls和ww并根据用户ID生成对应的窗口标识码

与服务端建立连接
在这里插入图片描述

WebsocketDemo

根据流水线参与的用户进行消息分发
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值