`

[转]TOMCAT源码分析——SESSION管理分析(下)

 
阅读更多

前言

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

SESSION分配

  在《TOMCAT源码分析——请求原理分析(下)》一文的最后我们介绍了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追踪的效果。还记得《TOMCAT源码分析——请求原理分析(中)》一文中介绍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的值)。回头看代码清单1中会以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销毁

  在《TOMCAT源码分析——生命周期管理》一文中我们介绍了容器的生命周期管理相关的内容,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。

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的管理过程包括创建、分配、维护和跟踪、销毁等。

如需转载,请标明本文作者及出处——作者:jiaan.gja,本文原创首发:博客园,原文链接:http://www.cnblogs.com/jiaan-geng/p/4920036.html 
道生一,一生二,二生三,三生万物。
分享到:
评论

相关推荐

    tomcat6源码分析

    《Tomcat6源码分析——深入理解Web服务器的运行机制》 Tomcat6作为Apache软件基金会的Jakarta项目的一部分,是一款广泛使用的Java Servlet容器,它实现了Java Servlet和JavaServer Pages(JSP)规范,为开发和部署...

    Tomcat集群——使用MSM管理集群Session

    【标题】:“Tomcat集群——使用MSM管理集群Session” 在分布式系统中,尤其是在基于Java的Web应用中,实现session的共享是确保用户状态在不同服务器之间无缝切换的关键。Tomcat,作为流行的开源Servlet容器,提供...

    TOMCAT源码分析(启动框架).pdf

    ### TOMCAT源码分析——启动框架详解 #### 一、前言 TOMCAT作为一款广泛使用的开源Java Servlet容器,其内部实现复杂且强大。本文旨在深入剖析TOMCAT的启动框架及其整体架构,帮助读者更好地理解其工作原理。...

    JSP源码——图书管理系统(java+mssql).zip

    《JSP源码——图书管理系统(java+mssql)》是一个基于Java技术与Microsoft SQL Server数据库的Web应用程序,旨在实现对图书信息的高效管理。这个系统涵盖了图书的增删改查、用户管理、借阅与归还等功能,是学习JSP...

    JSP源码——THB2B.zip

    源码分析: 1. **目录结构**:源码的目录结构通常包含Web-INF、WEB-INF/classes、WEB-INF/lib、jsp页面、静态资源文件夹等。Web-INF下存放的是应用配置和类文件,classes存储编译后的Java类,lib存放依赖的jar包,...

    JSP源码——[新闻文章]永恒文章管理系统(YHCMS) v2.0 源码版_yhcms_v20_src.zip

    【JSP源码详解——永恒文章管理系统YHCMS v2.0】 JSP(JavaServer Pages)是一种基于Java技术的动态网页开发技术,它允许开发者将静态HTML与动态Java代码结合,以创建交互式Web应用。永恒文章管理系统YHCMS v2.0是...

    JSP源码——[影音娱乐]彩森视频网络电台DQUS版_dqus.zip

    JSP源码可能包括处理用户登录、注册、权限验证的逻辑,以及防止未授权访问的措施,如会话管理(session management)和cookie。 6. **性能优化** 大型的影音娱乐平台需要考虑性能优化,这可能涉及到缓存策略、负载...

    JSP源码——[信息办公]玉玺学生信息管理系统_webapps.zip

    【JSP源码——[信息办公]玉玺学生信息管理系统_webapps.zip】是一个包含JSP技术的源代码项目,主要用于实现学生信息管理的信息化办公系统。这个系统可能涵盖了对学生数据的增删查改、统计分析等功能,对于理解和学习...

    Tomcat与JavaWeb开发技术详解(孙卫琴)

    《Tomcat与JavaWeb开发技术详解》一书深入剖析了JavaWeb开发中的核心组件——Tomcat服务器,旨在帮助开发者理解并掌握如何有效地使用Tomcat进行Web应用的部署和管理。Tomcat作为开源的轻量级应用服务器,是JavaEE...

    httpSession

    6. **源码解析**:深入到具体的Web容器(如Tomcat、Jetty)的源代码中,理解它们如何处理session的创建、存储和销毁。 7. **工具使用**:可能介绍了一些辅助管理session的工具或库,如使用浏览器开发者工具查看...

    JSP实例开发源码——家庭理财系统(java+applet).zip

    【JSP实例开发源码——家庭理财系统(java+applet)】 这个压缩包包含的是一个基于JSP技术的家庭理财系统实例的源代码。JSP(Java Server Pages)是一种动态网页开发技术,它允许开发者在HTML页面中嵌入Java代码,以...

    TP312_Ch700_JSP项目开发实践课本源码——第三部分

    《TP312_Ch700_JSP项目开发实践课本源码——第三部分》是针对JSP(JavaServer Pages)项目开发的一个学习资源,主要涵盖了课程的最后三个章节。这个压缩包包含了一系列的源代码文件,旨在帮助学习者深入理解和实践...

    WEB服务器工作机制由浅至深(6):【How Tomcat Works】第12章StandardContext翻译分析

    本篇将从【How Tomcat Works】的第12章——"StandardContext"的翻译和分析入手,深入解析Tomcat的工作流程。 1. **StandardContext组件** StandardContext是Tomcat中的核心组件之一,它负责管理和维护一个Web应用...

    求精要诀——JavaEE编程开发案例精讲 源代码

    本书中,所有源代码都需要在Tomcat环境下运行,读者可以借此机会学习如何配置和管理Tomcat服务器。 源代码包含的"code"目录下,很可能包含了以下部分: 1. **Servlet程序**:展示如何创建和配置Servlet,处理HTTP...

    学生管理系统源码,学生管理系统源码php,Java源码.zip.zip

    《学生管理系统源码详解——PHP与Java实现》 学生管理系统是一种常见的信息管理软件,它用于高效地管理和跟踪学生的个人信息、成绩、出勤等数据。在本文中,我们将深入探讨两个主流编程语言——PHP和Java实现的学生...

    how tomcat works和jetty-src和tomcat7-src

    标题 "how tomcat works" 和 "jetty-src" 以及 "tomcat7-src" 提到的是两个著名的Java Web服务器——Apache Tomcat和Jetty的源代码。这些源代码是理解Web服务器工作原理的重要资源,尤其是对于Java开发者和系统管理...

    JSP源码——网上购物系统(JavaBean+Servlet+jsp).zip

    《基于JavaBean、Servlet与JSP的网上购物系统解析》 网上购物系统是现代电子商务的重要组成部分,本系统采用JavaWeb技术栈,结合...通过分析和理解源码,可以加深对Web开发流程、组件通信和业务逻辑实现的理解。

    LambdaProbe Tomcat Manager

    LambdaProbe的名字来源于其核心概念——它像一个“探针”一样深入到Tomcat内部,提供实时的性能数据和管理操作。 首先,我们来探讨一下LambdaProbe的主要特点。它提供了多种监控指标,包括但不限于: 1. **服务器...

    有一个JSP网站源码 信息管理系统(完整实用)

    **JSP网站源码——信息管理系统详解** JSP(JavaServer Pages)是一种基于Java技术的动态网页开发标准,它允许开发者将HTML、CSS、JavaScript与Java代码混合编写,以实现服务器端的动态网页生成。本套"信息管理系统...

    JavaWeb宿舍管理系统项目源码 下载 项目分管理员、宿舍管理员以及学生三个类型。.zip

    本项目是一个基于JavaWeb技术实现的宿舍管理系统,旨在提供一个高效、便捷的宿舍管理平台,以满足不同角色——管理员、宿舍管理员和学生的使用需求。系统设计了三个主要用户类型,分别为管理员、宿舍管理员和学生,...

Global site tag (gtag.js) - Google Analytics