项目中想用做个实时统计,像是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的key
private final static String WS_SESSION_KEY = "session";
// 静态变量,用来记录当前在线连接数
private static volatile int onlineCount = 0;
@OnOpen
public 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));
}
@OnClose
public 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;
@Override
public 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 ;
}
......
};'''