一天,我的同事阿勇问我:你说如果tomcat session过期了,websocket还会继续往页面推送数据吗?
听到该问题后,尽管当时我的表面冷静沉着,但内心做着飞快的如下盘算。
首先我们常用的session是http请求通过cookie所存的sessionId在内存中寻找的,所以cookie/session机制是基于http协议的。
然后websocket只有在初始握手时期才使用http协议,之后就升级为ws协议了。
而ws协议和http协议同属在tcp协议之上的应用层协议,并没有隶属关系。
所以websocket应该是无法知道tomcat session过期的。
那么答案就很明显了:还会推送!
就在我即将脱口而出的时候,一个理智的声音在脑海中提醒我,等等!
如果session都失效了,websocket还可以推送消息,这不是bug吗?tomcat会允许这样的极不合理的现象存在吗?
理智告诉我,久经沙场的tomcat应该不会有这么明显的问题。
此时,我想到了一个叫狄仁杰的胖老头的口头禅:其中必有蹊跷。
于是,我缓缓对阿勇说,这个我研究一下,回头告诉你。
来都来了,先看下tomcat session的实现机制吧
首先要说的是tomcat从8.5版本之后就抛弃了BIO线程模型,默认采用NIO线程模型。
所以前期的流程是:Selector 线程将socket传递给Worker线程, 在Worker线程中,会完成从socket中读取http request,解析成HttpServletRequest对象,分派到相应的servlet并完成逻辑,然后将response通过socket发回client。
故事就发生在Request对象这里。
- package org.apache.catalina.connector;
-
- public class Request implements HttpServletRequest {
-
- protected Session session = null;
-
- protected String requestedSessionId = null;
-
- protected Cookie[] cookies = null;
-
- 。。。。。。
-
- }
request对象持有Session,Cookies,RequestSessionId等多个对象。
那么session是什么时候生成的呢?
在 Session在用户第一次访问服务器Servlet,jsp等动态资源就会被自动创建。创建流程如下:
首先会Request.getSession()方法会被调用。
- Request.java
-
- public HttpSession getSession() {
-
- //不存在session时会创建
-
- Session session = doGetSession(true);
-
- if (session == null) {
-
- return null;
-
- }
-
-
-
-
-
- return session.getSession();
-
- }
-
- //重点分析部分,session的创建过程
-
- protected Session doGetSession(boolean create) {
-
- //省略部分代码
-
- ......
-
- //如果session 已经存在,判断是否过期。
-
- if ((session != null) && !session.isValid()) {
-
- //如果已经过期,则置为null
-
- session = null;
-
- }
-
- //存在且没有过期,直接返回
-
- if (session != null) {
-
- return session;
-
- }
-
- //省略部分代码
-
- ......
-
- //判断request中的sessionId是否存在,该值是从request请求里的cookie或者url中解析出来
-
- if (requestedSessionId != null) {
-
- try {
-
- //根据requestSessionId 查找session
-
- session = manager.findSession(requestedSessionId);
-
- } catch (IOException e) {
-
- if (log.isDebugEnabled()) {
-
- log.debug(sm.getString("request.session.failed", requestedSessionId, e.getMessage()), e);
-
- } else {
-
- log.info(sm.getString("request.session.failed", requestedSessionId, e.getMessage()));
-
- }
-
- session = null;
-
- }
-
- //判断session是否过期
-
- if ((session != null) && !session.isValid()) {
-
- session = null;
-
- }
-
- if (session != null) {
-
- session.access();
-
- return session;
-
- }
-
- }
-
- //省略部分代码
-
- ......
-
- //根据sessionId创建session,如sessionId不存在,则在内部自动生成sessionId。
-
- session = manager.createSession(sessionId);
-
-
-
-
-
- if (session != null && trackModesIncludesCookie) {
-
- Cookie cookie = ApplicationSessionCookieConfig.createSessionCookie(
-
- context, session.getIdInternal(), isSecure());
-
- //session成功创建,将session对应sessionId 写入cookie
-
- response.addSessionCookieInternal(cookie);
-
- }
-
- return session;
-
- }
session是什么时候过期的呢?
在上面代码中我们看到有session.isValid()方法多次被调用,我们进去看看。
- StandardSession.java
-
- public boolean isValid() {
-
- //根据标识位快速判断
-
- if (!this.isValid) {
-
- return false;
-
- }
-
- //根据标识位快速判断
-
- if (this.expiring) {
-
- return true;
-
- }
-
- //根据标识位和使用次数快速判断
-
- if (ACTIVITY_CHECK && accessCount.get() > 0) {
-
- return true;
-
- }
-
- //配置中的session过期时间
-
- if (maxInactiveInterval > 0) {
-
- //计算从最后一次session被请求到现在的时间,为闲置时间
-
- int timeIdle = (int) (getIdleTimeInternal() / 1000L);
-
- //如果限制时间大于配置的session过期时间
-
- if (timeIdle >= maxInactiveInterval) {
-
- //进行过期操作。
-
- expire(true);
-
- }
-
- }
-
- return this.isValid;
-
- }
我们再看下session过期操作的内容
- StandardSession.java
-
- public void expire(boolean notify) {
-
- //双检锁
-
- if (!isValid) {
-
- return;
-
- }
-
- synchronized (this) {
-
- if (expiring || !isValid) {
-
- return;
-
- }
-
- if (manager == null) {
-
- return;
-
- }
-
- expiring = true;
-
- Context context = manager.getContext();
-
- //这里对session观察者进行通知session失效事件的通知
-
- if (notify) {
-
- ClassLoader oldContextClassLoader = null;
-
- try {
-
- oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null);
-
- Object listeners[] = context.getApplicationLifecycleListeners();
-
- if (listeners != null && listeners.length > 0) {
-
- HttpSessionEvent event =
-
- new HttpSessionEvent(getSession());
-
- for (int i = 0; i < listeners.length; i++) {
-
- int j = (listeners.length - 1) - i;
-
- if (!(listeners[j] instanceof HttpSessionListener)) {
-
- continue;
-
- }
-
- HttpSessionListener listener =
-
- (HttpSessionListener) listeners[j];
-
- try {
-
- context.fireContainerEvent("beforeSessionDestroyed",
-
- listener);
-
- listener.sessionDestroyed(event);
-
- context.fireContainerEvent("afterSessionDestroyed",
-
- listener);
-
- } catch (Throwable t) {
-
- ExceptionUtils.handleThrowable(t);
-
- try {
-
- context.fireContainerEvent(
-
- "afterSessionDestroyed", listener);
-
- } catch (Exception e) {
-
- // Ignore
-
- }
-
- manager.getContext().getLogger().error
-
- (sm.getString("standardSession.sessionEvent"), t);
-
- }
-
- }
-
- }
-
- } finally {
-
- context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader);
-
- }
-
- }
-
- if (ACTIVITY_CHECK) {
-
- accessCount.set(0);
-
- }
-
- //从manager中移出该session
-
- manager.remove(this, true);
-
-
-
- if (notify) {
-
- fireSessionEvent(Session.SESSION_DESTROYED_EVENT, null);
-
- }
-
- //设置标识位
-
- setValid(false);
-
- expiring = false;
-
-
-
- // 解除相关绑定
-
- String keys[] = keys();
-
- ClassLoader oldContextClassLoader = null;
-
- try {
-
- oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null);
-
- for (String key : keys) {
-
- removeAttributeInternal(key, notify);
-
- }
-
- } finally {
-
- context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader);
-
- }
-
- }
-
- }
总的来说,做了三件事,一是通知观察者session失效事件,二是从manager中移出失效session,三是解除该session的相关关联关系。
还有个疑问,这次session过期操作是通过http请求触发的,那如果该session过期用户再也不请求,session不就没有办法从内存中移出了嘛,这不就内存泄漏了吗?
所以此处肯定另有机关。
果不其然。
在tomcat启动时会启动一个background线程,对过期session进行清理。
- ManagerBase.java
-
- public void backgroundProcess() {
-
- count = (count + 1) % processExpiresFrequency;
-
- if (count == 0) {
-
- processExpires();
-
- }
-
- }
-
- public void processExpires() {
-
- long timeNow = System.currentTimeMillis();
-
- Session sessions[] = findSessions();
-
- int expireHere = 0 ;
-
- for (Session session : sessions) {
-
- if (session != null && !session.isValid()) {
-
- expireHere++;
-
- }
-
- }
-
- //省略部分代码
-
- ......
-
- }
这就说的通了,当请求触发时会判断session是否过期,如果过期则删除。另外还另起一个线程,对过期session进行以10秒一次的定期删除。
session搞清楚了,下面有请我们的今天的主角websocket登场。
websocket的会话保持用的也是session,不过这个session是WsSession,跟httpSession的关系呢,就是WsSession持有了HttpSession的Id。
代码如下:
- WsSession.java
-
- //父类Session并不是前文的org.apache.catalina.Session,而是javax.websocket.Session。两者没有关系
-
- public class WsSession implements Session {
-
- //省略部分代码
-
- ......
-
- private final WsWebSocketContainer webSocketContainer;
-
- private final URI requestUri;
-
- private final Map
> requestParameterMap; -
- private final String queryString;
-
- private final Principal userPrincipal;
-
- private final Map
pathParameters; -
- //持有httpSessionId
-
- private final String httpSessionId;
-
- //session状态标识位
-
- private volatile State state = State.OPEN;
-
- //校验session状态表示位
-
- private void checkState() {
-
- if (state == State.CLOSED) {
-
- /*
- * As per RFC 6455, a WebSocket connection is considered to be
- * closed once a peer has sent and received a WebSocket close frame.
- */
-
- throw new IllegalStateException(sm.getString("wsSession.closed", id));
-
- }
-
- }
在websocket client和server端交互的过程中,会多次调用checkState方法,而checkState方法是判断state属性。
那么我们看下state属性什么时候会发现变化,最终我们可以找到一个WsSessionListener。
- //实现了HttpSessionListener接口
-
- public class WsSessionListener implements HttpSessionListener{
-
- private final WsServerContainer wsServerContainer;
-
- public WsSessionListener(WsServerContainer wsServerContainer) {
-
- this.wsServerContainer = wsServerContainer;
-
- }
-
- @Override
-
- public void sessionDestroyed(HttpSessionEvent se) {
-
- wsServerContainer.closeAuthenticatedSession(se.getSession().getId());
-
- }
-
- }
-
-
-
- WsServerContainer.java
-
- ...省略其他方法
-
- //
-
- public void closeAuthenticatedSession(String httpSessionId) {
-
- Set
wsSessions = authenticatedSessions.remove(httpSessionId); -
-
-
-
-
- if (wsSessions != null && !wsSessions.isEmpty()) {
-
- for (WsSession wsSession : wsSessions) {
-
- try {
-
- //wsSession关闭
-
- wsSession.close(AUTHENTICATED_HTTP_SESSION_CLOSED);
-
- } catch (IOException e) {
-
- // Any IOExceptions during close will have been caught and the
-
- // onError method called.
-
- }
-
- }
-
- }
-
- }
-
- WsSession.java
-
- public void close(CloseReason closeReason) throws IOException {
-
- doClose(closeReason, closeReason);
-
- }
-
- public void doClose(CloseReason closeReasonMessage, CloseReason closeReasonLocal) {
-
- doClose(closeReasonMessage, closeReasonLocal, false);
-
- }
-
- public void doClose(CloseReason closeReasonMessage, CloseReason closeReasonLocal,
-
- boolean closeSocket) {
-
- //双检锁
-
- if (state != State.OPEN) {
-
- return;
-
- }
-
- synchronized (stateLock) {
-
- //已关闭,直接返回
-
- if (state != State.OPEN) {
-
- return;
-
- }
-
- if (log.isDebugEnabled()) {
-
- log.debug(sm.getString("wsSession.doClose", id));
-
- }
-
-
-
-
-
- //清理该WsSession绑定的相关的信息
-
- try {
-
- wsRemoteEndpoint.setBatchingAllowed(false);
-
- } catch (IOException e) {
-
- log.warn(sm.getString("wsSession.flushFailOnClose"), e);
-
- fireEndpointOnError(e);
-
- }
-
-
-
- //将state属性值为关闭
-
- if (state != State.OUTPUT_CLOSED) {
-
- state = State.OUTPUT_CLOSED;
-
- sendCloseMessage(closeReasonMessage);
-
- if (closeSocket) {
-
- wsRemoteEndpoint.close();
-
- }
-
- fireEndpointOnClose(closeReasonLocal);
-
- }
-
- }
-
- 。。。
-
- }
那么我们看到state属性在WsSessionListener中进行了关闭,那么WsSessionListener又是什么时候触发的呢?
前面代码中我们看到WsSessionListener实现了HttpSessionListener接口。
等等,这个HttpSessionListener很眼熟嘛,听口音像是老乡,有点印象。
果然,往前一翻,就发现了在之前的StandardSession.java的分析中,就有过一面之缘。
让我们还原一下当时初次见面时的的情景。
- StandardSession.java
-
- public void expire(boolean notify) {
-
- //双检锁
-
- if (!isValid) {
-
- return;
-
- }
-
- synchronized (this) {
-
- if (expiring || !isValid) {
-
- return;
-
- }
-
- if (manager == null) {
-
- return;
-
- }
-
- expiring = true;
-
- Context context = manager.getContext();
-
- //这里对session观察者进行通知session失效事件的通知
-
- if (notify) {
-
- ClassLoader oldContextClassLoader = null;
-
- try {
-
- oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null);
-
- Object listeners[] = context.getApplicationLifecycleListeners();
-
- if (listeners != null && listeners.length > 0) {
-
- HttpSessionEvent event =
-
- new HttpSessionEvent(getSession());
-
- for (int i = 0; i < listeners.length; i++) {
-
- int j = (listeners.length - 1) - i;
-
- if (!(listeners[j] instanceof HttpSessionListener)) {
-
- continue;
-
- }
-
- //就是这里,在http Session过期时,对HttpSessionListener进行了通知
-
- HttpSessionListener listener =
-
- (HttpSessionListener) listeners[j];
-
- try {
-
- context.fireContainerEvent("beforeSessionDestroyed",
-
- listener);
-
- listener.sessionDestroyed(event);
-
- context.fireContainerEvent("afterSessionDestroyed",
-
- listener);
-
- } catch (Throwable t) {
-
- ExceptionUtils.handleThrowable(t);
-
- try {
-
- context.fireContainerEvent(
-
- "afterSessionDestroyed", listener);
-
- } catch (Exception e) {
-
- // Ignore
-
- }
-
- manager.getContext().getLogger().error
-
- (sm.getString("standardSession.sessionEvent"), t);
-
- }
-
- }
-
- }
-
- } finally {
-
- context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader);
-
- }
-
- }
-
- 。。。
-
- }
这样情况就明了了,http Session 失效时会通过观察者模式通知 WsSessionListener对对应httpSessionId的WsSession进行过期处理,这样WsSession也就跟随着HttpSession一同失效了。
所以开始的问题的答案也就浮出了水面:一旦http Session 过期了,webService就会停止信息的推送。
镜头转回来,我缓缓对阿勇说:事情的来龙去脉就是这样。