前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Spring+websocket+quartz实现消息定时推送

Spring+websocket+quartz实现消息定时推送

作者头像
Vincent-yuan
发布于 2021-12-08 09:35:34
发布于 2021-12-08 09:35:34
1.3K00
代码可运行
举报
文章被收录于专栏:Vincent-yuanVincent-yuan
运行总次数:0
代码可运行
  • websocket
  • 实现步骤
    • 一、环境搭建
    • 二、完成后台的功能

websocket

简单的说,websocket是真正实现了全双工通信的服务器向客户端推的互联网技术。

全双工与单工、半双工的区别?

  • 全双工:简单地说,就是可以同时进行信号的双向传输(A->B且B->A),是瞬时同步的。
  • 单工、半双工:一个时间段内只有一个动作发生。

推送和拉取的区别?

  • 推:由服务器主动发消息给客户端,就像广播。优势在于,信息的主动性和及时性。
  • 拉:由客户端主动请求所需要的数据。

实现消息通信的几种方式?

  • 传统的http协议实现方式:。
  • 传统的socket技术。
  • websocket协议实现方式。

接下来我们主要讲第三种,使用websocket协议,来实现服务端定时向客户端推送消息。

  • 开发环境:jdk1.8、tomcat7
  • 后台:springmvc、websocket、quartz
  • 前台:html5中新增的API
  • 开发工具:IDEA、maven

实现步骤

一、环境搭建

(1)导入相关约束:

在pom文件中加入需要的约束,spring相关的约束,请各位自己导入,这里我就不贴出来了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<!-- 定时器的包 -->
    <dependency>
      <groupId>org.quartz-scheduler</groupId>
      <artifactId>quartz</artifactId>
      <version>2.3.0</version>
    </dependency>
<!-- 
 spring-support.jar 这个jar 文件包含支持UI模版(Velocity,FreeMarker,JasperReports),邮件服务,脚本服务(JRuby),缓存Cache(EHCache),任务计划Scheduling(uartz)方面的类。 
 外部依赖spring-context, (spring-jdbc, Velocity, FreeMarker, JasperReports, BSH, Groovy, JRuby, Quartz, EHCache) 
 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context-support</artifactId>
      <version>5.1.1.RELEASE</version>
    </dependency>
<!-- websocket的包 -->
    <dependency>
      <groupId>javax.websocket</groupId>
      <artifactId>javax.websocket-api</artifactId>
      <version>1.1</version>
      <scope>provided</scope>
    </dependency>
    
<!--
ps:如果使用原始的配置方式,需要导入spring-websocket、spring-messaging的包,我们这里就通过注解实现
-->
(2)配置xml文件

web.xml中就配置前端控制器,大家自行配置。然后,加载springmvc的配置文件。

springmvc.xml文件中

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    <!-- 自动将控制器加载到bean -->
    <context:component-scan base-package="com.socket.web" />
 <!-- 配置视图解析器 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/"/>
        <property name="suffix" value=".jsp"/>
        <property name="contentType" value="text/html; charset=utf-8"/>
    </bean>
    <!-- 自动注册 DefaultAnnotationHandlerMapping 与 AnnotationMethodHandlerAdapter 两个 bean, 解决了 @Controller 注解的使用前提配置 -->
    <mvc:annotation-driven/>
    
    <!-- 使用fastjson 解析json   因为本人的项目中用到了fastjson,所以这段配置大家可以忽略。 -->
    <mvc:annotation-driven>
        <mvc:message-converters register-defaults="true">
            <bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
                <property name="supportedMediaTypes">
                    <list>
                        <value>text/html;charset=UTF-8</value>
                        <value>application/json</value>
                    </list>
                </property>
                <property name="features">
                    <list>
                        <value>WriteMapNullValue</value>
                        <value>QuoteFieldNames</value>
                    </list>
                </property>
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>

到此,环境就基本搭建完成了。

二、完成后台的功能

这里我就直接贴出代码了,上面有相关的注释。

首先,完成websocket的实现类

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package com.socket.web.socket;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @Author: 
 * @ProjectName: socket
 * @Package: com.socket.web.socket
 * @ClassName: WebSocketServer
 * @Description:
 * @Version: 1.0
 **/
//ServerEndpoint它的功能主要是将目前的类定义成一个websocket服务器端。注解的值将被用于监听用户连接的终端访问URL地址。
@ServerEndpoint(value = "/socket/{ip}")
@Component
public class WebSocketServer {

    //使用slf4j打日志
    private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketServer.class);

    //用来记录当前在线连接数
    private static int onLineCount = 0;

    //用来存放每个客户端对应的WebSocketServer对象
    private static ConcurrentHashMap<String,WebSocketServer> webSocketMap = new ConcurrentHashMap<String, WebSocketServer>();

    //某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;

    //客户端的ip地址
    private String ip;

    /**
     * 连接建立成功,调用的方法,与前台页面的onOpen相对应
     * @param ip ip地址
     * @param session 会话
     */
    @OnOpen
    public void onOpen(@PathParam("ip")String ip,Session session){
        //根据业务,自定义逻辑实现
        this.session = session;
        this.ip = ip;
        webSocketMap.put(ip,this);  //将当前对象放入map中
        addOnLineCount();  //在线人数加一
        LOGGER.info("有新的连接加入,ip:{}!当前在线人数:{}",ip,getOnLineCount());
    }

    /**
     * 连接关闭调用的方法,与前台页面的onClose相对应
     * @param ip
     */
    @OnClose
    public void onClose(@PathParam("ip")String ip){
        webSocketMap.remove(ip);  //根据ip(key)移除WebSocketServer对象
        subOnLineCount();
        LOGGER.info("WebSocket关闭,ip:{},当前在线人数:{}",ip,getOnLineCount());
    }

    /**
     * 当服务器接收到客户端发送的消息时所调用的方法,与前台页面的onMessage相对应
     * @param message
     * @param session
     */
    @OnMessage
    public void onMessage(String message,Session session){
        //根据业务,自定义逻辑实现
        LOGGER.info("收到客户端的消息:{}",message);
    }

    /**
     * 发生错误时调用,与前台页面的onError相对应
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session,Throwable error){
        LOGGER.error("WebSocket发生错误");
        error.printStackTrace();
    }


    /**
     * 给当前用户发送消息
     * @param message
     */
    public void sendMessage(String message){
        try{
            //getBasicRemote()是同步发送消息,这里我就用这个了,推荐大家使用getAsyncRemote()异步
            this.session.getBasicRemote().sendText(message);
        }catch (IOException e){
            e.printStackTrace();
            LOGGER.info("发送数据错误:,ip:{},message:{}",ip,message);
        }
    }

    /**
     * 给所有用户发消息
     * @param message
     */
    public static void sendMessageAll(final String message){
        //使用entrySet而不是用keySet的原因是,entrySet体现了map的映射关系,遍历获取数据更快。
        Set<Map.Entry<String, WebSocketServer>> entries = webSocketMap.entrySet();
        for (Map.Entry<String, WebSocketServer> entry : entries) {
            final WebSocketServer webSocketServer = entry.getValue();
            //这里使用线程来控制消息的发送,这样效率更高。
            new Thread(new Runnable() {
                public void run() {
                    webSocketServer.sendMessage(message);
                }
            }).start();
        }
    }

    /**
     * 获取当前的连接数
     * @return
     */
    public static synchronized int getOnLineCount(){
        return WebSocketServer.onLineCount;
    }

    /**
     * 有新的用户连接时,连接数自加1
     */
    public static synchronized void addOnLineCount(){
        WebSocketServer.onLineCount++;
    }

    /**
     * 断开连接时,连接数自减1
     */
    public static synchronized void subOnLineCount(){
        WebSocketServer.onLineCount--;
    }

    public Session getSession(){
        return session;
    }
    public void setSession(Session session){
        this.session = session;
    }

    public static ConcurrentHashMap<String, WebSocketServer> getWebSocketMap() {
        return webSocketMap;
    }

    public static void setWebSocketMap(ConcurrentHashMap<String, WebSocketServer> webSocketMap) {
        WebSocketServer.webSocketMap = webSocketMap;
    }
}

然后写我们的定时器(quartz),这里我就不详解定时器了。大家可以自行去了解。

这里我使用的是xml注解的方式,创建一个job类,此类不需要继承任何类和实现任何接口。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package com.socket.web.quartz;

import com.socket.web.socket.WebSocketServer;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @Author: 
 * @ProjectName: socket
 * @Package: com.socket.web.quartz
 * @ClassName: TestJob
 * @Description:
 * @Version: 1.0
 **/
public class TestJob {

    public void task(){
        //获取WebSocketServer对象的映射。
        ConcurrentHashMap<String, WebSocketServer> map = WebSocketServer.getWebSocketMap();
        if (map.size() != 0){
            for (Map.Entry<String, WebSocketServer> entry : map.entrySet()) {
                WebSocketServer webSocketServer = entry.getValue();
                try {
                    //向客户端推送消息
                    webSocketServer.getSession().getBasicRemote().sendText("每隔两秒,向客户端推送一次数据");
                }catch (IOException e){
                    e.printStackTrace();
                }
            }
        }else {
            System.out.println("WebSocket未连接");
        }
    }
}

定时器的实现类就完成了,我们还需要在springmvc.xml中进行配置

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<!-- 要执行的任务类 -->
    <bean id="testJob" class="com.socket.web.quartz.TestJob"></bean>

    <!-- 将需要执行的定时任务注入job中 -->
    <bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
        <property name="targetObject" ref="testJob"/>
        <!-- 任务类中需要执行的方法 -->
        <property name="targetMethod" value="task"></property>
        <!-- 上一次未执行完成的,要等待有再执行。 -->
        <property name="concurrent" value="false" />
    </bean>

    <!-- 基本的定时器,会绑定具体的任务。 -->
    <bean id="trigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
        <property name="jobDetail" ref="jobDetail"/>
        <property name="startDelay" value="3000"/>
        <property name="repeatInterval" value="2000"/>
    </bean>

    <bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <property name="triggers">
            <list>
                <ref bean="trigger"/>
            </list>
        </property>
    </bean>

接下来是controller层的代码,就一个登录的功能。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package com.socket.web.controller;

import com.socket.domain.User;
import com.sun.org.apache.bcel.internal.generic.RETURN;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.UUID;

/**
 * @Author: 
 * @ProjectName: socket
 * @Package: com.socket.web
 * @ClassName: ChatController
 * @Description:
 * @CreateDate: 2018/11/9 11:04
 * @Version: 1.0
 **/
@RequestMapping("socket")
@Controller
public class ChatController {

    /**
     * 跳转到登录页面
     * @return
     */
    @RequestMapping(value = "/login",method = RequestMethod.GET)
    public String goLogin(){
        return "login";
    }

    /**
     * 跳转到聊天页面
     * @param request
     * @return
     */
    @RequestMapping(value = "/home",method = RequestMethod.GET)
    public String goMain(HttpServletRequest request){
        HttpSession session = request.getSession();
        if (null == session.getAttribute("USER_SESSION")){
            return "login";
        }
        return "home";
    }

    @RequestMapping(value = "/login",method = RequestMethod.POST)
    public String login(User user, HttpServletRequest request){
        HttpSession session = request.getSession();
        //将用户放入session
        session.setAttribute("USER_SESSION",user);
        return "redirect:home";
    }

}

以上就是登录的代码了,基本上就是伪代码,只要输入用户名就可以了,后面的逻辑,大家可以根据自己的业务来实现。

最后就是前台页面的设计了,登录,login.jsp

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<c:set var="path" value="${pageContext.request.contextPath}"/>
<html>
<head>
    <title>登录</title>
</head>
<body>
<form action="${path}/socket/login" method="post">
    登录名:<input type="text" name="username"/>
    <input type="submit" value="登录"/>
</form>
</body>
</html>

消息接收页面,home.jsp

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>聊天</title>
    
    <script type="text/javascript">
        //判断当前浏览器是否支持WebSocket
        var webSocket = null;
        if ('WebSocket' in window) {
            webSocket = new WebSocket("ws://localhost:9001/socket/127.0.0.1");
        }
        else if ('MozWebSocket' in window) {
            webSocket = new MozWebSocket("ws://localhost:9001/socket/127.0.0.1");
        }
        else {
            alert('Not support webSocket');
        }

        //打开socket,握手
        webSocket.onopen = function (event) {
            alert("websocket已经连接");
        }
        //接收推送的消息
        webSocket.onmessage = function (event) {
            console.info(event);
            alert(event.data);
        }
        //错误时
        webSocket.onerror = function (event) {
            console.info("发生错误");
            alert("websocket发生错误" + event);
        }

        //关闭连接
        webSocket.onclose = function () {
            console.info("关闭连接");
        }

        //监听窗口关闭
        window.onbeforeunload = function (event) {
            webSocket.close();
        }
    </script>
</head>
<body>

</body>
</html>

基本上,数据推送的功能就完成了,下面附上效果图。

启动tomcat。后台定时器两秒刷新一次,判断是否有websocket连接。

登录页面:

数据推送页面:

服务器定时向客户端推送数据的功能就完成了

参考:blog.csdn.net/qq_32101993/blog.csdn.net/qq_32101993/blog.csdn.net/qq_32101993/ article/details/83994524/

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-12-07 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
【iOS】UI基础Day3-笔记(UIButton、购物车综合案例)
代码中使用UIButton //实例化一个按钮 UIButton *button = [[UIButton alloc] init]; //设置按钮的frame button.frame = CGRectMake(100, 100, 120, 30); //设置按钮的背景颜色 button.backgroundColor = [UIColor greenColor]; //设置按钮普通状态下的文字和文字颜色 [button setTitle:@"普通状态" forState:UIControlStateNo
肓己
2021/08/12
6510
iOS-UIButton 全面解析UIButton 的全面解析
UIButton 的全面解析 建议收藏,用到的时候来这里一查就都明白了 //初始化Button 不用alloca init 的方法 用便利构造器初始化 UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; typedef NS_ENUM(NSInteger, UIButtonType) { UIButtonTypeCustom = 0, -自定义风格 UIButtonTypeSystem NS_ENUM
xx_Cc
2018/05/10
1.8K0
UIButton使用方法汇总
//按钮初始化类方法 UIButton *button1 = [UIButton buttonWithType:UIButtonTypeRoundedRect];//这里创建一个圆角矩形的按钮 //按钮初始化实例方法 UIButton *button1=[[UIButton alloc]initWithFrame:CGRectMake(50, 300, 200, 50)]; //能够定义的button类型有以下6种, // typedef enum { // UIButtonTypeCustom
猿人谷
2018/01/17
1.4K0
iOS调用相册和摄像头
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view.
用户8983410
2021/10/29
1.8K0
iOS_UIButton 简单操作
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/116488.html原文链接:https://javaforall.cn
全栈程序员站长
2022/07/07
3160
iOS录屏框架ReplayKit的应用总结
ReplayKit是iOS自带的一个屏幕录制的框架,其支持应用程序对当前应用内页面进行录屏,并将最终的视频保存到系统相册中。ReplayKit在iOS 9之后引入,其接口简介,可以非常方便的为应用添加录屏功能。需要注意,在某些iOS 12系统上,开启录屏可能会失败(通常需要重启设备解决)。
珲少
2020/05/13
3.5K0
1.注册或登录页面设计:UILabel,UIButton,UITextField
学习iOS开发已经有一段时日了,之前一直没有系统的对iOS开发的相关知识进行归纳总结,导致很多知识点云里雾里在脑子里形不成iOS开发的思想,现将自己在学习过程中遇到的一些知识进行总结,希望能对iOS初学者能有一定的帮助。最初学iOS的时候苦于没有大神指点,全靠自己一点点摸索,确实走了很多弯路,不希望还有小伙伴跟我一样走过多的弯路。   由于本人只是从去年11月份才开始玩iOS(附上自己的学习路线,如下图),受限于能力,难免有一些不完善或不恰当的地方,希望大神们多多见谅,勿拍砖,有不足或需要完善的地方也希望
猿人谷
2018/01/17
2.6K0
1.注册或登录页面设计:UILabel,UIButton,UITextField
iOS开发之微信聊天工具栏的封装
之前山寨了一个新浪微博(iOS开发之山寨版新浪微博小结),这几天就山寨个微信吧。之前已经把微信的视图结构简单的拖了一下(iOS开发之微信山寨版),今天就开始给微信加上具体的实现功能,那么就先从微信的聊天界面开始吧。提到封装是少不了写代码的,在封装组件的时候,为了组件的可移植性,我们就不能用storyboard来拖拽了。为了屏幕的适配,适应不同屏幕的手机,所以在封装组件的时候是少不了为我们的组件来添加约束。今天博客中的所有代码都是脱离storyboard的,这些代码在别的工程中也是可以使用的。好,废话少说,切
lizelu
2018/01/11
2.6K0
iOS开发之微信聊天工具栏的封装
Quartz2D复习(三) --- 涂鸦
和上一篇手势解锁不一样,手势解锁只画了一条路径,从触摸开始--》触摸移动--》触摸结束 ,然后路径完成了,渲染出来就是手势解锁了;
tandaxia
2018/09/27
7090
Quartz2D复习(三) --- 涂鸦
iOS-UI控件之UIButton
---恢复内容开始--- UIButton 既可以显示图片,又可以显示文字,还能随时调整内部位置 系统自带尺寸 storyboard内部调整UIButton属性 状态 监听按钮点击事件 凡是继承自UI
用户1941540
2018/05/11
9740
iOS调整导航条BarButtonItem与titleView 的间距
与屏幕边界 或者与titleView 的间距 只要分别调整rightBarButtonItems 数组元素的顺序。
公众号iOS逆向
2021/08/25
2.3K0
iOS调整导航条BarButtonItem与titleView 的间距
iOS_自定义UITabBarController标签视图控制器
首先创建一个类,继承自UItabBarController 然后在.m文件中:
mikimo
2022/07/20
6150
iOS 短信验证码倒计时按钮的实现
创建按钮, 添加点击方法; 用NSTimer定时器, 每秒执行一次, 定时改变Button的title,改变Button的样式, 设置Button不可点击; 若倒计时结束, 定时器关闭, 并改变Button的样式, 可以点击。
网罗开发
2021/01/29
2.2K0
iOS 短信验证码倒计时按钮的实现
【IOS开发基础系列】UIButton专题
//需要导入框架QuartzCore.framework,并且在当前类中引用#import
江中散人_Jun
2023/10/16
3810
【IOS开发基础系列】UIButton专题
iOS8新特性扩展(Extension)应用之四——自定义键盘控件
        iOS8系统的开放第三方键盘,使得用户在输入法的选择上更加自主灵活,也更加贴近不同语言的输入风格。这篇博客,将介绍如何开发一个第三方的键盘控件。
珲少
2018/08/16
1.3K0
iOS8新特性扩展(Extension)应用之四——自定义键盘控件
iOS开发~获取验证码倒计时实现
在app开发中经常会遇到,输入手机号获取验证码的功能,下面就和大家分享一下,获取验证码倒计时的功能实现 首先给大家看一下页面展示
网罗开发
2021/01/29
1.1K0
iOS开发~获取验证码倒计时实现
记录下UIButton的图文妙用和子控件的优先显示
  UIButton的用处特别多,这里只记录下把按钮应用在图文显示的场景,和需要把图片作为按钮的背景图片显示场景;
tandaxia
2018/09/27
1.8K0
记录下UIButton的图文妙用和子控件的优先显示
iOS-UIButton设置高亮状态下的背景色
UIButton一般分为高亮的普通两种状态,原生的方法可以设置这两种不同状态下的文字颜色,文字内容,背景图片,按钮图片。但是不能设置按钮的背景色。
Lee坚武
2019/12/13
1.9K0
controller如何拿到自定义view的点击事件?
如下图所示:自定义PersonalCenterView,如何在controller拿到按钮(小箭头)的点击方法?
iOSSir
2019/06/14
6100
controller如何拿到自定义view的点击事件?
iOS百度地图开发之路径规划
路线规划 示例程序 使用百度地图SDK版本为2.9.1。 需要将改 代码: #import <MapKit/MapKit.h> #import <BaiduMapAPI_Map/BMKMapView.h> #import <BaiduMapAPI_Location/BMKLocationService.h> #import <BaiduMapAPI_Search/BMKSearchComponent.h> #import <BaiduMapAPI_Map/BMKPolylineView.h> #impor
hrscy
2018/08/30
1.3K0
iOS百度地图开发之路径规划
推荐阅读
相关推荐
【iOS】UI基础Day3-笔记(UIButton、购物车综合案例)
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档