一场技术破案的经过

源起:

一天,我的同事阿勇问我:你说如果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<String, List<String>> requestParameterMap;

    private final String queryString;

    private final Principal userPrincipal;

    private final Map<String, String> 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<WsSession> 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就会停止信息的推送。

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

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值