在我们以往的软件或者网站使用中,都有遇到过这种情况,莫名的弹出广告或者通知!而在我们的业务系统中,有的时候也需要群发通知公告的方式去告知网站用户一些信息,那么这种功能是怎么实现的呢,本 Chat 将使用 Spring Boot+WebSocket 来实现这类功能!让你在可以实际运用到项目当中。

通过本 Chat 你将学习到:

  • Spring Boot 工程搭建;
  • 如何将 WebSocket 继承到项目中;
  • 如何设计全网通告业务相关表与功能;
  • 关于 WebSocket 的常见问题解决。

背景介绍

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

springboot 与 webSocker 整合

使用 Intellij IDEA 快速创建一个 springboot + 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;@Configurationpublic 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")@Componentpublic 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 源码地址《===

效果展示

通告表设计

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

接下来

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

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

接着如果那里要使用 Spring 管理在 Bean 的话,就可以使用这种方式使用applicationContext.getBean(BulletinService.class)


本文首发于 GitChat,未经授权不得转载,转载需与 GitChat 联系。

阅读全文: http://gitbook.cn/gitchat/activity/5d4c6849f6bd6e3c4ef1ccef

您还可以下载 CSDN 旗下精品原创内容社区 GitChat App , GitChat 专享技术内容哦。

基于 WebSocket 的实时通告功能,推送在线与未登录用户相关推荐

  1. netty服务器定时发送消息,netty+websocket+quartz实现消息定时推送

    netty+websocket+quartz实现消息定时推送&&IM聊天室 在讲功能实现之前,我们先来捋一下底层的原理,后面附上工程结构及代码 1.NIO NIO主要包含三大核心部分: ...

  2. 基于goEasy消息推送的扫码登录

    此篇介绍我做的基于goEasy消息推送的扫码登录的具体实现.将我的思路和代码一起贴出希望能帮助到对此感兴趣的童鞋,同时也希望大家有什么意见和建议也可以向我提出,另外如果有问题想与我探讨的可以加我的扣扣 ...

  3. 抖音火爆的微信早安推送在线版,基于邮件,多样式是可选

    邮件推送背景 往期已经支持了微信公众号测试号的微信早安推送,可参考文章:抖音火爆的微信早安推送在线版,无需搭建代码,简单配置即可给心爱的他/她定时推送消息了配置,但是基于微信的推送存在一下几个问题: ...

  4. DailyInfo——利用Python实现基于企业微信的每日图文推送

    DailyInfo 基于企业微信的每日图文推送 项目地址 Gitee地址 Github地址 支持功能 Bing必应 每日壁纸 金山词霸 每日一句 ONE·一个 一图一句 和风天气 多地区天气预报 农历 ...

  5. Spring之WebSocket网页聊天以及服务器推送

    Spring之WebSocket网页聊天以及服务器推送 转自:http://www.xdemo.org/spring-websocket-comet/ /Springframework /Spring ...

  6. 【苹果家庭群发推送】软件安装Apple推送是一种基于IMESSAGE平台的新信息推送功效的营销软件

    推荐内容IMESSGAE相关 作者推荐内容 iMessage苹果推软件 *** 点击即可查看作者要求内容信息 作者推荐内容 1.家庭推内容 *** 点击即可查看作者要求内容信息 作者推荐内容 2.相册 ...

  7. 抖音火爆的早安推送在线版,新功能速递,支持推送时间自定义,添加生日日期计算

    抖音火爆的早安推送在线版,新功能速递,支持推送时间自定义,添加生日日期计算 传送门 问题咨询:1319723770@qq.com 新功能一:支持推送时间自定义 操作路径 消息推送列表界面,找到需要修改 ...

  8. 抖音火爆的微信早安推送在线版,无需搭建代码,简单配置即可给心爱的他/她定时推送消息了

    抖音火爆的早安推送在线版,无需搭建代码,简单配置即可给心爱的他/她定时推送消息了 只需通过简单的配置,无需自己搭建代码环境,申请各种api,甚至保持电脑程序开机等.配置完成后,即可实现每天定时配送. ...

  9. 抖音火爆的早安推送在线版,常见问题处理

    抖音火爆的早安推送在线版,常见问题处理 传送门 问题咨询:1319723770@qq.com 推送结果查看方式 进入公众号消息推送 推送结果页面,查看推送结果 消息没有收到,问题处理 问题一:推送结果 ...

最新文章

  1. python手机app 授权登录_Appium+unittest+python登录app
  2. Caliburn.Micro学习笔记(三)----事件聚合IEventAggregator和 IhandleT
  3. LintCode 563. 背包问题 V(DP)
  4. vba 指定列后插入列_Excle中的VBA介绍分享
  5. python编程软件哪个好-Python开发工具哪个好?好用的Python开发工具排行榜推荐下载...
  6. 鸿蒙适配手机时间,华为鸿蒙OS适配时间表出炉:麒麟9000最先,麒麟990第二
  7. ACM PKU 题目分类(完整整理版本)
  8. eNSP交换机配置IP
  9. IE6下的Aborted解决办法
  10. 2022-2028年中国SIP行业竞争现状及投资策略研究报告
  11. 2001-2019年300多个城市进口额、出口额、进出口额汇总
  12. 爬虫从头学之Requests+正则表达式爬取猫眼电影top100
  13. elasticsearch从入门到入门系列(一)---简单介绍及安装
  14. iOS自动化测试之基于模拟器的自动化测试
  15. Google搜索中国定制版已黄了,百度再次PK的希望或落空
  16. 思想的芦苇——把过程改进注入人文服务的思想以提升其价值
  17. 解析大多数WordPress用户都选择托管虚拟主机的原因
  18. 问题 A: 深入浅出学算法044-最大整数
  19. ML:LGBMClassifier、XGBClassifier和CatBoostClassifier的feature_importances_计算方法源代码解读之详细攻略
  20. Win11的两个实用技巧系列之dns异常和打印机重命名

热门文章

  1. 输入一个整数,判断是不是质数?
  2. 豆粕、白糖期货涨跌幅的可视化分析
  3. ARM架构与体系学习(二)——3级流水线
  4. 颜值经济下,植发成为一门好生意
  5. Stochastic dominance 随机优势
  6. 跨云平台大数据系统迁移实践
  7. 转:关于商业模式的那些迷思
  8. PostgreSQL performance test use ssh tunnel
  9. OptaPlanner源码学习-VRPTW问题计算得分
  10. Qt中使用Doxygen注释生成总结