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

栏目: Java · 发布时间: 6年前

内容简介:在我们以往的软件或者网站使用中,都有遇到过这种情况,莫名的弹出广告或者通知!而在我们的业务系统中,有的时候也需要群发通知公告的方式去告知网站用户一些信息,那么这种功能是怎么实现的呢,本文将使用springboot+webSocket来实现这类功能,当然也有其他方式来实现使用Intellij IDEA 快速创建一个springboot + webSocket项目首先要注入

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

springboot 与 webSocker整合

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

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

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://user-gold-cdn.xitu.io/2019/2/21/1690f655083376d7?w=721&h=457&f=gif&s=31053)
    //发送消息
    function send() {
        var message = $("#text").val();
        websocket.send(message);
        $("#text").val("");
    }
</script>
</html>
复制代码

GITHUB源码地址 《===

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

效果展示

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

通告表设计

通告表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注入操作,下面贴下实践

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

接下来

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

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

基于websocket的实时通告功能,推送在线用户,新登录用户
最后在 WebSocket endpoint 类添加相应的静态对象,并添加 set

方法

基于websocket的实时通告功能,推送在线用户,新登录用户
接着如果那里要使用Spring管理在Bean的话,就可以使用这种方式使用 applicationContext.getBean(BulletinService.class)

以上所述就是小编给大家介绍的《基于websocket的实时通告功能,推送在线用户,新登录用户》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Growth Hacker Marketing

Growth Hacker Marketing

Ryan Holiday / Portfolio / 2013-9-3 / USD 10.31

Dropbox, Facebook, AirBnb, Twitter. A new generation of multibillion dollar brands built without spending a dime on “traditional marketing.” No press releases, no PR firms, and no billboards in Times ......一起来看看 《Growth Hacker Marketing》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

html转js在线工具
html转js在线工具

html转js在线工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具