基于websocket的实时通告功能,推送在线用户,新登录用户

简介: SpringBoot 部署与Spring部署都有一些差别,但现在用Srpingboot的公司多,SpringBoot创建项目快,所以使用该方式来讲解,有一个问题就是开发WebSocket时发现无法通过@Autowired注入bean,一直为空。

著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

背景介绍

在我们以往的软件或者网站使用中,都有遇到过这种情况,莫名的弹出广告或者通知!而在我们的业务系统中,有的时候也需要群发通知公告的方式去告知网站用户一些信息,那么这种功能是怎么实现的呢,本文将使用springboot+webSocket来实现这类功能,当然也有其他方式来实现长连接/websocket/SSE等主流服务器推送技术比较

springboot 与 webSocker整合

使用Intellij IDEA 快速创建一个springboot + webSocket项目

0d96e0e3ecbe09b6b54fd1b74aaf353.png

Maven的pom.xml内容

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
  • webSocket核心是@ServerEndpoint这个注解。这个注解是Javaee标准里的注解,tomcat7以上已经对其进行了实现,如果是用传统方法使用tomcat发布的项目,只要在pom文件中引入javaee标准即可使用。
  • 但使用springboot内置tomcat时,就不需要引入javaee-api了,spring-boot已经包含了。
  • springboot的高级组件会自动引用基础的组件,像spring-boot-starter-websocket就引入了spring-boot-starter-web和spring-boot-starter,所以不要重复引入
  • springboot已经做了深度的集成和优化,注意是否添加了不需要的依赖、配置或声明。由于很多讲解组件使用的文章是和spring集成的,会有一些配置。在使用springboot时,由于springboot已经有了自己的配置,再这些配置有可能导致各种各样的异常。
<dependency>
      <groupId>javax</groupId>
      <artifactId>javaee-api</artifactId>
      <version>7.0</version>
      <scope>provided</scope>
    </dependency>

使用@ServerEndpoint创建websocket端点

首先要注入ServerEndpointExporter类,这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint。要注意,如果使用独立的servlet容器,而不是直接使用springboot的内置容器,就不要注入ServerEndpointExporter,因为 它(ServerEndpointExporter) 将由容器自己提供和管理。

WebSocketConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

接下来就是写websocket的具体实现类,很简单,直接上代码:

BulletinWebSocket.java

package com.example.websocket.controller;
import com.example.websocket.service.BulletinService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Date;
import java.util.concurrent.CopyOnWriteArraySet;
/**
 * @ServerEndpoint 该注解用来指定一个URI,客户端可以通过这个URI来连接到WebSocket。
 * 类似Servlet的注解mapping。无需在web.xml中配置。
 * configurator = SpringConfigurator.class是为了使该类可以通过Spring注入。
 * @Author jiangpeng
 */
@ServerEndpoint(value = "/webSocket/bulletin")
@Component
public class BulletinWebSocket {
    private static final Logger LOGGER = LoggerFactory.getLogger(BulletinWebSocket.class);
    private static ApplicationContext applicationContext;
    public static void setApplicationContext(ApplicationContext context) {
        applicationContext = context;
    }
    public BulletinWebSocket() {
        LOGGER.info("BulletinWebSocket init ");
    }
    // concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
    private static CopyOnWriteArraySet<BulletinWebSocket> BULLETIN_WEBSOCKETS = new CopyOnWriteArraySet<BulletinWebSocket>();
    // 与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;
    /**
     * 连接建立成功调用的方法
     * */
    @OnOpen
    public void onOpen(Session session) throws IOException {
        this.session = session;
        // 加入set中
        BULLETIN_WEBSOCKETS.add(this);
        // 新登录用户广播通知
        this.session.getBasicRemote().sendText(applicationContext.getBean(BulletinService.class).getBulletin()+"-"+new Date());
        LOGGER.info("有新连接加入{}!当前在线人数为{}", session, getOnlineCount());
    }
    @OnClose
    public void onClose() {
        BULLETIN_WEBSOCKETS.remove(this);
        LOGGER.info("有一连接关闭!当前在线人数为{}", getOnlineCount());
    }
    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     * @param session 可选的参数
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        LOGGER.info("来自客户端的信息:{}", message);
    }
    @OnError
    public void onError(Session session, Throwable error) {
        LOGGER.error("发生错误:{}", session.toString());
        error.printStackTrace();
    }
    /**
     * 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。
     * 因为使用了Scheduled定时任务,所以方法不是有参数
     * @throws Exception
     */
    @Scheduled(cron = "0/2 * * * * ?")
    public void sendMessage() throws IOException {
        // 所有在线用户广播通知
        BULLETIN_WEBSOCKETS.forEach(socket -> {
            try {
                socket.session.getBasicRemote().sendText("定时:"+applicationContext.getBean(BulletinService.class).getBulletin()+"-"+new Date());
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }
    public static synchronized int getOnlineCount() {
        return BULLETIN_WEBSOCKETS.size();
    }
}

使用springboot的唯一区别是要添加@Component注解,而使用独立容器不用,是因为容器自己管理websocket的,但在springboot中连容器都是spring管理的。

虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来

private static CopyOnWriteArraySet<BulletinWebSocket> BULLETIN_WEBSOCKETS = new CopyOnWriteArraySet<BulletinWebSocket>();

html代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>static</h1>
<div id="msg" class="panel-body">
</div>
<input id="text" type="text"/>
<button onclick="send()">发送</button>
</body>
<script src="https://cdn.bootcss.com/web-socket-js/1.0.0/web_socket.js"></script>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script type="text/javascript">
    var websocket = null;
    //判断当前浏览器是否支持WebSocket
    if ('WebSocket' in window) {
        websocket = new WebSocket("ws://127.0.0.1:8080/webSocket/bulletin");
    }
    else {
        alert("对不起!你的浏览器不支持webSocket")
    }
    //连接发生错误的回调方法
    websocket.onerror = function () {
        setMessageInnerHTML("error");
    };
    //连接成功建立的回调方法
    websocket.onopen = function (event) {
        setMessageInnerHTML("加入连接");
    };
    //接收到消息的回调方法
    websocket.onmessage = function (event) {
        setMessageInnerHTML(event.data);
    };
    //连接关闭的回调方法
    websocket.onclose = function () {
        setMessageInnerHTML("断开连接");
    };
    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,
    // 防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function () {
        var is = confirm("确定关闭窗口?");
        if (is) {
            websocket.close();
        }
    };
    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML) {
        $("#msg").append(innerHTML + "<br/>")
    };
    //关闭连接
    function closeWebSocket() {
        websocket.close();
    }
![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/2/21/1690f655083376d7~tplv-t2oaga2asx-image.image)
    //发送消息
    function send() {
        var message = $("#text").val();
        websocket.send(message);
        $("#text").val("");
    }
</script>
</html>

GITHUB源码地址《===

890c3296f6bd5d05c4497a7aae1fbfb.png

效果展示

47e75927563f3257fe8e13837d57be1.png

通告表设计

通告表Bulletin

CREATE TABLE `bulletin` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号id',
  `title` varchar(50) COLLATE utf8_bin NOT NULL COMMENT '标题',
  `content` varchar(1000) COLLATE utf8_bin NOT NULL COMMENT '内容',
  `user_type` tinyint(1) NOT NULL COMMENT '通告对象类型 1:单个用户  2:多个用户  3:全部用户',
  `user_roles` varchar(20) COLLATE utf8_bin DEFAULT NULL COMMENT '通告对象角色',
  `user_depts` varchar(20) COLLATE utf8_bin DEFAULT NULL COMMENT '通告对象部门',
  `type` tinyint(1) DEFAULT NULL COMMENT '通告类型 1:系统升级',
  `publish_time` datetime DEFAULT NULL COMMENT '发布时间',
  `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 0:待发布  1:已发布 2:撤销 ',
  `created_at` datetime NOT NULL COMMENT '创建时间',
  `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `created_by` int(11) NOT NULL COMMENT '创建人',
  `updated_by` int(11) NOT NULL COMMENT '修改人',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='通告表';

用户标记表BulletinUser

CREATE TABLE `bulletin_user` (
  `bulletin_id` int(11) NOT NULL COMMENT '通告编号id',
  `user_id` int(11) NOT NULL COMMENT '用户id',
  `is_read` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否阅读 0否 1是',
  `created_at` datetime NOT NULL COMMENT '创建时间',
  `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`bulletin_id`,`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='用户通告标记表';

业务规则

添加通告

  • 单个用户:通告表添加一条记录,用户标记表添加一条记录
  • 多个用户:通告表添加一条记录,用户标记表添加多条记录
  • 全部用户:通告表添加一条记录

阅读公告

  • 单个用户:修改用户标记表中的记录
  • 多个用户:修改用户标记表中的记录
  • 全部用户:用户标记表添加阅读记录

发现新通告的规则

  • 单个用户:通告表中有,并且通告对象类型是“单个用户”,并且用户标记表中的未读标记是“0”
  • 多个用户:通告表中有,并且通告对象类型是“多个用户”,并且用户标记表中的未读标记是“0”
  • 全部用户:通告表中有,并且通告对象类型是“全部用户”,并且用户标记表中没有用户的信息

通告弹窗提示

  1. 在线用户可以收到并弹窗显示,看过的就不用再显示了 (websocket 服务查询当前用户是否有未读的公告,也就是所有全部用户类型通告编号 not in 已读通告编号,多出来的结果就是需要弹窗的通告, 可以时间筛选,免得新员工弹所有公告 )
  2. 没看过的一登录也会弹窗显示或者实时
  3. 前端任何页面都可以接受到最新通告并弹窗(公共parent.js做websocket监听)

以上的功能实现居然可以参考上面 BulletinWebSocket.java 中的这几块代码

/**
     * 连接建立成功调用的方法
     * */
    @OnOpen
    public void onOpen(Session session) throws IOException {
        this.session = session;
        // 加入set中
        BULLETIN_WEBSOCKETS.add(this);
        // 新登录用户广播通知
        this.session.getBasicRemote().sendText(applicationContext.getBean(BulletinService.class).getBulletin()+"-"+new Date());
        LOGGER.info("有新连接加入{}!当前在线人数为{}", session, getOnlineCount());
    }
public void sendMessage() throws IOException {
        // 所有在线用户广播通知
        BULLETIN_WEBSOCKETS.forEach(socket -> {
            try {
                socket.session.getBasicRemote().sendText("定时:"+applicationContext.getBean(BulletinService.class).getBulletin()+"-"+new Date());
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }

总结

SpringBoot 部署与Spring部署都有一些差别,但现在用Srpingboot的公司多,SpringBoot创建项目快,所以使用该方式来讲解,有一个问题就是开发WebSocket时发现无法通过@Autowired注入bean,一直为空。怎么解决呢?

其实不是不能注入,是已经注入了,但是客户端每建立一个链接就会创建一个对象,这个对象没有任何的bean注入操作,下面贴下实践

@ServerEndpoint(value = "/websocket") 
@Component  
public class RoomwebSocket {  
private static Logger log= LoggerFactory.getLogger( name: "RoomWebSocket");
http://blog.csdn.net/Lotts
public RoomWebSocket(){ 在构造方法里加入断点,会发现,启动的时候进入断点,客户 
log.error("websocket初始化啦..."); 端发起连接的时候也进入了断点
  } 

接下来

@Override
@ public <T> T getEndpointInstance(Class<T> clazz)  clazz:"class com.ns002.game.douniu.web.websocket.RoomWebSocket" 
throws InstantiationException { try {
return clazz.newInstance(); clazz
} catch (IllegalAccessException e) { http://blog.csdn.net/Lotts
InstantiationException ie = new InstantiationException(); ie.initCause(e);
throw ie; 客户端发起连接的时候,我们通过刚刚设置的断点进入下一步,可以看到初始化过程,整个初始化过程没有 
进行任何的bean的注入操作

解决办法就是springboot的启动类注入一个static的对象

@SpringBootApplication@EnableScheduling
public class WebSocketApplication {
public static void main(String[] args) {
ConfigurableApplicationContextapplicationContext=SpringApplication.run(WebSocketApplication.class, args)/注入applicationContext
BulletinWebSocket.setApplicationContext(applicationContext)

最后在WebSocket endpoint类添加相应的静态对象,并添加set方法

@ServerEndpoint(value = "/webSocket/bulletin' -@Component
public class BulletinWebSocket {
private static final Logger LOGGER=LoggerFactory.getLogger(BulletinWebSocket.class);
private static ApplicationContext applicationContext;
public static void setApplicationContext(ApplicationContext context){
applicationContext=context;
public BulletinWebSocket(){
LOGGER.info("BulletinWebSocket init ");
}

接着如果那里要使用Spring管理在Bean的话,就可以使用这种方式使用

applicationContext.getBean(BulletinService.class)


目录
相关文章
|
7月前
使用uniapp实现websocket聊天功能
使用uniapp实现websocket聊天功能
|
2月前
|
前端开发 JavaScript UED
探索Python Django中的WebSocket集成:为前后端分离应用添加实时通信功能
通过在Django项目中集成Channels和WebSocket,我们能够为前后端分离的应用添加实时通信功能,实现诸如在线聊天、实时数据更新等交互式场景。这不仅增强了应用的功能性,也提升了用户体验。随着实时Web应用的日益普及,掌握Django Channels和WebSocket的集成将为开发者开启新的可能性,推动Web应用的发展迈向更高层次的实时性和交互性。
101 1
|
4月前
|
Linux C++ Docker
【Azure 应用服务】App Service for Linux 中实现 WebSocket 功能 (Python SocketIO)
【Azure 应用服务】App Service for Linux 中实现 WebSocket 功能 (Python SocketIO)
|
5月前
|
前端开发 JavaScript API
探索Python Django中的WebSocket集成:为前后端分离应用添加实时通信功能
【7月更文挑战第17天】现代Web开发趋势中,前后端分离配合WebSocket满足实时通信需求。Django Channels扩展了Django,支持WebSocket连接和异步功能。通过安装Channels、配置设置、定义路由和消费者,能在Django中实现WebSocket交互。前端使用WebSocket API连接后端,实现双向数据流,如在线聊天功能。集成Channels提升Web应用的实时性和用户体验,适应实时交互场景的需求。**
229 6
|
4月前
|
JavaScript 前端开发 网络协议
WebSocket在Java Spring Boot+Vue框架中实现消息推送功能
在现代Web应用中,实时消息提醒是一项非常重要的功能,能够极大地提升用户体验。WebSocket作为一种在单个TCP连接上进行全双工通信的协议,为实现实时消息提醒提供了高效且低延迟的解决方案。本文将详细介绍如何在Java Spring Boot后端和Vue前端框架中利用WebSocket实现消息提醒功能。
222 0
|
7月前
|
前端开发
t-io websocket的聊天功能学习记录(二)
t-io websocket的聊天功能学习记录(二)
112 0
|
7月前
t-io websocket的聊天功能学习记录(一)
t-io websocket的聊天功能学习记录(一)
126 0
|
移动开发 监控 网络协议
基于Socket通讯(C#)和WebSocket协议(net)编写的两种聊天功能(文末附源码下载地址)
基于Socket通讯(C#)和WebSocket协议(net)编写的两种聊天功能(文末附源码下载地址)
|
设计模式 前端开发 Java
用WebSocket实现一个简易的群聊功能,so easy
本文主要来讲解如何使用WebSocket来实现一个简易的群聊功能。
|
前端开发 Cloud Native Java
使用Spring WebSocket实现实时通信功能
使用Spring WebSocket实现实时通信功能
115 0