目录
一、需求(前台消息实时推送)
后台管理员创建公告,前台实时更新
二、前端代码
$(function () {
init();
//启动公告管理消息WebSocket
startNoticeCountWebSocket();
})
var noticeWebSocketInterval = null; //websocket重连
var noticeWs=null;
//公告管理消息WebSocket
function startNoticeCountWebSocket() {
var wsPath = $('#wsPath').val();
if ("WebSocket" in window){
noticeWs=new WebSocket(wsPath + "noticeCountWebSocket");
}
//打开连接
noticeWs.onopen=function(event){
if(noticeWebSocketInterval != null) {
console.log("公告管理重连服务.. \n");
clearInterval(noticeWebSocketInterval);
noticeWebSocketInterval = null; //已经连接就把重连任务去除
}
console.log("公告管理连接已建立..\n");
}
//接收到数据
noticeWs.onmessage=function(event){
var res = JSON.parse(event.data);
if(res && res.success){
if(res.obj.noticeSum != 0){
$('#noticeSum').replaceWith('<div id="noticeSum" class="backred">'+res.obj.noticeSum+'</div>');
$("#noticeImg.change").addClass('change');
}else{
$("#noticeImg.change").removeClass('change');
$("#noticeSum.backred").css("background","transparent");
}
} else {
$.messager.alert("提示","公告信息加载失败!","info");
}
}
//关闭连接
noticeWs.onclose=function(){
console.log("公告管理连接已关闭..\n");
ws.close();
startNoticeReconnectInterval();
}
//连接发生错误
noticeWs.onerror = function(){
console.log("公告管理连接发生了错误..\n");
}
}
//重启公告管理
function startNoticeReconnectInterval() {
if(noticeWebSocketInterval == null) {
noticeWebSocketInterval = setInterval(function() {
console.log("尝试启动公告管理重连服务...\n");
startNoticeCountWebSocket();
},30000);
}
}
三、后台代码
1.如何获取HttpSession(可略过)
由于websocket的协议与Http协议是不同的,所以造成了无法直接拿到session
先来看一下@ServerEndpoint注解的源码,我们看到最后的一个方法,也就是加粗的方法。可以看到,它要求返回一个ServerEndpointConfig.Configurator的子类,我们写一个类去继承它。
package javax.websocket.server;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.websocket.Decoder;
import javax.websocket.Encoder;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ServerEndpoint {
/**
* URI or URI-template that the annotated class should be mapped to.
* @return The URI or URI-template that the annotated class should be mapped
* to.
*/
String value();
String[] subprotocols() default {};
Class<? extends Decoder>[] decoders() default {};
Class<? extends Encoder>[] encoders() default {};
public Class<? extends ServerEndpointConfig.Configurator> configurator()
default ServerEndpointConfig.Configurator.class;
}
当我们覆盖modifyHandshake方法时,可以看到三个参数,其中后面两个参数让我们感觉有点见过的感觉,我们查看一HandshakeRequest的源码
package javax.websocket.server;
import java.net.URI;
import java.security.Principal;
import java.util.List;
import java.util.Map;
/**
* Represents the HTTP request that asked to be upgraded to WebSocket.
*/
public interface HandshakeRequest {
static final String SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key";
static final String SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol";
static final String SEC_WEBSOCKET_VERSION = "Sec-WebSocket-Version";
static final String SEC_WEBSOCKET_EXTENSIONS= "Sec-WebSocket-Extensions";
Map<String,List<String>> getHeaders();
Principal getUserPrincipal();
URI getRequestURI();
boolean isUserInRole(String role);
/**
* Get the HTTP Session object associated with this request. Object is used
* to avoid a direct dependency on the Servlet API.
* @return The javax.servlet.http.HttpSession object associated with this
* request, if any.
*/
Object getHttpSession();
Map<String, List<String>> getParameterMap();
String getQueryString();
}
我们发现它是一个接口,接口中规范了这样的一个方法 ,上面有相应的注释,说明可以从Servlet API中获取到相应的HttpSession
/**
* Get the HTTP Session object associated with this request. Object is used
* to avoid a direct dependency on the Servlet API.
* @return The javax.servlet.http.HttpSession object associated with this
* request, if any.
*/
Object getHttpSession();
其实通过上面的源码分析,你们应该知道了HttpSession的获取。但是下面又多了一行代码
sec.getUserProperties().put(HttpSession.class.getName(), httpSession);
这行代码又是什么意思呢?我们看一下ServerEnpointConfig的声明,我们发现这个接口继承了EndpointConfig的接口,好,我们看一下EndpointConfig的源码
public interface ServerEndpointConfig extends EndpointConfig
package javax.websocket;
import java.util.List;
import java.util.Map;
public interface EndpointConfig {
List<Class<? extends Encoder>> getEncoders();
List<Class<? extends Decoder>> getDecoders();
Map<String,Object> getUserProperties();
}
我们发现了这样的一个方法定义
Map<String,Object> getUserProperties();
可以看到,它是一个map,从方法名也可以理解到,它是用户的一些属性的存储,那既然它提供了get方法,那么也就意味着我们可以拿到这个map,并且对这里面的值进行操作,所以就有了上面的
sec.getUserProperties().put(HttpSession.class.getName(), httpSession);
2.获取HttpSession
public class WebSocketServerConfigurator extends Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
HttpSession httpSession = (HttpSession) request.getHttpSession();
if(httpSession != null) { //有可能存在session有问题的情况(比如服务器后台重启)
sec.getUserProperties().put(HttpSession.class.getName(), httpSession);
}
}
}
3.接收前端请求并发送数据
/**
* @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端,
* 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
*/
@ServerEndpoint(value ="/noticeCountWebSocket",configurator = WebSocketServerConfigurator.class)
public class NoticeCountWebSocket {
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
private static CopyOnWriteArraySet<NoticeCountWebSocket> webSocketSet = new CopyOnWriteArraySet<NoticeCountWebSocket>();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
//是否发送
private boolean sendFlag = true;
/**
* 连接建立成功调用的方法
* @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
@OnOpen
public void onOpen(Session session,EndpointConfig config){
this.session = session;
webSocketSet.add(this); //加入set中
addOnlineCount(); //在线数加1
System.out.println("有新连接加入!当前在线人数为" + getOnlineCount());
//打开之后每两分钟向前台发送数据
try{
while(sendFlag) {
SysNoticeServiceI noticeServiceI = (SysNoticeServiceI) ContextLoader.getCurrentWebApplicationContext().getBean("sysNoticeServiceImpl");
HttpSession httpSession= (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
Long sumNotices = noticeServiceI.getSumNotices(httpSession);
Json jsonPush = new Json();
jsonPush.setSuccess(true);
Map map=new HashMap();
map.put("noticeSum",sumNotices);
jsonPush.setObj(map);
String alarmInfo = JSONObject.toJSONString(jsonPush);
session.getBasicRemote().sendText(alarmInfo);
Thread.sleep(Constants.WEBSOCKET_INDEX_TIMES); //停止30秒
}
}catch (Exception e){
e.printStackTrace();
System.out.println("发送错误内容:" + e.getMessage());
}
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(){
webSocketSet.remove(this); //从set中删除
subOnlineCount(); //在线数减1
System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
sendFlag = false; //停止线程
}
/**
* 收到客户端消息后调用的方法
* @param message 客户端发送过来的消息
* @param session 可选的参数
*/
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("来自客户端的消息:" + message);
//群发消息
for(NoticeCountWebSocket item: webSocketSet){
try {
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
continue;
}
}
}
/**
* 发生错误时调用
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error){
System.out.println("发生错误");
error.printStackTrace();
}
/**
* 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。
* @param message
* @throws IOException
*/
public void sendMessage(String message) throws IOException{
this.session.getBasicRemote().sendText(message);
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
NoticeCountWebSocket.onlineCount++;
}
public static synchronized void subOnlineCount() {
NoticeCountWebSocket.onlineCount--;
}
}
四、Websocket url 404错误
修改tomcat的\conf\context.xml文件,在<Context>中添加如下代码
<Loader delegate="true"></Loader>