Spring+websocket+quartz實(shí)現(xiàn)消息定時(shí)推送
websocket
簡(jiǎn)單的說(shuō),websocket是真正實(shí)現(xiàn)了全雙工通信的服務(wù)器向客戶端推的互聯(lián)網(wǎng)技術(shù)。
全雙工與單工、半雙工的區(qū)別?
全雙工:簡(jiǎn)單地說(shuō),就是可以同時(shí)進(jìn)行信號(hào)的雙向傳輸(A->B且B->A),是瞬時(shí)同步的。 單工、半雙工:一個(gè)時(shí)間段內(nèi)只有一個(gè)動(dòng)作發(fā)生。
推送和拉取的區(qū)別?
推:由服務(wù)器主動(dòng)發(fā)消息給客戶端,就像廣播。優(yōu)勢(shì)在于,信息的主動(dòng)性和及時(shí)性。 拉:由客戶端主動(dòng)請(qǐng)求所需要的數(shù)據(jù)。
實(shí)現(xiàn)消息通信的幾種方式?
傳統(tǒng)的http協(xié)議實(shí)現(xiàn)方式:。 傳統(tǒng)的socket技術(shù)。 websocket協(xié)議實(shí)現(xiàn)方式。
接下來(lái)我們主要講第三種,使用websocket協(xié)議,來(lái)實(shí)現(xiàn)服務(wù)端定時(shí)向客戶端推送消息。
開(kāi)發(fā)環(huán)境:jdk1.8、tomcat7 后臺(tái):springmvc、websocket、quartz 前臺(tái):html5中新增的API 開(kāi)發(fā)工具:IDEA、maven
實(shí)現(xiàn)步驟
一、環(huán)境搭建
(1)導(dǎo)入相關(guān)約束:
在pom文件中加入需要的約束,spring相關(guān)的約束,請(qǐng)各位自己導(dǎo)入,這里我就不貼出來(lái)了。
????<dependency>
??????<groupId>org.quartz-schedulergroupId>
??????<artifactId>quartzartifactId>
??????<version>2.3.0version>
????dependency>
????<dependency>
??????<groupId>org.springframeworkgroupId>
??????<artifactId>spring-context-supportartifactId>
??????<version>5.1.1.RELEASEversion>
????dependency>
????<dependency>
??????<groupId>javax.websocketgroupId>
??????<artifactId>javax.websocket-apiartifactId>
??????<version>1.1version>
??????<scope>providedscope>
????dependency>
????
(2)配置xml文件
web.xml中就配置前端控制器,大家自行配置。然后,加載springmvc的配置文件。
springmvc.xml文件中
????
????<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>
????
????<mvc:annotation-driven/>
????
????
????<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-8value>
????????????????????????<value>application/jsonvalue>
????????????????????list>
????????????????property>
????????????????<property?name="features">
????????????????????<list>
????????????????????????<value>WriteMapNullValuevalue>
????????????????????????<value>QuoteFieldNamesvalue>
????????????????????list>
????????????????property>
????????????bean>
????????mvc:message-converters>
????mvc:annotation-driven>
到此,環(huán)境就基本搭建完成了。
二、完成后臺(tái)的功能
這里我就直接貼出代碼了,上面有相關(guān)的注釋。
首先,完成websocket的實(shí)現(xiàn)類。
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:?清風(fēng)一陣吹我心
?*?@ProjectName:?socket
?*?@Package:?com.socket.web.socket
?*?@ClassName:?WebSocketServer
?*?@Description:
?*?@Version:?1.0
?**/
//ServerEndpoint它的功能主要是將目前的類定義成一個(gè)websocket服務(wù)器端。注解的值將被用于監(jiān)聽(tīng)用戶連接的終端訪問(wèn)URL地址。
@ServerEndpoint(value?=?"/socket/{ip}")
@Component
public?class?WebSocketServer?{
????//使用slf4j打日志
????private?static?final?Logger?LOGGER?=?LoggerFactory.getLogger(WebSocketServer.class);
????//用來(lái)記錄當(dāng)前在線連接數(shù)
????private?static?int?onLineCount?=?0;
????//用來(lái)存放每個(gè)客戶端對(duì)應(yīng)的WebSocketServer對(duì)象
????private?static?ConcurrentHashMap?webSocketMap?=?new?ConcurrentHashMap();
????//某個(gè)客戶端的連接會(huì)話,需要通過(guò)它來(lái)給客戶端發(fā)送數(shù)據(jù)
????private?Session?session;
????//客戶端的ip地址
????private?String?ip;
????/**
?????*?連接建立成功,調(diào)用的方法,與前臺(tái)頁(yè)面的onOpen相對(duì)應(yīng)
?????*?@param?ip?ip地址
?????*?@param?session?會(huì)話
?????*/
????@OnOpen
????public?void?onOpen(@PathParam("ip")String?ip,Session?session){
????????//根據(jù)業(yè)務(wù),自定義邏輯實(shí)現(xiàn)
????????this.session?=?session;
????????this.ip?=?ip;
????????webSocketMap.put(ip,this);??//將當(dāng)前對(duì)象放入map中
????????addOnLineCount();??//在線人數(shù)加一
????????LOGGER.info("有新的連接加入,ip:{}!當(dāng)前在線人數(shù):{}",ip,getOnLineCount());
????}
????/**
?????*?連接關(guān)閉調(diào)用的方法,與前臺(tái)頁(yè)面的onClose相對(duì)應(yīng)
?????*?@param?ip
?????*/
????@OnClose
????public?void?onClose(@PathParam("ip")String?ip){
????????webSocketMap.remove(ip);??//根據(jù)ip(key)移除WebSocketServer對(duì)象
????????subOnLineCount();
????????LOGGER.info("WebSocket關(guān)閉,ip:{},當(dāng)前在線人數(shù):{}",ip,getOnLineCount());
????}
????/**
?????*?當(dāng)服務(wù)器接收到客戶端發(fā)送的消息時(shí)所調(diào)用的方法,與前臺(tái)頁(yè)面的onMessage相對(duì)應(yīng)
?????*?@param?message
?????*?@param?session
?????*/
????@OnMessage
????public?void?onMessage(String?message,Session?session){
????????//根據(jù)業(yè)務(wù),自定義邏輯實(shí)現(xiàn)
????????LOGGER.info("收到客戶端的消息:{}",message);
????}
????/**
?????*?發(fā)生錯(cuò)誤時(shí)調(diào)用,與前臺(tái)頁(yè)面的onError相對(duì)應(yīng)
?????*?@param?session
?????*?@param?error
?????*/
????@OnError
????public?void?onError(Session?session,Throwable?error){
????????LOGGER.error("WebSocket發(fā)生錯(cuò)誤");
????????error.printStackTrace();
????}
????/**
?????*?給當(dāng)前用戶發(fā)送消息
?????*?@param?message
?????*/
????public?void?sendMessage(String?message){
????????try{
????????????//getBasicRemote()是同步發(fā)送消息,這里我就用這個(gè)了,推薦大家使用getAsyncRemote()異步
????????????this.session.getBasicRemote().sendText(message);
????????}catch?(IOException?e){
????????????e.printStackTrace();
????????????LOGGER.info("發(fā)送數(shù)據(jù)錯(cuò)誤:,ip:{},message:{}",ip,message);
????????}
????}
????/**
?????*?給所有用戶發(fā)消息
?????*?@param?message
?????*/
????public?static?void?sendMessageAll(final?String?message){
????????//使用entrySet而不是用keySet的原因是,entrySet體現(xiàn)了map的映射關(guān)系,遍歷獲取數(shù)據(jù)更快。
????????Set>?entries?=?webSocketMap.entrySet();
????????for?(Map.Entry?entry?:?entries)?{
????????????final?WebSocketServer?webSocketServer?=?entry.getValue();
????????????//這里使用線程來(lái)控制消息的發(fā)送,這樣效率更高。
????????????new?Thread(new?Runnable()?{
????????????????public?void?run()?{
????????????????????webSocketServer.sendMessage(message);
????????????????}
????????????}).start();
????????}
????}
????/**
?????*?獲取當(dāng)前的連接數(shù)
?????*?@return
?????*/
????public?static?synchronized?int?getOnLineCount(){
????????return?WebSocketServer.onLineCount;
????}
????/**
?????*?有新的用戶連接時(shí),連接數(shù)自加1
?????*/
????public?static?synchronized?void?addOnLineCount(){
????????WebSocketServer.onLineCount++;
????}
????/**
?????*?斷開(kāi)連接時(shí),連接數(shù)自減1
?????*/
????public?static?synchronized?void?subOnLineCount(){
????????WebSocketServer.onLineCount--;
????}
????public?Session?getSession(){
????????return?session;
????}
????public?void?setSession(Session?session){
????????this.session?=?session;
????}
????public?static?ConcurrentHashMap?getWebSocketMap()? {
????????return?webSocketMap;
????}
????public?static?void?setWebSocketMap(ConcurrentHashMap?webSocketMap) ?{
????????WebSocketServer.webSocketMap?=?webSocketMap;
????}
}
然后寫(xiě)我們的定時(shí)器(quartz),這里我就不詳解定時(shí)器了。大家可以自行去了解。
這里我使用的是xml注解的方式,創(chuàng)建一個(gè)job類,此類不需要繼承任何類和實(shí)現(xiàn)任何接口。
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:?清風(fēng)一陣吹我心
?*?@ProjectName:?socket
?*?@Package:?com.socket.web.quartz
?*?@ClassName:?TestJob
?*?@Description:
?*?@Version:?1.0
?**/
public?class?TestJob?{
????public?void?task(){
????????//獲取WebSocketServer對(duì)象的映射。
????????ConcurrentHashMap?map?=?WebSocketServer.getWebSocketMap();
????????if?(map.size()?!=?0){
????????????for?(Map.Entry?entry?:?map.entrySet())?{
????????????????WebSocketServer?webSocketServer?=?entry.getValue();
????????????????try?{
????????????????????//向客戶端推送消息
????????????????????webSocketServer.getSession().getBasicRemote().sendText("每隔兩秒,向客戶端推送一次數(shù)據(jù)");
????????????????}catch?(IOException?e){
????????????????????e.printStackTrace();
????????????????}
????????????}
????????}else?{
????????????System.out.println("WebSocket未連接");
????????}
????}
}
定時(shí)器的實(shí)現(xiàn)類就完成了,我們還需要在springmvc.xml中進(jìn)行配置
springmvc.xml配置:
????<bean?id="testJob"?class="com.socket.web.quartz.TestJob">bean>
????
????<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>
接下來(lái)是controller層的代碼,就一個(gè)登錄的功能。
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:?清風(fēng)一陣吹我心
?*?@ProjectName:?socket
?*?@Package:?com.socket.web
?*?@ClassName:?ChatController
?*?@Description:
?*?@CreateDate:?2018/11/9?11:04
?*?@Version:?1.0
?**/
@RequestMapping("socket")
@Controller
public?class?ChatController?{
????/**
?????*?跳轉(zhuǎn)到登錄頁(yè)面
?????*?@return
?????*/
????@RequestMapping(value?=?"/login",method?=?RequestMethod.GET)
????public?String?goLogin(){
????????return?"login";
????}
????/**
?????*?跳轉(zhuǎn)到聊天頁(yè)面
?????*?@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";
????}
}
以上就是登錄的代碼了,基本上就是偽代碼,只要輸入用戶名就可以了,后面的邏輯,大家可以根據(jù)自己的業(yè)務(wù)來(lái)實(shí)現(xiàn)。
最后就是前臺(tái)頁(yè)面的設(shè)計(jì)了,登錄,login.jsp
<%@?page?contentType="text/html;charset=UTF-8"?language="java"?%>
var="path"?value="${pageContext.request.contextPath}"/>
????登錄
消息接收頁(yè)面,home.jsp
<%@?page?contentType="text/html;charset=UTF-8"?language="java"?%>
????聊天
????
????
基本上,數(shù)據(jù)推送的功能就完成了,下面附上效果圖。
啟動(dòng)tomcat。后臺(tái)定時(shí)器兩秒刷新一次,判斷是否有websocket連接。

登錄頁(yè)面:

數(shù)據(jù)推送頁(yè)面:

服務(wù)器定時(shí)向客戶端推送數(shù)據(jù)的功能就完成了,有不明白的可以給博主留言,如果有什么錯(cuò)誤,也希望各位朋友指出,謝謝大家。
本文源碼:
https://github.com/Qingfengchuiwoxin/websocket
