• 一场技术破案的经过


    源起:

    一天,我的同事阿勇问我:你说如果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对象这里。

    1. package org.apache.catalina.connector;
    2. public class Request implements HttpServletRequest {
    3. protected Session session = null;
    4. protected String requestedSessionId = null;
    5. protected Cookie[] cookies = null;
    6. 。。。。。。
    7. }

    request对象持有Session,Cookies,RequestSessionId等多个对象。

    那么session是什么时候生成的呢?

    在 Session在用户第一次访问服务器Servlet,jsp等动态资源就会被自动创建。创建流程如下:

    首先会Request.getSession()方法会被调用。

    1. Request.java
    2. public HttpSession getSession() {
    3.     //不存在session时会创建
    4.     Session session = doGetSession(true);
    5.     if (session == null) {
    6.         return null;
    7.     }
    8.     return session.getSession();
    9. }
    10. //重点分析部分,session的创建过程
    11. protected Session doGetSession(boolean create) {
    12.     //省略部分代码
    13.     ......
    14.     //如果session 已经存在,判断是否过期。
    15.     if ((session != null) && !session.isValid()) {
    16.     //如果已经过期,则置为null
    17.         session = null;
    18.     }
    19.    //存在且没有过期,直接返回
    20.     if (session != null) {
    21.         return session;
    22.     }
    23.     //省略部分代码
    24.     ......
    25.     //判断request中的sessionId是否存在,该值是从request请求里的cookie或者url中解析出来
    26.     if (requestedSessionId != null) {
    27.         try {
    28.             //根据requestSessionId 查找session
    29.             session = manager.findSession(requestedSessionId);
    30.         } catch (IOException e) {
    31.             if (log.isDebugEnabled()) {
    32.                 log.debug(sm.getString("request.session.failed", requestedSessionId, e.getMessage()), e);
    33.             } else {
    34.                 log.info(sm.getString("request.session.failed", requestedSessionId, e.getMessage()));
    35.             }
    36.             session = null;
    37.         }
    38.         //判断session是否过期
    39.         if ((session != null) && !session.isValid()) {
    40.             session = null;
    41.         }
    42.         if (session != null) {
    43.             session.access();
    44.             return session;
    45.         }
    46.     }
    47.     //省略部分代码
    48.     ......
    49.     //根据sessionId创建session,如sessionId不存在,则在内部自动生成sessionId。
    50.     session = manager.createSession(sessionId);
    51.    
    52.     if (session != null && trackModesIncludesCookie) {
    53.         Cookie cookie = ApplicationSessionCookieConfig.createSessionCookie(
    54.                 context, session.getIdInternal(), isSecure());
    55.         //session成功创建,将session对应sessionId 写入cookie
    56.         response.addSessionCookieInternal(cookie);
    57.     }
    58.     return session;
    59. }

    session是什么时候过期的呢?

    在上面代码中我们看到有session.isValid()方法多次被调用,我们进去看看。

    1. StandardSession.java
    2. public boolean isValid() {
    3.     //根据标识位快速判断
    4.     if (!this.isValid) {
    5.         return false;
    6.     }
    7.     //根据标识位快速判断
    8.     if (this.expiring) {
    9.         return true;
    10.     }
    11.     //根据标识位和使用次数快速判断
    12.     if (ACTIVITY_CHECK && accessCount.get() > 0) {
    13.         return true;
    14.     }
    15.     //配置中的session过期时间
    16.     if (maxInactiveInterval > 0) {
    17.         //计算从最后一次session被请求到现在的时间,为闲置时间
    18.         int timeIdle = (int) (getIdleTimeInternal() / 1000L);
    19.         //如果限制时间大于配置的session过期时间
    20.         if (timeIdle >= maxInactiveInterval) {
    21.             //进行过期操作。
    22.             expire(true);
    23.         }
    24.     }
    25.     return this.isValid;
    26. }

    我们再看下session过期操作的内容

    1. StandardSession.java
    2. public void expire(boolean notify) {
    3.     //双检锁
    4.     if (!isValid) {
    5.         return;
    6.     }
    7.     synchronized (this) {
    8.         if (expiring || !isValid) {
    9.             return;
    10.         }
    11.         if (manager == null) {
    12.             return;
    13.         }
    14.         expiring = true;
    15.         Context context = manager.getContext();
    16.         //这里对session观察者进行通知session失效事件的通知
    17.         if (notify) {
    18.             ClassLoader oldContextClassLoader = null;
    19.             try {
    20.                 oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null);
    21.                 Object listeners[] = context.getApplicationLifecycleListeners();
    22.                 if (listeners != null && listeners.length > 0) {
    23.                     HttpSessionEvent event =
    24.                         new HttpSessionEvent(getSession());
    25.                     for (int i = 0; i < listeners.length; i++) {
    26.                         int j = (listeners.length - 1) - i;
    27.                         if (!(listeners[j] instanceof HttpSessionListener)) {
    28.                             continue;
    29.                         }
    30.                         HttpSessionListener listener =
    31.                             (HttpSessionListener) listeners[j];
    32.                         try {
    33.                             context.fireContainerEvent("beforeSessionDestroyed",
    34.                                     listener);
    35.                             listener.sessionDestroyed(event);
    36.                             context.fireContainerEvent("afterSessionDestroyed",
    37.                                     listener);
    38.                         } catch (Throwable t) {
    39.                             ExceptionUtils.handleThrowable(t);
    40.                             try {
    41.                                 context.fireContainerEvent(
    42.                                         "afterSessionDestroyed", listener);
    43.                             } catch (Exception e) {
    44.                                 // Ignore
    45.                             }
    46.                             manager.getContext().getLogger().error
    47.                                 (sm.getString("standardSession.sessionEvent"), t);
    48.                         }
    49.                     }
    50.                 }
    51.             } finally {
    52.                 context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader);
    53.             }
    54.         }
    55.         if (ACTIVITY_CHECK) {
    56.             accessCount.set(0);
    57.         }
    58.                 //从manager中移出该session
    59.         manager.remove(this, true);
    60.         if (notify) {
    61.             fireSessionEvent(Session.SESSION_DESTROYED_EVENT, null);
    62.         }
    63.                 //设置标识位
    64.         setValid(false);
    65.         expiring = false;
    66.         // 解除相关绑定
    67.         String keys[] = keys();
    68.         ClassLoader oldContextClassLoader = null;
    69.         try {
    70.             oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null);
    71.             for (String key : keys) {
    72.                 removeAttributeInternal(key, notify);
    73.             }
    74.         } finally {
    75.             context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader);
    76.         }
    77.     }
    78. }

    总的来说,做了三件事,一是通知观察者session失效事件,二是从manager中移出失效session,三是解除该session的相关关联关系。

    还有个疑问,这次session过期操作是通过http请求触发的,那如果该session过期用户再也不请求,session不就没有办法从内存中移出了嘛,这不就内存泄漏了吗?

    所以此处肯定另有机关

    果不其然。

    在tomcat启动时会启动一个background线程,对过期session进行清理。

    1. ManagerBase.java
    2. public void backgroundProcess() {
    3.     count = (count + 1) % processExpiresFrequency;
    4.     if (count == 0) {
    5.         processExpires();
    6.     }
    7. }
    8. public void processExpires() {
    9.     long timeNow = System.currentTimeMillis();
    10.     Session sessions[] = findSessions();
    11.     int expireHere = 0 ;
    12.     for (Session session : sessions) {
    13.         if (session != null && !session.isValid()) {
    14.             expireHere++;
    15.         }
    16.     }
    17.         //省略部分代码
    18.     ......
    19. }

    这就说的通了,当请求触发时会判断session是否过期,如果过期则删除。另外还另起一个线程,对过期session进行以10秒一次的定期删除。

    session搞清楚了,下面有请我们的今天的主角websocket登场。

    websocket的会话保持用的也是session,不过这个session是WsSession,跟httpSession的关系呢,就是WsSession持有了HttpSession的Id。

    代码如下:

    1. WsSession.java
    2. //父类Session并不是前文的org.apache.catalina.Session,而是javax.websocket.Session。两者没有关系
    3. public class WsSession implements Session {
    4.     //省略部分代码
    5.     ......
    6.     private final WsWebSocketContainer webSocketContainer;
    7.     private final URI requestUri;
    8.     private final Map> requestParameterMap;
    9.     private final String queryString;
    10.     private final Principal userPrincipal;
    11.     private final Map pathParameters;
    12.     //持有httpSessionId
    13.     private final String httpSessionId;
    14.     //session状态标识位
    15.     private volatile State state = State.OPEN;
    16.     //校验session状态表示位
    17.     private void checkState() {
    18.         if (state == State.CLOSED) {
    19.         /*
    20.          * As per RFC 6455, a WebSocket connection is considered to be
    21.          * closed once a peer has sent and received a WebSocket close frame.
    22.          */
    23.         throw new IllegalStateException(sm.getString("wsSession.closed", id));
    24.         }
    25.     }

    在websocket client和server端交互的过程中,会多次调用checkState方法,而checkState方法是判断state属性。

    那么我们看下state属性什么时候会发现变化,最终我们可以找到一个WsSessionListener。

    1. //实现了HttpSessionListener接口
    2. public class WsSessionListener implements HttpSessionListener{
    3.     private final WsServerContainer wsServerContainer;
    4.     public WsSessionListener(WsServerContainer wsServerContainer) {
    5.         this.wsServerContainer = wsServerContainer;
    6.     }
    7.     @Override
    8.     public void sessionDestroyed(HttpSessionEvent se) {
    9.         wsServerContainer.closeAuthenticatedSession(se.getSession().getId());
    10.     }
    11. }
    12. WsServerContainer.java
    13. ...省略其他方法
    14. //
    15. public void closeAuthenticatedSession(String httpSessionId) {
    16.     Set wsSessions = authenticatedSessions.remove(httpSessionId);
    17.     if (wsSessions != null && !wsSessions.isEmpty()) {
    18.         for (WsSession wsSession : wsSessions) {
    19.             try {
    20.                                 //wsSession关闭
    21.                 wsSession.close(AUTHENTICATED_HTTP_SESSION_CLOSED);
    22.             } catch (IOException e) {
    23.                 // Any IOExceptions during close will have been caught and the
    24.                 // onError method called.
    25.             }
    26.         }
    27.     }
    28. }
    29. WsSession.java
    30. public void close(CloseReason closeReason) throws IOException {
    31.     doClose(closeReason, closeReason);
    32. }
    33. public void doClose(CloseReason closeReasonMessage, CloseReason closeReasonLocal) {
    34.     doClose(closeReasonMessage, closeReasonLocal, false);
    35. }
    36. public void doClose(CloseReason closeReasonMessage, CloseReason closeReasonLocal,
    37.         boolean closeSocket) {
    38.     //双检锁
    39.     if (state != State.OPEN) {
    40.         return;
    41.     }
    42.     synchronized (stateLock) {
    43.      //已关闭,直接返回
    44.         if (state != State.OPEN) {
    45.             return;
    46.         }
    47.         if (log.isDebugEnabled()) {
    48.             log.debug(sm.getString("wsSession.doClose", id));
    49.         }
    50.         //清理该WsSession绑定的相关的信息
    51.         try {
    52.             wsRemoteEndpoint.setBatchingAllowed(false);
    53.         } catch (IOException e) {
    54.             log.warn(sm.getString("wsSession.flushFailOnClose"), e);
    55.             fireEndpointOnError(e);
    56.         }
    57.         //将state属性值为关闭
    58.         if (state != State.OUTPUT_CLOSED) {
    59.             state = State.OUTPUT_CLOSED;
    60.             sendCloseMessage(closeReasonMessage);
    61.             if (closeSocket) {
    62.                 wsRemoteEndpoint.close();
    63.             }
    64.             fireEndpointOnClose(closeReasonLocal);
    65.         }
    66.     }
    67.         。。。
    68. }

    那么我们看到state属性在WsSessionListener中进行了关闭,那么WsSessionListener又是什么时候触发的呢?

    前面代码中我们看到WsSessionListener实现了HttpSessionListener接口。

    等等,这个HttpSessionListener很眼熟嘛,听口音像是老乡,有点印象。

    果然,往前一翻,就发现了在之前的StandardSession.java的分析中,就有过一面之缘。

    让我们还原一下当时初次见面时的的情景。

    1. StandardSession.java
    2. public void expire(boolean notify) {
    3.     //双检锁
    4.     if (!isValid) {
    5.         return;
    6.     }
    7.     synchronized (this) {
    8.         if (expiring || !isValid) {
    9.             return;
    10.         }
    11.         if (manager == null) {
    12.             return;
    13.         }
    14.         expiring = true;
    15.         Context context = manager.getContext();
    16.         //这里对session观察者进行通知session失效事件的通知
    17.         if (notify) {
    18.             ClassLoader oldContextClassLoader = null;
    19.             try {
    20.                 oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null);
    21.                 Object listeners[] = context.getApplicationLifecycleListeners();
    22.                 if (listeners != null && listeners.length > 0) {
    23.                     HttpSessionEvent event =
    24.                         new HttpSessionEvent(getSession());
    25.                     for (int i = 0; i < listeners.length; i++) {
    26.                         int j = (listeners.length - 1) - i;
    27.                         if (!(listeners[j] instanceof HttpSessionListener)) {
    28.                             continue;
    29.                         }
    30.                  //就是这里,在http Session过期时,对HttpSessionListener进行了通知
    31.                         HttpSessionListener listener =
    32.                             (HttpSessionListener) listeners[j];
    33.                         try {
    34.                             context.fireContainerEvent("beforeSessionDestroyed",
    35.                                     listener);
    36.                             listener.sessionDestroyed(event);
    37.                             context.fireContainerEvent("afterSessionDestroyed",
    38.                                     listener);
    39.                         } catch (Throwable t) {
    40.                             ExceptionUtils.handleThrowable(t);
    41.                             try {
    42.                                 context.fireContainerEvent(
    43.                                         "afterSessionDestroyed", listener);
    44.                             } catch (Exception e) {
    45.                                 // Ignore
    46.                             }
    47.                             manager.getContext().getLogger().error
    48.                                 (sm.getString("standardSession.sessionEvent"), t);
    49.                         }
    50.                     }
    51.                 }
    52.             } finally {
    53.                 context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader);
    54.             }
    55.         }
    56.        。。。
    57. }

    这样情况就明了了,http Session 失效时会通过观察者模式通知 WsSessionListener对对应httpSessionId的WsSession进行过期处理,这样WsSession也就跟随着HttpSession一同失效了。

    所以开始的问题的答案也就浮出了水面:一旦http Session 过期了,webService就会停止信息的推送。

    镜头转回来,我缓缓对阿勇说:事情的来龙去脉就是这样

  • 相关阅读:
    生成式AI:未来的发展方向是什么?
    make编译相关教程(经验版)
    Spring Security根据角色在登录后重定向用户
    C++中悬垂指针(delete后指针)仍然可以访问所指内存的问题
    RocketMQ生产者原理及最佳实践
    Win10安装Anaconda和VSCode
    gRPC之metadata
    栈实现综合计算器(思路分析) [数据结构][Java]
    【PyTorch】深度学习实践之反向传播 Back Propagation
    Linux 安装 mongodb
  • 原文地址:https://blog.csdn.net/zhangzhen02/article/details/126171948