Tomcat7.0源码分析——Session管理分析(下)

简介: 版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/beliefer/article/details/52451061 前言  在《Tomcat7.0源码分析——Session管理分析(上)》一文中我介绍了Session、Session管理器,还以StandardManager为例介绍了Session管理器的初始化与启动,本文将接着介绍Session管理的其它内容。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/beliefer/article/details/52451061

前言

  在《Tomcat7.0源码分析——Session管理分析(上)》一文中我介绍了Session、Session管理器,还以StandardManager为例介绍了Session管理器的初始化与启动,本文将接着介绍Session管理的其它内容。

Session分配

  在《Tomcat7.0源码分析——请求原理分析(下)》一文的最后我们介绍了Filter的职责链,Tomcat接收到的请求会经过Filter职责链,最后交给具体的Servlet处理。以访问http://localhost:8080/host-manager这个路径为例,可以清楚的看到整个调用栈(如图1所示,由于原图本人没有妥善保存,只剩下了这张打过水印的)中的Filter的职责链及之后的JspServlet,最后到达org.apache.catalina.connector.Request的getSession方法。


图1  请求调用栈

  Request的getSession方法(见代码清单1)用于获取当前请求对应的会话信息,如果没有则创建一个新的Session。

代码清单1

    public HttpSession getSession(boolean create) {
        Session session = doGetSession(create);
        if (session == null) {
            return null;
        }
        
        return session.getSession();
    }

doGetSession方法的实现见代码清单2。

代码清单2

    protected Session doGetSession(boolean create) {

        // There cannot be a session if no context has been assigned yet
        if (context == null)
            return (null);

        // Return the current session if it exists and is valid
        if ((session != null) && !session.isValid())
            session = null;
        if (session != null)
            return (session);

        // Return the requested session if it exists and is valid
        Manager manager = null;
        if (context != null)
            manager = context.getManager();
        if (manager == null)
            return (null);      // Sessions are not supported
        if (requestedSessionId != null) {
            try {
                session = manager.findSession(requestedSessionId);
            } catch (IOException e) {
                session = null;
            }
            if ((session != null) && !session.isValid())
                session = null;
            if (session != null) {
                session.access();
                return (session);
            }
        }

        // Create a new session if requested and the response is not committed
        if (!create)
            return (null);
        if ((context != null) && (response != null) &&
            context.getServletContext().getEffectiveSessionTrackingModes().
                    contains(SessionTrackingMode.COOKIE) &&
            response.getResponse().isCommitted()) {
            throw new IllegalStateException
              (sm.getString("coyoteRequest.sessionCreateCommitted"));
        }

        // Attempt to reuse session id if one was submitted in a cookie
        // Do not reuse the session id if it is from a URL, to prevent possible
        // phishing attacks
        // Use the SSL session ID if one is present. 
        if (("/".equals(context.getSessionCookiePath()) 
                && isRequestedSessionIdFromCookie()) || requestedSessionSSL ) {
            session = manager.createSession(getRequestedSessionId());
        } else {
            session = manager.createSession(null);
        }

        // Creating a new session cookie based on that session
        if ((session != null) && (getContext() != null)
               && getContext().getServletContext().
                       getEffectiveSessionTrackingModes().contains(
                               SessionTrackingMode.COOKIE)) {
            Cookie cookie =
                ApplicationSessionCookieConfig.createSessionCookie(
                        context, session.getIdInternal(), isSecure());
            
            response.addSessionCookieInternal(cookie);
        }

        if (session == null) {
            return null;
        }
        
        session.access();
        return session;
    }

依据代码清单2,整个获取Session的步骤如下:

  1. 判断当前Request对象是否已经存在有效的Session信息,如果存在则返回此Session,否则进入下一步;
  2. 获取Session管理器,比如StandardManager;
  3. 从StandardManager的Session缓存中获取Session,如果有则返回此Session,否则进入下一步;
  4. 创建Session;
  5. 创建保存Session ID的Cookie;
  6. 通过调用Session的access方法更新Session的访问时间以及访问次数。
  我们来着重阅读ManagerBase实现的createSession方法,见代码清单3。

代码清单3

    public Session createSession(String sessionId) {
        
        // Recycle or create a Session instance
        Session session = createEmptySession();

        // Initialize the properties of the new session and return it
        session.setNew(true);
        session.setValid(true);
        session.setCreationTime(System.currentTimeMillis());
        session.setMaxInactiveInterval(this.maxInactiveInterval);
        if (sessionId == null) {
            sessionId = generateSessionId();
        }
        session.setId(sessionId);
        sessionCounter++;

        return (session);

    }

至此,Session的创建与分配就介绍这些。

Session追踪

  HTTP是一种无状态的协议,如果一个客户端只是单纯地请求一个文件,服务器端并不需要知道一连串的请求是否来自于相同的客户端,而且也不需要担心客户端是否处在连接状态。但是这样的通信协议使得服务器端难以判断所连接的客户端是否是同一个人。当进行Web程序开发时,我们必须想办法将相关的请求结合一起,并且努力维持用户的状态在服务器上,这就引出了会话追踪(session tracking)。

  Tomcat追踪Session主要借助其ID,因此在接收到请求后应该需要拿到此请求对应的会话ID,这样才能够和StandardManager的缓存中维护的Session相匹配,达到Session追踪的效果。还记得《Tomcat7.0源码分析——请求原理分析(中)》一文中介绍CoyoteAdapter的service方法时调用的postParseRequest方法吗?其中有这么一段代码,见代码清单4。

代码清单4

    if (request.getServletContext().getEffectiveSessionTrackingModes()
                .contains(SessionTrackingMode.URL)) {
            
            // Get the session ID if there was one
            String sessionID = request.getPathParameter(
                    ApplicationSessionCookieConfig.getSessionUriParamName(
                            request.getContext()));
            if (sessionID != null) {
                request.setRequestedSessionId(sessionID);
                request.setRequestedSessionURL(true);
            }
        }

        // 省去中间无关代码// Finally look for session ID in cookies and SSL session
        parseSessionCookiesId(req, request);
        parseSessionSslId(request);
        return true;
    }

根据代码清单4可以看出postParseRequest方法的执行步骤如下:

  1. 如果开启了会话跟踪(session tracking),则需要从缓存中获取维护的Session ID;
  2. 从请求所带的Cookie中获取Session ID;
  3. 如果Cookie没有携带Session ID,但是开启了会话跟踪(session tracking),则可以从SSL中获取Session ID;

从缓存中获取维护的Session ID

代码清单4中首先调用getSessionUriParamName方法(见代码清单5)获取Session的参数名称。

代码清单5

    public static String getSessionUriParamName(Context context) {
        
        String result = getConfiguredSessionCookieName(context);
        
        if (result == null) {
            result = DEFAULT_SESSION_PARAMETER_NAME; 
        }
        
        return result; 
    }

从代码清单2看出,getSessionUriParamName方法首先调用getConfiguredSessionCookieName方法获取Session的Cookie名称,如果没有则默认为jsessionid(常量DEFAULT_SESSION_PARAMETER_NAME的值)。回头看代码清单4中会以getSessionUriParamName方法返回的值作为request.getPathParameter(见代码清单6)的参数查询Session ID。

代码清单6

    protected String getPathParameter(String name) {
        return pathParameters.get(name);
    }

从请求所带的Cookie中获取Session ID

  代码清单4中调用的parseSessionCookiesId方法(见代码清单7)用来从Cookie中获取Session ID。

代码清单7

    protected void parseSessionCookiesId(org.apache.coyote.Request req, Request request) {

        // If session tracking via cookies has been disabled for the current
        // context, don't go looking for a session ID in a cookie as a cookie
        // from a parent context with a session ID may be present which would
        // overwrite the valid session ID encoded in the URL
        Context context = (Context) request.getMappingData().context;
        if (context != null && !context.getServletContext()
                .getEffectiveSessionTrackingModes().contains(
                        SessionTrackingMode.COOKIE))
            return;
        
        // Parse session id from cookies
        Cookies serverCookies = req.getCookies();
        int count = serverCookies.getCookieCount();
        if (count <= 0)
            return;

        String sessionCookieName =
            ApplicationSessionCookieConfig.getSessionCookieName(context);

        for (int i = 0; i < count; i++) {
            ServerCookie scookie = serverCookies.getCookie(i);
            if (scookie.getName().equals(sessionCookieName)) {
                // Override anything requested in the URL
                if (!request.isRequestedSessionIdFromCookie()) {
                    // Accept only the first session id cookie
                    convertMB(scookie.getValue());
                    request.setRequestedSessionId
                        (scookie.getValue().toString());
                    request.setRequestedSessionCookie(true);
                    request.setRequestedSessionURL(false);
                    if (log.isDebugEnabled())
                        log.debug(" Requested cookie session id is " +
                            request.getRequestedSessionId());
                } else {
                    if (!request.isRequestedSessionIdValid()) {
                        // Replace the session id until one is valid
                        convertMB(scookie.getValue());
                        request.setRequestedSessionId
                            (scookie.getValue().toString());
                    }
                }
            }
        }

    }

从SSL中获取Session ID

  代码清单4中调用的parseSessionSslId方法(见代码清单8)用来从SSL中获取Session ID。

代码清单8

    protected void parseSessionSslId(Request request) {
        if (request.getRequestedSessionId() == null &&
                SSL_ONLY.equals(request.getServletContext()
                        .getEffectiveSessionTrackingModes()) &&
                        request.connector.secure) {
            // TODO Is there a better way to map SSL sessions to our sesison ID?
            // TODO The request.getAttribute() will cause a number of other SSL
            //      attribute to be populated. Is this a performance concern?
            request.setRequestedSessionId(
                    request.getAttribute(SSLSupport.SESSION_ID_KEY).toString());
            request.setRequestedSessionSSL(true);
        }
    }

Session销毁

  在《Tomcat7.0源码分析——生命周期管理》一文中我们介绍了容器的生命周期管理相关的内容,StandardEngine作为容器,其启动过程中也会调用startInternal方法(见代码清单9)。

代码清单9

    @Override
    protected synchronized void startInternal() throws LifecycleException {
        
        // Log our server identification information
        if(log.isInfoEnabled())
            log.info( "Starting Servlet Engine: " + ServerInfo.getServerInfo());

        // Standard container startup
        super.startInternal();
    }

StandardEngine的startInternal方法实际代理了父类ContainerBase的startInternal方法(见代码清单10)。

代码清单10

    @Override
    protected synchronized void startInternal() throws LifecycleException {

        // Start our subordinate components, if any
        if ((loader != null) && (loader instanceof Lifecycle))
            ((Lifecycle) loader).start();
        logger = null;
        getLogger();
        if ((logger != null) && (logger instanceof Lifecycle))
            ((Lifecycle) logger).start();
        if ((manager != null) && (manager instanceof Lifecycle))
            ((Lifecycle) manager).start();
        if ((cluster != null) && (cluster instanceof Lifecycle))
            ((Lifecycle) cluster).start();
        if ((realm != null) && (realm instanceof Lifecycle))
            ((Lifecycle) realm).start();
        if ((resources != null) && (resources instanceof Lifecycle))
            ((Lifecycle) resources).start();

        // Start our child containers, if any
        Container children[] = findChildren();
        for (int i = 0; i < children.length; i++) {
            children[i].start();
        }

        // Start the Valves in our pipeline (including the basic), if any
        if (pipeline instanceof Lifecycle)
            ((Lifecycle) pipeline).start();


        setState(LifecycleState.STARTING);

        // Start our thread
        threadStart();

    }

代码清单10一开始对各种子容器进行了启动(由于与本文内容关系不大,所以不多作介绍),最后会调用threadStart方法。threadStart的实现见代码清单11。

代码清单11

    protected void threadStart() {

        if (thread != null)
            return;
        if (backgroundProcessorDelay <= 0)
            return;

        threadDone = false;
        String threadName = "ContainerBackgroundProcessor[" + toString() + "]";
        thread = new Thread(new ContainerBackgroundProcessor(), threadName);
        thread.setDaemon(true);
        thread.start();

    }

threadStart方法启动了一个后台线程,任务为ContainerBackgroundProcessor。ContainerBackgroundProcessor的run方法中主要调用了processChildren方法,见代码清单12。

代码清单12

    protected class ContainerBackgroundProcessor implements Runnable {

        public void run() {
            while (!threadDone) {
                try {
                    Thread.sleep(backgroundProcessorDelay * 1000L);
                } catch (InterruptedException e) {
                    // Ignore
                }
                if (!threadDone) {
                    Container parent = (Container) getMappingObject();
                    ClassLoader cl = 
                        Thread.currentThread().getContextClassLoader();
                    if (parent.getLoader() != null) {
                        cl = parent.getLoader().getClassLoader();
                    }
                    processChildren(parent, cl);
                }
            }
        }

        protected void processChildren(Container container, ClassLoader cl) {
            try {
                if (container.getLoader() != null) {
                    Thread.currentThread().setContextClassLoader
                        (container.getLoader().getClassLoader());
                }
                container.backgroundProcess();
            } catch (Throwable t) {
                log.error("Exception invoking periodic operation: ", t);
            } finally {
                Thread.currentThread().setContextClassLoader(cl);
            }
            Container[] children = container.findChildren();
            for (int i = 0; i < children.length; i++) {
                if (children[i].getBackgroundProcessorDelay() <= 0) {
                    processChildren(children[i], cl);
                }
            }
        }

    }

processChildren方法会不断迭代StandardEngine的子容器并调用这些子容器的backgroundProcess方法。这里我们直接来看StandardEngine的孙子容器StandardManager的backgroundProcess实现,即ManagerBase的backgroundProcess方法,见代码清单13。

代码清单13

    public void backgroundProcess() {
        count = (count + 1) % processExpiresFrequency;
        if (count == 0)
            processExpires();
    }

backgroundProcess里实现了一个简单的算法:

  • count:计数器,起始为0;
  • processExpiresFrequency:执行processExpires方法的频率,默认为6。

每执行一次backgroundProcess方法,count会增加1,每当count+1与processExpiresFrequency求模等于0,则调用processExpires。简而言之,每执行processExpiresFrequency指定次数的backgroundProcess方法,执行一次processExpires方法。processExpires的实现见代码清单14所示。

代码清单14

    public void processExpires() {

        long timeNow = System.currentTimeMillis();
        Session sessions[] = findSessions();
        int expireHere = 0 ;
        
        if(log.isDebugEnabled())
            log.debug("Start expire sessions " + getName() + " at " + timeNow + " sessioncount " + sessions.length);
        for (int i = 0; i < sessions.length; i++) {
            if (sessions[i]!=null && !sessions[i].isValid()) {
                expireHere++;
            }
        }
        long timeEnd = System.currentTimeMillis();
        if(log.isDebugEnabled())
             log.debug("End expire sessions " + getName() + " processingTime " + (timeEnd - timeNow) + " expired sessions: " + expireHere);
        processingTime += ( timeEnd - timeNow );

    }

代码清单14中processExpires方法的执行步骤如下:

  1. 从缓存取出所有的Session;
  2. 逐个校验每个Session是否过期,对于已经过期的Session,则expireHere自增。
Session的标准实现是StandardSession,其isValid方法(见代码清单15)的主要功能是判断Session是否过期,对于过期的,则将其expiring状态改为true。判断过期的公式为:

( (当前时间 - Session的最后访问时间)/1000) >= 最大访问间隔

代码清单15

    public boolean isValid() {

        if (this.expiring) {
            return true;
        }

        if (!this.isValid) {
            return false;
        }

        if (ACTIVITY_CHECK && accessCount.get() > 0) {
            return true;
        }

        if (maxInactiveInterval >= 0) { 
            long timeNow = System.currentTimeMillis();
            int timeIdle;
            if (LAST_ACCESS_AT_START) {
                timeIdle = (int) ((timeNow - lastAccessedTime) / 1000L);
            } else {
                timeIdle = (int) ((timeNow - thisAccessedTime) / 1000L);
            }
            if (timeIdle >= maxInactiveInterval) {
                expire(true);
            }
        }

        return (this.isValid);
    }

总结

  Tomcat对于Session的管理过程包括创建、分配、维护和跟踪、销毁等。


后记:个人总结整理的《深入理解Spark:核心思想与源码分析》一书现在已经正式出版上市,目前京东、当当、天猫等网站均有销售,欢迎感兴趣的同学购买。


京东:http://item.jd.com/11846120.html 

当当:http://product.dangdang.com/23838168.html 


相关文章
|
7月前
|
设计模式 安全 Java
【分布式技术专题】「Tomcat技术专题」 探索Tomcat技术架构设计模式的奥秘(Server和Service组件原理分析)
【分布式技术专题】「Tomcat技术专题」 探索Tomcat技术架构设计模式的奥秘(Server和Service组件原理分析)
114 0
|
1月前
|
监控 Java 应用服务中间件
Spring Boot整合Tomcat底层源码分析
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置和起步依赖等特性,大大简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是其与Tomcat的整合。
61 1
|
3月前
|
监控 网络协议 应用服务中间件
【Tomcat源码分析】从零开始理解 HTTP 请求处理 (第一篇)
本文详细解析了Tomcat架构中复杂的`Connector`组件。作为客户端与服务器间沟通的桥梁,`Connector`负责接收请求、封装为`Request`和`Response`对象,并传递给`Container`处理。文章通过四个关键问题逐步剖析了`Connector`的工作原理,并深入探讨了其构造方法、`init()`与`start()`方法。通过分析`ProtocolHandler`、`Endpoint`等核心组件,揭示了`Connector`初始化及启动的全过程。本文适合希望深入了解Tomcat内部机制的读者。欢迎关注并点赞,持续更新中。如有问题,可搜索【码上遇见你】交流。
【Tomcat源码分析】从零开始理解 HTTP 请求处理 (第一篇)
|
3月前
|
人工智能 前端开发 Java
【Tomcat源码分析】启动过程深度解析 (二)
本文深入探讨了Tomcat启动Web应用的过程,重点解析了其加载ServletContextListener及Servlet的机制。文章从Bootstrap反射调用Catalina的start方法开始,逐步介绍了StandardServer、StandardService、StandardEngine、StandardHost、StandardContext和StandardWrapper的启动流程。每个组件通过Lifecycle接口协调启动,子容器逐层启动,直至整个服务器完全启动。此外,还详细分析了Pipeline及其Valve组件的作用,展示了Tomcat内部组件间的协作机制。
【Tomcat源码分析】启动过程深度解析 (二)
|
3月前
|
前端开发 Java 应用服务中间件
【Tomcat源码分析 】"深入探索:Tomcat 类加载机制揭秘"
本文详细介绍了Java类加载机制及其在Tomcat中的应用。首先回顾了Java默认的类加载器,包括启动类加载器、扩展类加载器和应用程序类加载器,并解释了双亲委派模型的工作原理及其重要性。接着,文章分析了Tomcat为何不能使用默认类加载机制,因为它需要解决多个应用程序共存时的类库版本冲突、资源共享、类库隔离及JSP文件热更新等问题。最后,详细展示了Tomcat独特的类加载器设计,包括Common、Catalina、Shared、WebApp和Jsp类加载器,确保了系统的稳定性和安全性。通过这种设计,Tomcat实现了不同应用程序间的类库隔离与共享,同时支持JSP文件的热插拔。
【Tomcat源码分析 】"深入探索:Tomcat 类加载机制揭秘"
|
3月前
|
设计模式 应用服务中间件 容器
【Tomcat源码分析】Pipeline 与 Valve 的秘密花园
本文深入剖析了Tomcat中的Pipeline和Valve组件。Valve作为请求处理链中的核心组件,通过接口定义了关键方法;ValveBase为其基类,提供了通用实现。Pipeline则作为Valve容器,通过首尾相连的Valve链完成业务处理。StandardPipeline实现了Pipeline接口,提供了详细的Valve管理逻辑。通过对代码的详细分析,揭示了模板方法模式和责任链模式的应用,展示了系统的扩展性和模块化设计。
【Tomcat源码分析】Pipeline 与 Valve 的秘密花园
|
3月前
|
设计模式 人工智能 安全
【Tomcat源码分析】生命周期机制 Lifecycle
Tomcat内部通过各种组件协同工作,构建了一个复杂的Web服务器架构。其中,`Lifecycle`机制作为核心,管理组件从创建到销毁的整个生命周期。本文详细解析了Lifecycle的工作原理及其方法,如初始化、启动、停止和销毁等关键步骤,并展示了LifecycleBase类如何通过状态机和模板模式实现这一过程。通过深入理解Lifecycle,我们可以更好地掌握组件生命周期管理,提升系统设计能力。欢迎关注【码上遇见你】获取更多信息,或搜索【AI贝塔】体验免费的Chat GPT。希望本章内容对你有所帮助。
|
4月前
|
Arthas Java 应用服务中间件
一次Tomcat返回404的分析
一个Web应用部署在阿里云EDAS上,使用Tomcat 7.0.59.3,在测试环境遭遇所有接口返回404的问题,而生产环境正常。测试与生产环境主要差异在于Apollo配置不同。通过Arthas工具监控,确认Spring已正确加载Controller,并且请求未进入Spring或Filter处理流程。进一步分析发现,Tomcat内部处理流程中设置了404状态码,最终定位到`org.apache.coyote.http11.AbstractHttp11Processor.process`方法存在问题。通过对代码逻辑的分析,确定原因是请求URL路径不正确。修正URL路径后问题得到解决。
98 1
一次Tomcat返回404的分析
|
4月前
|
网络协议 Java 应用服务中间件
Tomcat源码分析 (一)----- 手撕Java Web服务器需要准备哪些工作
本文探讨了后端开发中Web服务器的重要性,特别是Tomcat框架的地位与作用。通过解析Tomcat的内部机制,文章引导读者理解其复杂性,并提出了一种实践方式——手工构建简易Web服务器,以此加深对Web服务器运作原理的认识。文章还详细介绍了HTTP协议的工作流程,包括请求与响应的具体格式,并通过Socket编程在Java中的应用实例,展示了客户端与服务器间的数据交换过程。最后,通过一个简单的Java Web服务器实现案例,说明了如何处理HTTP请求及响应,强调虽然构建基本的Web服务器相对直接,但诸如Tomcat这样的成熟框架提供了更为丰富和必要的功能。