项目中想用做个实时统计,像是110警情大屏那种,所以用到了websocket,结果踩了不少坑,再次记录下。
环境:spring,springMVC(4.2.4.RELEASE),tomcat7
问题1:session对象是不一样的
http的时候,是javax.servlet.http.HttpSession
而websocket的时候javax.websocket.Session
http的session一般用于保存用户信息,根据用户,http请求的时候携带Cookie:JSESSIONID,进行区分,实现多例。http的session的getId()就是JSESSIONID,比如BD70D706E4B975EA5CE7418152D3F8DC这种。
而websocket的Session则有很多用处,保存信息,发送请求,可以说websocket前后端交互用的变量和方法,都保存在websocket的Session里。
同时,websocket的Session是根据前端创建连接多例的,也就是说,前端每new WebSocket进行open一次,就创建一个websocket的Session。websocket的Session的getId()是从1开始递增的序列。
关于http的session和websocket的session同步的问题。
场景:系统本身是标准的web项目,但是要求追加一个运营统计功能,实时监视数据变化,这时选择了更能体现实时的websocket,但是原本用户页面和websocket页面的登录怎么办?
具体来说,有以下几个问题
- websocket连接建立时,判断httpsession是否已经登录了,未登录的时候,拒绝登录
- httpsession退出的时候,断开websocket的连接
目前解决方式:
首先,创建对应的Configurator对象,在对象中临时保存httpsession
‘‘‘JAVApublic class WebsocketSessionConfigurator extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) {
// 建立websocket连接的时候,保存建立连接使用时候的session
HttpSession httpSession = (HttpSession) request.getHttpSession();
if (null != httpSession) {
config.getUserProperties().put(HttpSession.class.getName(), httpSession);
}
}
}‘‘‘
然后,在@ServerEndpoint中使用Configurator,并在onopen中将其保存在websocket的session中保存httpsession
‘‘‘JAVA
@Component
@ServerEndpoint(value = "/xxxx", configurator = WebsocketSessionConfigurator.class)
public class xxxxWebSocketController extends BaseWebSocketController {
// ***重要***// websocket的session连接对象用类集合(线程安全)private static Map<String, Session> wsSessionMap = Maps.newConcurrentMap();// httpsession对应websocket的session用private static Map<String, Set<String>> httpToWsSessionMap = Maps.newConcurrentMap();// 保存在wsSession中的httpsession的keyprivate final static String WS_SESSION_KEY = "session";// 静态变量,用来记录当前在线连接数private static volatile int onlineCount = 0;@OnOpenpublic void onOpen(@PathParam("param") String param, Session wsSession, EndpointConfig config) throws IOException { ???if (!config.getUserProperties().containsKey(HttpSession.class.getName())){ ???????// 未登录时 ???????wsSession.getAsyncRemote().sendText(ToolsUtil.strToJson("未登录,请先登录","99")); ???????wsSession.close(); ?// 后端主动断开会引发异常,改为前端判断后前端断开 ???????return ; ???} ???// httpsessinn保存 ???HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName()); ???wsSession.getUserProperties().put(WS_SESSION_KEY, httpSession); ???wsSessionMap.put(wsSession.getId(),wsSession); ???// http的session和websocket的session对应 ???if (!httpToWsSessionMap.containsKey(httpSession.getId())){ ???????httpToWsSessionMap.put(httpSession.getId(),Sets.newCopyOnWriteArraySet()); ???} ???httpToWsSessionMap.get(httpSession.getId()).add(wsSession.getId()); ???addOnlineCount(); ???// log信息 ???wsSession.getAsyncRemote().sendText(ToolsUtil.strToJson("登录成功","90")); ???System.out.println(MessageFormat.format("wsSession id:{0} ?httpSession id:{1} 的用户已登录websocket,当前连接数:{2}",wsSession.getId(), httpSession.getId(), onlineCount));}@OnClosepublic void onClose(Session wsSession, CloseReason reason) { ???if(wsSessionMap.containsKey(wsSession.getId())){ ???// 用户信息取得 ???HttpSession httpSession = (HttpSession)wsSession.getUserProperties().get(WS_SESSION_KEY); ???String wsSessionId = wsSession.getId(); ???// 退出处理 ???wsSessionMap.remove(wsSession.getId()); ???subOnlineCount(); ???if (httpToWsSessionMap.containsKey(httpSession.getId())){ ???????Set<String> setSession = httpToWsSessionMap.get(httpSession.getId()); ???????setSession.remove(wsSession.getId()); ???????if(setSession.size() == 0){ ???????????httpToWsSessionMap.remove(httpSession.getId()); ???????} ???} ???// log信息 ???System.out.println(MessageFormat.format("wsSession id:{0} ?httpSession id:{1} 的用户已退出websocket,当前连接数:{2}",wsSessionId, httpSession.getId(), onlineCount));}public static void disconnectByHttpsession(String httpsessionId) { ???sendMsgByHttpsession(httpsessionId,ToolsUtil.strToJson("用户登出","99"));}
}
‘‘‘
最后,当系统注销,进行登出的时候,注销。
‘‘‘JAVA
@ResponseBody
@RequestMapping(value="/exit")
public String exit(HttpServletRequest req,
HttpServletResponse resp){
HttpSession session = req.getSession(false);//防止创建Session
if(session != null){
session.removeAttribute(session.getId());
// 断开websocket连接
xxxxWebSocketController.disconnectByHttpsession(session.getId());
}
return ToolsUtil.strToJson("ok");
}‘‘‘
问题2:在@ServerEndpoint中注入server失败
在实际使用中,发现@Autowired来注入Service无效,报出空指针异常。
思考后,觉得这也是正常,ServerEndpoint的websocket对象,实际是创建一个websocket连接时创建的,而@Autowired是web项目启动,初始化时的事情。采坑,调查后,发现有两种解决方法,根据spring初始化方法请自行选择:
1,在web.xml中使用了ContextLoaderListener时(使用spring的方法初始化bean),只需要@ServerEndpoint的configurator使用SpringConfigurator,或者自定义的configurator继承SpringConfigurator。
‘‘‘JAVA
@ServerEndpoint(value = "/xxxxx", configurator = SpringConfigurator.class)
public class xxxxxWebSocketController extends BaseWebSocketController {
......
}
‘‘‘
或者
‘‘‘JAVA
public class WebsocketSessionConfigurator extends SpringConfigurator {
@Override
public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) {
.......
}
}‘‘‘
2,如果web.xml中使用DispatcherServlet,而没有用ContextLoaderListener时(springMVC的方式初始化bean)。推荐使用实现ApplicationContextAware,创建工具类的方法来创建bean:
创建类对象
‘‘‘JAVA
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
public class SpringContextHelper implements ApplicationContextAware {
private static ApplicationContext context = null;
@Overridepublic void setApplicationContext(ApplicationContext applicationContext) ???????throws BeansException { ???context = applicationContext;}public static Object getBean(String name){ ???return context.getBean(name);}
}
‘‘‘
bean定义:
<!--Spring中bean获取的工具类--><bean id="springContextUtils" class="com.utils.websocket.SpringContextHelper" />
使用:
‘‘‘JAVA
xxxService = (xxxService) SpringContextHelper.getBean("xxxService");
‘‘‘
问题3:在onopen中调用close()发生异常
场景:判断用户是否已经登录,未登录时拒绝连接。
在onopen中调用websocket的Session的close()后,连接正常断开,也执行了onclose,但是这之后报以下异常:
‘‘‘JAVAjava.lang.IllegalStateException: The WebSocket session has been closed and no method (apart from close()) may be called on a closed session
at org.apache.tomcat.websocket.WsSession.checkState(WsSession.java:643)
at org.apache.tomcat.websocket.WsSession.addMessageHandler(WsSession.java:168)
at org.apache.tomcat.websocket.pojo.PojoEndpointBase.doOnOpen(PojoEndpointBase.java:81)
at org.apache.tomcat.websocket.pojo.PojoEndpointServer.onOpen(PojoEndpointServer.java:70)
at org.apache.tomcat.websocket.server.WsHttpUpgradeHandler.init(WsHttpUpgradeHandler.java:129)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:629)
at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:310)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)‘‘‘
网上没有找到比较合适的答案,猜测是onopen的时候,判断是tomcat在onopen的时候,对close的处理还不充分,但是websocket本身是由前端发起的,所以转换思路,改为通过向前端发送特定的结果来使前端主动执行close方法来关闭连接,这样一来,前端发起,前端关闭。
‘‘‘JAVAsocket = new WebSocket(url);
socket.onopen = function(evt){
if (evt.data == "") {
return "";
}
// 约定,99为登出用的errorcode
var data = JSON.parse(evt.data)
if(data.code == "99"){
evt.target.close();
alert(data.code + ":" + data.data);
return ;
}
if(data.code != "00"){
console.log(data.code + ":" + data.data);
return ;
}
......
};‘‘‘
***
websocket采坑记
原文地址:https://www.cnblogs.com/changfanchangle/p/8808989.html