如何用WebSocket完成实时消息,实时进度条

发布于:2025-06-20 ⋅ 阅读:(20) ⋅ 点赞:(0)

可能大家都知道WebSocket是个什么东西了,像我们平常用到的聊天的功能,大都是通过WebSocket去建立连接,然后去通信。

而我们在企业开发的过程中很多时候都会遇到这种场景,例如一个C端系统的消息通知,任务进度条等待页面等等。可能大多数企业是通过前端定时去http协议去查询进度(例如我司的某些功能)。不可否认的是,这个方法确实简单快速。

但是如果前端频繁去调用我们的api接口会一直占用数据库连接之外,对我们的性能是个很大的挑战(如果你要查询的表里面有很多的数据的话),所以,我们这篇文章来实战一个WebSocket服务器主动去推送消息的这个功能出来。

img

我们最后做出的效果如下(用户进入个人中心页面获取当前的待办消息,如果有新加入的待办消息,则待办消息数量加一,可以点击通知icon查询具体的待办列表,todo:可以查看当前的待办进度条):

img

可以看到我们的页面在初始化的时候只调用了一次websocket的接口,之后有新的待办插入进来的时候,我们可以看到消息的红点已经自动加一了。

消息小红点

第一步,搭建maven环境

  		<dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
		
 	 <!--websocket-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
            <version>2.1.6.RELEASE</version>
        </dependency>

        <!--websocket-->

这里加两个依赖,第一个依赖是mysql的orm框架mp。因为我们这里需要用到待办列表,需要模拟有数据插入。

第二步,mysql表数据

CREATE TABLE `t_todo` (
  `id` bigint(50) NOT NULL AUTO_INCREMENT,
  `user_name` varchar(255) DEFAULT NULL COMMENT '用户名称',
  `name` varchar(255) DEFAULT NULL COMMENT '待办标题',
  PRIMARY KEY (`id`)
) COMMENT='待办表';

CREATE TABLE `t_todo_attr` (
  `id` bigint(50) NOT NULL AUTO_INCREMENT,
  `todo_id` bigint(50) DEFAULT NULL COMMENT '主表id',
  `status` int(4) DEFAULT NULL COMMENT '状态 1代表已完成',
  PRIMARY KEY (`id`)
) COMMENT='待办步骤表';

这两张表代表了一个主表一个子表,他们的关系是一对多。其中主表模拟一条待办数据,而子表存储一条待办数据中的子步骤,用status字段为1记录已经完成。

第三步,编写websocket接口

我们首先要初始化两张t_todo表对应的mybatisplus的实体类,获取在当前表中对应用户的待办条数:获取通知数量的sql

 LambdaQueryWrapper<Todo> todoLambdaQueryWrapper = new LambdaQueryWrapper<>();
        todoLambdaQueryWrapper.eq(Todo::getUserName, userName);
        TodoService todoService = SpringContextUtil.getBean(TodoService.class);
        int count = todoService.count(todoLambdaQueryWrapper);

这个相信大家都非常熟悉了吧。接下来我们需要通过这么一个操作使得后面的websocket生效:

package com.wangfugui.apprentice.config;

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

/**
 * WebScoket配置处理器
 */
@Configuration
public class WebSocketConfig {
	 /**
     * ServerEndpointExporter 作用
     *
     * 这个Bean会自动注册使用@ServerEndpoint注解声明的websocket endpoint
     *
     * @return
     */
	@Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

ServerEndpointExporter是Spring提供的一个用于自动注册WebSocket端点的类。当应用中使用了@ServerEndpoint注解来定义WebSocket端点时,我们则需要添加这个Bean,以便Spring能够自动发现并注册这些端点。

因为我们把ServerEndpointExporter注册成了一个bean,所以我们可以使用一个注解 @ServerEndpoint来定义一个 WebSocket 端点。WebSocket 端点是 WebSocket 连接的入口点,客户端通过这个端点与服务器建立连接。大家就可以理解为web应用中controller类中的RequestMapping注解就行了。

我们就像定义RequestMapping一样定义一个@ServerEndpoint:

package com.wangfugui.apprentice.service;


import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.wangfugui.apprentice.common.util.SpringContextUtil;
import com.wangfugui.apprentice.domain.Todo;
import org.springframework.stereotype.Component;

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 java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

@ServerEndpoint("/webSocket/{username}")
@Component
public class WebSocketTodoServer {

    //建立连接成功调用
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "username") String userName){
        sessionPools.put(userName, session);
        addOnlineCount();
        System.out.println(userName + "加入webSocket!当前人数为" + onlineNum);
        LambdaQueryWrapper<Todo> todoLambdaQueryWrapper = new LambdaQueryWrapper<>();
        todoLambdaQueryWrapper.eq(Todo::getUserName, userName);
        TodoService todoService = SpringContextUtil.getBean(TodoService.class);
        int count = todoService.count(todoLambdaQueryWrapper);
        sendMessage(session, String.valueOf(count));
        // 广播上线消息
    }

    //关闭连接时调用
    @OnClose
    public void onClose(@PathParam(value = "username") String userName){
        sessionPools.remove(userName);
        subOnlineCount();
        System.out.println(userName + "断开webSocket连接!当前人数为" + onlineNum);

    }

    //收到客户端信息后,根据接收人的username把消息推下去或者群发
    // to=-1群发消息
    @OnMessage
    public void onMessage(String message) throws IOException{
        System.out.println("server get" + message);

    }

    //错误时调用
    @OnError
    public void onError(Session session, Throwable throwable){
        System.out.println("发生错误");
        throwable.printStackTrace();
    }


}

大家注意一下这里有四个方法,@OnOpen, @OnClose, @OnMessage, 和 @OnError 是WebSocket的注解,分别对应连接建立、连接关闭、接收消息和发生错误时的回调方法。

  • onOpen 方法在客户端建立WebSocket连接时被调用
  • onClose 方法在客户端关闭WebSocket连接时被调用
  • onMessage 方法在接收到客户端发送的消息时被调用
  • onError 方法在WebSocket连接发生错误时被调用。

其中我们在onOpen方法中因为要在连接的时候就放回表中 张三对应的待办的数量,所以我们这里在连接的时候就给他返回数据。这里有三个注意的地方:

第一点是如果我们直接在代码里面这样注入TodoService是注入不进来的,因为在Spring Boot中,@ServerEndpoint注解的类默认不会被Spring容器管理,因此即使你使用了@Component注解,Spring也不会自动装配依赖。

@Autowired
TodoService todoService;

第二个点是如果我们需要像谁发送信息,则需要使用

  session.getBasicRemote().sendText(message);

这个方法,而这个session就是我们的会话,如果不了解session的话可以去查看一下作者往期的文章:

为什么服务端会知道我是谁-Session解析

而我们的session是存储在内存中的,所以我们使用这样的方式去存储每个websocket连接对应的session:

    //concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象。
    private static ConcurrentHashMap<String, Session> sessionPools = new ConcurrentHashMap<>();

    //发送消息
    public  static void sendMessage(Session session, String message) {
        if(session != null){
            synchronized (session) {
                System.out.println("发送数据:" + message);
                try {
                    session.getBasicRemote().sendText(message);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

并且在他上线的时候存储起来:

   sessionPools.put(userName, session);

第三点就是我们查看onopen方法的时候可以看到我们的形参:

    @OnOpen
    public void onOpen(Session session, @PathParam(value = "username") String userName){}

其中 @PathParam注解大家可以理解为 @PathVariable注解,里面value对应的 username值则是绑定了上面我们 @ServerEndpoint(“/webSocket/{username}”) 中里面的username。前面还有个session,我们固定这么写就行,会自动给我们塞入值的。

第四步,调用websocket接口

我们都知道如果你需要调用http接口,即一般我们后端暴露给前端的接口,都是这样调用的:

image-20250108145054501

以**http://或者https://**开头接ip加端口加我们的接口地址,最后以get方法,post方法等请求方式去调用我们的后端接口。而websocket和http不同的是,我们通过 ws://(ws:websocket的缩写)开头的接口路径去请求我们后端的接口,例如在我们案例中的ws接口则请求路径为:

image-20250108145444513

而我们连接之后,就会进入到我们被 OnOpen 注释的方法里面了,而进入了这个方法里面不需要执行完成这个websocket连接已经建立了。

image-20250108145716243

而我们在方法中查出了张三对应的待办数量,通过 sendMessage方法往当前session里面发送了一条消息,所以我们继续把这个方法放开,再去看我们的接口成功返回了张三对应的待办条数:

image-20250108150051959

第五步,绘制前端页面

那么我们如何在页面上面显示这个待办条数呢?我们可以写一个简单的html页面去连接我们的ws接口:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>消息通知与待办列表</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            display: flex;
            flex-direction: column;
            align-items: center;
            margin: 20px;
        }
        .message-container {
            display: flex;
            align-items: center;
            gap: 10px;
            cursor: pointer; /* 指示这是一个可点击元素 */
        }
        .bell-icon {
            width: 50px;
            height: 50px;
            background-image: url('bell-icon.png');
            background-size: cover;
            background-repeat: no-repeat;
        }
        .message-count {
            background-color: #ff4500;
            color: white;
            border-radius: 20px;
            padding: 10px 20px;
            font-weight: bold;
        }
        .todo-section {
            margin-top: 20px;
            display: none; /* 默认隐藏 */
        }
        .todo-list {
            list-style-type: none;
            padding: 0;
        }
        .todo-item {
            background-color: #f9f9f9;
            padding: 10px;
            margin-bottom: 5px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        .todo-item h3 {
            margin: 0;
            font-size: 18px;
        }
        .todo-item p {
            margin: 5px 0 0;
            color: #666;
        }
    </style>
</head>
<body>

<div class="message-container" id="messageContainer">
    <div class="bell-icon"></div>
    <span>消息</span>
    <span class="message-count">0</span>
</div>

<div class="todo-section" id="todoSection">
    <h1>待办列表</h1>
    <ul class="todo-list" id="todoList"></ul>
</div>

<script>
    // WebSocket 配置
    const socket = new WebSocket('ws://localhost:8077/webSocket/张三');

    // 连接打开时执行的回调函数
    socket.onopen = function(event) {
        console.log('WebSocket 连接已打开');
    };

    // 接收到消息时执行的回调函数
    socket.onmessage = function(event) {
        const messageCount = event.data;
        document.querySelector('.message-count').textContent = messageCount;
    };

    // 连接关闭时执行的回调函数
    socket.onclose = function(event) {
        console.log('WebSocket 连接已关闭');
    };

    // 连接发生错误时执行的回调函数
    socket.onerror = function(error) {
        console.error('WebSocket 错误:', error);
    };

    // 获取按钮元素
    const fetchButton = document.getElementById('fetchButton');
    const todoListElement = document.getElementById('todoList');
    const todoSection = document.getElementById('todoSection');
    const messageContainer = document.getElementById('messageContainer');

    // 点击通知图标时显示或隐藏待办列表
    messageContainer.addEventListener('click', () => {
        if (todoSection.style.display === 'none' || todoSection.style.display === '') {
            fetchTodoItems(); // 在显示之前获取待办事项
            todoSection.style.display = 'block';
        } else {
            todoSection.style.display = 'none';
        }
    });

 

    // 如果需要,在页面加载时自动获取待办事项(可选)
    // window.onload = fetchTodoItems;
</script>

</body>
</html>

效果不错,成功显示了待办的消息数量:

image-20250108150511227

第六步,插入一条新的待办

建立了websocket连接之后,我们的需求是如果用户张三有新的待办加入进来,我们则需要动态的显示最新的待办数量。

写http接口我们小学二年级就学过怎么写了,我们写一个插入待办的http接口:

package com.wangfugui.apprentice.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.wangfugui.apprentice.common.util.ResponseUtils;
import com.wangfugui.apprentice.domain.Todo;
import com.wangfugui.apprentice.domain.TodoAttr;
import com.wangfugui.apprentice.service.TodoAttrService;
import com.wangfugui.apprentice.service.TodoService;
import com.wangfugui.apprentice.service.WebSocketTodoAttrServer;
import com.wangfugui.apprentice.service.WebSocketTodoServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author: masiyi
 * @Date: 2025/1/2
 * @Describe:
 */
@RestController
@RequestMapping("/todo")
public class TodoController {

    @Autowired
    private TodoService todoService;

    @Autowired
    private WebSocketTodoServer webSocketTodoServer;


    @PostMapping("/insert")
    public ResponseUtils insert(@RequestParam("todoName") String todoName,
                                @RequestParam("userName") String userName) {
        Todo todo = new Todo();
        todo.setName(todoName);
        todo.setUserName(userName);
        todoService.save(todo);
        LambdaQueryWrapper<Todo> todoLambdaQueryWrapper = new LambdaQueryWrapper<>();
        todoLambdaQueryWrapper.eq(Todo::getUserName, userName);
        int count = todoService.count(todoLambdaQueryWrapper);
        webSocketTodoServer.sendInfo(userName, String.valueOf(count));
        return ResponseUtils.success(todoName);
    }

   
}


这个时候我们通过该接口模拟给张三添加一条新的待办数据的同时看一下我们的前端页面会发生什么变化:

msedge_IfrUt7zi8N

可以看到成功调用,我们这个接口作用如下:接收一个HTTP POST请求,该请求包含了待办事项的名称和用户名。方法将新的待办事项保存到数据库,并统计该用户的待办事项总数,然后通过WebSocket服务器向用户发送这个总数。

而我们的数据库里面也成功地插入了新数据:

image-20250108151814216

但是如果我们不用websocket这个技术实现,我们就需要提供一个http接口给前端,这个接口的作用是计算张三对应的待办数量,而前端则需要写一个定时器去循环调用,例如一秒调用一次。不仅仅消耗了很多的网络资源,还会给用户造成卡顿的感觉,最后也会因为前端的频繁请求对我们的后端数据库造成巨大的压力。

而我们使用了websocket这个技术之后,我们只建立了一次网络连接,后续如果有新的待办数据进来,我们服务器主动发送一条最新的待办数量给前端即可。

img

图片来源:CSDN bug捕手

第七步,获取待办列表

这一步,因为我们是待办存储在数据库中的,而且用户只是点击通知才会显示待办列表,所以这里我们不需要使用websocket了,我们使用我们小学二年级学到的消息写一个http接口查询我们的待办列表即可:

    @GetMapping("/list")
    public ResponseUtils list(@RequestParam("userName") String userName) {
        LambdaQueryWrapper<Todo> todoLambdaQueryWrapper = new LambdaQueryWrapper<>();
        todoLambdaQueryWrapper.eq(Todo::getUserName, userName);
        return ResponseUtils.success(todoService.list(todoLambdaQueryWrapper));
    }

最后再写一个前端,在原来的列表上面渲染出来:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>消息通知与待办列表</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            display: flex;
            flex-direction: column;
            align-items: center;
            margin: 20px;
        }
        .message-container {
            display: flex;
            align-items: center;
            gap: 10px;
            cursor: pointer; /* 指示这是一个可点击元素 */
        }
        .bell-icon {
            width: 50px;
            height: 50px;
            background-image: url('bell-icon.png');
            background-size: cover;
            background-repeat: no-repeat;
        }
        .message-count {
            background-color: #ff4500;
            color: white;
            border-radius: 20px;
            padding: 10px 20px;
            font-weight: bold;
        }
        .todo-section {
            margin-top: 20px;
            display: none; /* 默认隐藏 */
        }
        .todo-list {
            list-style-type: none;
            padding: 0;
        }
        .todo-item {
            background-color: #f9f9f9;
            padding: 10px;
            margin-bottom: 5px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        .todo-item h3 {
            margin: 0;
            font-size: 18px;
        }
        .todo-item p {
            margin: 5px 0 0;
            color: #666;
        }
    </style>
</head>
<body>

<div class="message-container" id="messageContainer">
    <div class="bell-icon"></div>
    <span>消息</span>
    <span class="message-count">0</span>
</div>

<div class="todo-section" id="todoSection">
    <h1>待办列表</h1>
    <ul class="todo-list" id="todoList"></ul>
</div>

<script>
    // WebSocket 配置
    const socket = new WebSocket('ws://localhost:8077/webSocket/张三');

    // 连接打开时执行的回调函数
    socket.onopen = function(event) {
        console.log('WebSocket 连接已打开');
    };

    // 接收到消息时执行的回调函数
    socket.onmessage = function(event) {
        const messageCount = event.data;
        document.querySelector('.message-count').textContent = messageCount;
    };

    // 连接关闭时执行的回调函数
    socket.onclose = function(event) {
        console.log('WebSocket 连接已关闭');
    };

    // 连接发生错误时执行的回调函数
    socket.onerror = function(error) {
        console.error('WebSocket 错误:', error);
    };

    // 获取按钮元素
    const fetchButton = document.getElementById('fetchButton');
    const todoListElement = document.getElementById('todoList');
    const todoSection = document.getElementById('todoSection');
    const messageContainer = document.getElementById('messageContainer');

    // 点击通知图标时显示或隐藏待办列表
    messageContainer.addEventListener('click', () => {
        if (todoSection.style.display === 'none' || todoSection.style.display === '') {
            fetchTodoItems(); // 在显示之前获取待办事项
            todoSection.style.display = 'block';
        } else {
            todoSection.style.display = 'none';
        }
    });

    // 定义一个异步函数来获取并显示待办事项
    async function fetchTodoItems() {
        try {
            // 发送 GET 请求
            const response = await fetch('http://localhost:8077/todo/list?userName=张三');

            // 检查响应是否成功
            if (!response.ok) {
                throw new Error('网络响应失败');
            }

            // 解析 JSON 响应
            const data = await response.json();

            // 检查 code 是否为 "0000" 表示操作成功
            if (data.code !== '0000') {
                throw new Error(data.msg || '操作失败');
            }

            // 清空之前的待办事项列表
            todoListElement.innerHTML = '';

            // 遍历 data.data 并生成 HTML
            data.data.forEach(item => {
                const todoItem = document.createElement('li');
                todoItem.classList.add('todo-item');
                todoItem.innerHTML = `
                        <h3>${item.name}</h3>
                        <p>ID: ${item.id}</p>
                        <p>用户名: ${item.userName}</p>
                    `;
                todoListElement.appendChild(todoItem);
            });

        } catch (error) {
            // 处理错误
            alert('获取待办事项失败: ' + error.message);
        }
    }

    // 如果需要,在页面加载时自动获取待办事项(可选)
    // window.onload = fetchTodoItems;
</script>

</body>
</html>

image-20250108153253755

嗯。。丑是丑了点,我们三年级再把它优化一下,这样每次点击通知图标icon的时候就可以查看待办的列表了,而每次点击icon都会有收起和展开的操作。展开即调用http接口获取最新的待办数据。

进度条

不知道大家有没有注意到前面我们新建的一个todoattr子表,这个与主表对应的是一对多的关系,每个主表对应多个子表数据,如果子表完成了待办子项,则把status改为1。实现方式和主表一样,先创建一个子表对应的ServerEndpoint:

package com.wangfugui.apprentice.service;


import com.wangfugui.apprentice.common.util.SpringContextUtil;
import org.springframework.stereotype.Component;

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 java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

@ServerEndpoint("/webSocketTodoAttr/{todoId}")
@Component
public class WebSocketTodoAttrServer {

	 //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
    private static AtomicInteger onlineNum = new AtomicInteger();

    //concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象。
    private static ConcurrentHashMap<String, Session> sessionPools = new ConcurrentHashMap<>();

    //发送消息
    public void sendMessage(Session session, String message) {
        if(session != null){
            synchronized (session) {
                System.out.println("发送数据:" + message);
                try {
                    session.getBasicRemote().sendText(message);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
    //给指定用户发送信息
    public void sendInfo(String todoId, String message){
        Session session = sessionPools.get(todoId);
        try {
            sendMessage(session, message);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    // 群发消息
    public void broadcast(String message){
    	for (Session session: sessionPools.values()) {
            try {
                sendMessage(session, message);
            } catch(Exception e){
                e.printStackTrace();
                continue;
            }
        }
    }

    //建立连接成功调用
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "todoId") String todoId){
        sessionPools.put(todoId, session);
        addOnlineCount();
        System.out.println(todoId + "加入webSocket!当前数为" + onlineNum);
        TodoAttrService todoAttrService = SpringContextUtil.getBean(TodoAttrService.class);
        String progress = todoAttrService.progress(Long.valueOf(todoId));
        sendMessage(session, String.valueOf(progress));
        // 广播上线消息
    }

    //关闭连接时调用
    @OnClose
    public void onClose(@PathParam(value = "username") String userName){
        sessionPools.remove(userName);
        subOnlineCount();
        System.out.println(userName + "断开webSocket连接!当前人数为" + onlineNum);

    }

    //收到客户端信息后,根据接收人的username把消息推下去或者群发
    // to=-1群发消息
    @OnMessage
    public void onMessage(String message) throws IOException{
        System.out.println("server get" + message);

    }

    //错误时调用
    @OnError
    public void onError(Session session, Throwable throwable){
        System.out.println("发生错误");
        throwable.printStackTrace();
    }

    public static void addOnlineCount(){
        onlineNum.incrementAndGet();
    }

    public static void subOnlineCount() {
        onlineNum.decrementAndGet();
    }
    
    public static AtomicInteger getOnlineNumber() {
        return onlineNum;
    }
    
    public static ConcurrentHashMap<String, Session> getSessionPools() {
        return sessionPools;
    }
}

然后我们再往子表里面插入数据:

-- ----------------------------
-- Records of t_todo_attr
-- ----------------------------
INSERT INTO `t_todo_attr` VALUES (1874755040375627778, 1874997307589959681, 1);
INSERT INTO `t_todo_attr` VALUES (1874755040375627779, 1874997307589959681, 1);
INSERT INTO `t_todo_attr` VALUES (1874755040375627780, 1874997307589959681, 1);
INSERT INTO `t_todo_attr` VALUES (1874755040375627781, 1874997307589959681, 1);
INSERT INTO `t_todo_attr` VALUES (1874755040375627782, 1874997307589959681, 1);
INSERT INTO `t_todo_attr` VALUES (1874755040375627783, 1874997307589959681, 0);
INSERT INTO `t_todo_attr` VALUES (1874755040375627784, 1874997307589959681, 0);
INSERT INTO `t_todo_attr` VALUES (1874755040375627785, 1874997307589959681, 0);

最后我们再写一个更新子表状态的http接口就好了:

    @PostMapping("/attr")
    public ResponseUtils updateAttr(@RequestParam("id") Long id) {
        TodoAttr todoAttr = new TodoAttr();
        todoAttr.setId(id);
        todoAttr.setStatus(1);
        todoAttrService.updateById(todoAttr);
        TodoAttr byId = todoAttrService.getById(id);
        webSocketTodoAttrServer.sendInfo(String.valueOf(byId.getTodoId()),
                String.valueOf(todoAttrService.progress(byId.getTodoId())));
        return ResponseUtils.success();
    }

我们最后看一下实现效果:

先连接一下进度条的ws接口,可以看到我们当前这条待办的完成度为63%。

image-20250108155115861

然后我们完成一条待办子表之后再看看进度条数据会如何变化:

image-20250108155241822

image-20250108155304071

可以看到进度条成功增加了!至于前端我们就不写了,这里只演示进度条的后端即可。

拓展

我们也可以通过websocket发送json消息去实现一个多人在线聊天的功能:

    //建立连接成功调用
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "username") String userName){
        sessionPools.put(userName, session);
        addOnlineCount();
        System.out.println(userName + "加入webSocket!当前人数为" + onlineNum);
        // 广播上线消息
        Message msg = new Message();
        msg.setDate(new Date());
        msg.setTo("0");
        msg.setText(userName);
        broadcast(JSON.toJSONString(msg,true));
    }

    //关闭连接时调用
    @OnClose
    public void onClose(@PathParam(value = "username") String userName){
        sessionPools.remove(userName);
        subOnlineCount();
        System.out.println(userName + "断开webSocket连接!当前人数为" + onlineNum);
        // 广播下线消息
        Message msg = new Message();
        msg.setDate(new Date());
        msg.setTo("-2");
        msg.setText(userName);
        broadcast(JSON.toJSONString(msg,true));
    }

    //收到客户端信息后,根据接收人的username把消息推下去或者群发
    // to=-1群发消息
    @OnMessage
    public void onMessage(String message) throws IOException{
        System.out.println("server get" + message);
        Message msg=JSON.parseObject(message, Message.class);
		msg.setDate(new Date());
		if (msg.getTo().equals("-1")) {
			broadcast(JSON.toJSONString(msg,true));
		} else {
			sendInfo(msg.getTo(), JSON.toJSONString(msg,true));
		}
    }

————————————————本内容最终版权归csdn博主掉头发的王富贵所有,本文为博主原创文章,未经博主允许不得转载。 
                        
原文链接:https://blog.csdn.net/csdnerM/article/details/121208211

总结

我们了解了其相对于传统 HTTP 请求的优势,以及如何在实际项目中实现和使用 WebSocket 进行实时通信。WebSocket 作为一种高效的双向通信协议,已经在现代 Web 应用中得到了广泛应用,特别是在需要实时更新数据的场景下,如聊天应用、在线游戏、金融交易平台和协作工具等。

WebSocket 的优势

  • 低延迟:WebSocket 提供了真正的全双工通信,允许服务器和客户端之间即时交换消息,消除了轮询带来的延迟。
  • 减少网络开销:与传统的 HTTP 请求相比,WebSocket 在连接建立后不再需要每次通信都携带完整的 HTTP 头信息,从而显著减少了网络带宽的消耗。
  • 更好的用户体验:通过实时推送数据,用户可以立即获得最新的信息,提升了应用的响应速度和交互性。
  • 简化开发:WebSocket API 简单易用,开发者可以轻松集成到现有的 Web 应用中,而无需复杂的额外配置。

未来展望

随着互联网技术的不断发展,WebSocket 的应用场景将越来越广泛。未来的 Web 应用将更加注重实时性和交互性,而 WebSocket 正是实现这一目标的理想选择。无论是构建更智能的物联网设备、增强虚拟现实和增强现实体验,还是打造更加流畅的社交平台,WebSocket 都将在其中发挥重要作用。

WebSocket 不仅仅是一种通信协议,它代表了 Web 开发的一个新纪元,使得实时通信变得更加简单和高效!!

本文中的仓库地址已开源:websocket

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到