• websocket采坑记


    项目中想用做个实时统计,像是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页面的登录怎么办?
    具体来说,有以下几个问题

    1. websocket连接建立时,判断httpsession是否已经登录了,未登录的时候,拒绝登录
    2. 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 ;
    }

    ......
    

    };'''


  • 相关阅读:
    XPOSED优秀模块列表 反射
    XPOSED优秀模块列表 ENABLE CALL RECORDING (三星启用通话录音)
    sp_Rename批量修改数据表的列名
    我的第一个sql server function
    js传递参数时是按照值传递的
    TreeView 绑定到深度未知的数据源
    silverlight中WCF服务定义终结点后可以方便部署
    Jquery常用方法合集,超实用
    sql 触发器 if条件判断
    如何用js判断document里的一个对象是否存在?或是是否有效
  • 原文地址:https://www.cnblogs.com/changfanchangle/p/8808989.html
Copyright © 2020-2023  润新知