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 ;

}

......

};‘‘‘

***

原文地址:https://www.cnblogs.com/changfanchangle/p/8808989.html

时间: 2024-10-07 09:58:11

websocket采坑记的相关文章

github 采坑记 —— 项目提交到github后部分文件缺失

在使用git push到GitHub上后,发现部分文件缺失,如下图所示: 可以看到dist文件夹为 运行 npm run build 之后打包生成的文件,node_modules 文件也是缺失的 导致文件没有提交的原因是在项目根目录下有个文件: 打开文件: 可以看再提交时有些文件被忽略了,可以将相应代码删除,然后重新push到GitHub上 想要的文件就提交上去了! 多多采坑,多多总结,多多练习~~~ 原文地址:https://www.cnblogs.com/amy2017/p/10087455

分布式TensorFlow 采坑记

单机版的TF没毛病,但是当大家在Tensorflow Github里面找到可用的模型,想分布式跑到时候,就会跑出来各种奇怪的问题.我尝试了几种不同构造TF的方式,算是成功渡过了踩坑期,特别记录一下.如果能帮助到各位TF boy最好. 方法一:自己手动写分布式协议 比如logistic regression 在master上运行的伪代码如下 with tf.Session('grpc://vm1:2222') as sess: sess.run(initialization) while not

aidl使用采坑记

什么是AIDL? AIDL是 Android Interface definition language的缩写,它是一种Android内部进程通信接口的描述语言,通过它我们可以定义进程间的通信接口 AIDL可以解决什么问题? 可以实现多个应用程序共享同一个Service的功能,比如:IM服务可以提供给多个APP使用,先在推送基本都是采取这种方案 可以跨进程调用服务里的方法 搭建了简单的Service框架 1.继承Service public class PushService extends S

AWS Redshift 采坑记

1.不要使用快速启动集群的方式建立,否则vpc是一个巨坑 2.要配置对应的Role 并配置化 role arn 原文地址:https://www.cnblogs.com/Johnson-zhao/p/10758126.html

php7采坑记:浮点型数据比较

今天在项目中遇到一个奇怪的问题,经过计算后的double类型的变量的值相等的两个变量进行比较,结果却是不相等. <?php $a=42735.04; $b=17806.2; $c=$a/36; $c=round($c,2); $d=$c*15; echo '$b value is: '. $b ."\n"; echo '$d value is: '. $d ."\n"; if($b == $d){     echo "ok\n"; }els

mpvue采坑记(同一个页面或者组件反复进入动态数据被覆盖)

该问题出现的issue:https://github.com/Meituan-Dianping/mpvue/issues/140 使用场景: 在使用mpvue开发小程序中,出现同路由复用,使用不同页面的情况. 例如: 进入:首页->商品详情页1(id=1)->(商品详情页的推荐商品列表进入)商品详情页2(id=2)->(商品详情页的推荐商品列表进入)商品详情页3(id=3) 退回:商品详情页3(id=3)<-商品详情页2(id=2)<-商品详情页1(id=1)<-首页

tensorflow 2.1 采坑记

tf 2.1 安装了好多遍,把python 从3.6 搞到了3.7还是没办法安装成功 问题出在这里 要使用这些新软件包,用户必须安装「Microsoft Visual C ++ Redistributable for Visual Studio 2015.2017 和 2019」,下载地址传送:https://support.microsoft.com/zh-cn/help/2977003/the-latest-supported-visual-c-downloads. 原文地址:https:/

zabbix 之安装采坑记

很久没有安装过zabbix,理论上应该是很简单,但是还是遇到好几个小问题,导致浪费了两个小时时间了要,特此记录一下 如果没有研发源码的能力,建议选择LTS版本 zabbix 4.0 官方安装文档: https://www.zabbix.com/documentation/4.0/zh/manual/installation/install 点击安装后无反应 ,发现是php命令没有安装, php安装不完全导致 yum install php php-opcache php-devel php-mb

UiAutomator2.0升级填坑记

UiAutomator2.0升级填坑记 SkySeraph May. 28th 2017 Email:[email protected] 更多精彩请直接访问SkySeraph个人站点:www.skyseraph.com 啰嗦 Google Android Developers 在2015年3月就发布了UiAutomator 2.0版本(下文简称U2),而公司的核心产品中用到还是UiAutomator老版本(下文简称U1),业界用U2的也不是很多,虽然有诸多问题和不便(如高版本OS中不支持Remo