Tomcat Source Code Analysis - - - Understanding Session Mechanism thoroughly

Posted by workbench on Fri, 23 Aug 2019 05:45:31 +0200

Overview of Tomcat Session

Firstly, HTTP is a stateless protocol, which means that every HTTP request initiated is a completely new request (without any connection with the previous request, the server will not retain any information of the previous request), and Session appears to solve this problem, associating every request on the client side, to be true. The Session mechanism usually uses Cookie (which holds the Unified Identifier Number in the cookie), URI additional parameters, or SSL (which is the unique identity of various attributes in SSL as a Client request), while the default Session tracing mechanism (URL + COOKIE) is specified in the Initialization Application Context if the Connector configures the SSLEna. Bled, then the session tracing mode will be added to the tracing mechanism through SSL (the ApplicationContext.populateSessionTrackingModes() method)

Overview of Cookie

Cookie is a small handful of text information (KV) that exists in the Header in Http transmission, and each browser sends its own Cookie information back to the server (PS: Cookie content is stored in the browser); with this technology, the server knows who sent the request (such as Ses here). Session is based on adding a globally unique identifier JsessionId to the Cookie to distinguish which user's request is in the Http transmission.

Analysis of Cookie in Tomcat

In Tomcat 8.0.5, Cookie's analysis is operated through the internal function processCookies(). (In fact, it assigns the content of Http header directly to Cookie object. Cookie finds the name of "Cookie" data in the Header and takes it out for analysis.) Here we mainly look at the whole process from the perspective of Jid session. How does a procedure trigger? Let's look directly at the parsing of jsessionId in the function CoyoteAdapter.postParseRequest().

// Try from URL, Cookie, SSL Getting the request in the reply ID, And will mapRequired Set to false
String sessionID = null;
// 1. Does it support adoption? URI Tail affix JSessionId Ways to track Session Change (The default is supported)
if (request.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.URL)) {
    // 2. from URI Take the parameters of the suffix jsessionId Data (SessionConfig.getSessionUriParamName It is acquisition correspondence. cookie Name, default jsessionId, Can be in web.xml Definition)
    sessionID = request.getPathParameter( SessionConfig.getSessionUriParamName(request.getContext()));
    if (sessionID != null) { 
        // 3. If from URI It's taken inside. jsessionId, Then assign the value directly to request
        request.setRequestedSessionId(sessionID);
        request.setRequestedSessionURL(true);
    }
}

// Look for session ID in cookies and SSL session
// 4. adopt cookie Get inside JSessionId Value
parseSessionCookiesId(req, request);   
// 5. stay SSL Mode acquisition JSessionId Value                             
parseSessionSslId(request);                                         

/**
 * Parse session id in URL.
 */
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 = request.getMappingData().context;
    // 1. Tomcat Does it support adoption? cookie Mechanism tracking session
    if (context != null && !context.getServletContext()
            .getEffectiveSessionTrackingModes().contains(
                    SessionTrackingMode.COOKIE)) {                      
        return;
    }

    // Parse session id from cookies
     // 2. Obtain Cookie Actual reference object (PS: There's no trigger yet. Cookie analysis, that is serverCookies It's empty data., Data is only stored http header inside)
    Cookies serverCookies = req.getCookies(); 
    // 3. It's right here to start. Cookie analysis Header Data inside (PS: In fact, it's a round-robin search. Header The one inside name yes Cookie Data, Take it out for analysis)    
    int count = serverCookies.getCookieCount();                         
    if (count <= 0) {
        return;
    }

    // 4. Obtain sessionId Name of JSessionId
    String sessionCookieName = SessionConfig.getSessionCookieName(context); 

    for (int i = 0; i < count; i++) {
        // 5. Poll all parsed Cookie
        ServerCookie scookie = serverCookies.getCookie(i);      
        // 6. compare Cookie Is the name of jsessionId        
        if (scookie.getName().equals(sessionCookieName)) {              
            logger.info("scookie.getName().equals(sessionCookieName)");
            logger.info("Arrays.asList(Thread.currentThread().getStackTrace()):" + Arrays.asList(Thread.currentThread().getStackTrace()));
            // Override anything requested in the URL
            // 7. Whether or not? jsessionId Not yet resolved (And only the first successful value will be parsed. set go in)
            if (!request.isRequestedSessionIdFromCookie()) {            
                // Accept only the first session id cookie
                // 8. take MessageBytes Turn into char
                convertMB(scookie.getValue());        
                // 9. Set up jsessionId Value                
                request.setRequestedSessionId(scookie.getValue().toString());
                request.setRequestedSessionCookie(true);
                request.setRequestedSessionURL(false);
                if (log.isDebugEnabled()) {
                    log.debug(" Requested cookie session id is " +
                        request.getRequestedSessionId());
                }
            } else {
                // 10. if Cookie There are several in it. jsessionid, Overlay set value
                if (!request.isRequestedSessionIdValid()) {             
                    // Replace the session id until one is valid
                    convertMB(scookie.getValue());
                    request.setRequestedSessionId
                        (scookie.getValue().toString());
                }
            }
        }
    }

}
In fact, the above steps are to parse jsessionId from URI, Cookie and SSL in turn, among which parsing from Cookie is the most commonly used. In this Tomcat version, parsing jsessionid from cookie hides deeper, triggered by Cookie.getCookieCount(), the whole parsing process. In fact, it is to traverse the data in the thread header in turn, find the name="Cookie" data, and parse the string (which is not described here); if the client transmits jsessionId, the server has parsed it and set it into the Request object, but the Session object has not yet been set. Triggered creation, at most, is to find out whether the Session corresponding to jsessionId exists in Manager.

Analysis of tomcat session design

The tomcat session component diagram is shown below, where Context corresponds to a webapp application, each webapp has multiple HttpSessionListeners, and the sessions of each application are managed independently, while the creation and destruction of sessions are accomplished by the Manager component, which maintains N Session instance objects internally. In the previous article, we analyzed the Context component, whose default implementation is StandardContext, which has a one-to-one relationship with the Manager. When the Manager creates and destroys a session, it needs to use StandardContext to obtain the HttpSessionListener list and notify the event, while the Background of StandardContext Threads clean up the Manager's expired session

The main methods of the org.apache.catalina.Manager interface are as follows. It provides the getter/setter interface of Context, org.apache.catalina.SessionIdGenerator, and the API interface of creating, adding, removing, searching and traversing Session. It also provides the load/unload interface for Session persistence. In loading/unloading session information, persistence depends on different implementation classes, of course.

public interface Manager {
    public Context getContext();
    public void setContext(Context context);
    public SessionIdGenerator getSessionIdGenerator();
    public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator);
    public void add(Session session);
    public void addPropertyChangeListener(PropertyChangeListener listener);
    public void changeSessionId(Session session);
    public void changeSessionId(Session session, String newId);
    public Session createEmptySession();
    public Session createSession(String sessionId);
    public Session findSession(String id) throws IOException;
    public Session[] findSessions();
    public void remove(Session session);
    public void remove(Session session, boolean update);
    public void removePropertyChangeListener(PropertyChangeListener listener);
    public void unload() throws IOException;
    public void backgroundProcess();
    public boolean willAttributeDistribute(String name, Object value);
}

tomcat 8.5 provides four implementations. Standard Manager is used by default. tomcat also provides a solution for cluster sessions, but it is seldom used in practical projects.

  • Standard Manager: By default, Manager manages sessions in memory, and downtime will result in session loss; however, when calling Lifecycle's start/stop interface, jdk serialization will be used to save session information, so when tomcat discovers that an application's file has changed to reload, in this case Session information will not be lost
  • Delta Manager: Incremental Session Manager, used in Tomcat cluster session manager, a node changes Session information will be synchronized to all nodes in the cluster, so as to ensure the real-time Session information, but this will bring greater network overhead.
  • Backup Manager: A session manager for Tomcat clusters. Unlike Delta Manager, changes in Session information by one node are synchronized only to another backup node in the cluster.
  • Persistent Manager: Session information is written to disk when the session is idle for a long time, which limits the number of active sessions in memory. In addition, it supports fault tolerance and backs up Session information in memory to disk regularly.

Let's look at the Class Diagram of Standard Manager, which is also a Lifecycle component, and ManagerBase implements the main logic.

Creation of Session in Tomcat

After the above Cookie parsing, if there is jsessionId, it has been set into Request. When did Session trigger the creation? Mainly the code request.getSession(), see the code:

public class SessionExample extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException  {
        HttpSession session = request.getSession();
        // other code......
    }
}

Let's take a look at getSession():

// Obtain request Corresponding session
public HttpSession getSession() {
    // Here is the passage. managerBase.sessions Obtain Session
    Session session = doGetSession(true); 
    if (session == null) {
        return null;
    }
    return session.getSession();
}

// create Represents whether to create StandardSession
protected Session doGetSession(boolean create) {              

    // There cannot be a session if no context has been assigned yet
    // 1. test StandardContext
    if (context == null) {
        return (null);                                           
    }

    // Return the current session if it exists and is valid
     // 2. check Session Effectiveness
    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) {
        //Get StandardContext Corresponding StandardManager,Context and Manager One-to-one relationship
        manager = context.getManager();
    }
    if (manager == null)
     {
        return (null);      // Sessions are not supported
    }
    if (requestedSessionId != null) {
        try {        
            // 3. adopt managerBase.sessions Obtain Session
            // 4. Through the client sessionId from managerBase.sessions To obtain Session object
            session = manager.findSession(requestedSessionId);   
        } catch (IOException e) {
            session = null;
        }
         // 5. judge session Is it effective?
        if ((session != null) && !session.isValid()) {          
            session = null;
        }
        if (session != null) {
            // 6. session access +1
            session.access();                                    
            return (session);
        }
    }

    // Create a new session if requested and the response is not committed
    // 7. Create based on identity StandardSession ( false Direct return)
    if (!create) {
        return (null);                                           
    }
    // Current Context Does it support adoption? cookie Ways to track Session
    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.
    // 8. I can't find it here. session, Direct creation Session come out
    if (("/".equals(context.getSessionCookiePath()) && isRequestedSessionIdFromCookie()) || requestedSessionSSL ) {
        session = manager.createSession(getRequestedSessionId()); // 9. Read from the client sessionID, And according to this sessionId Establish Session
    } 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)) {
        // 10. according to sessionId To create a Cookie
        Cookie cookie = ApplicationSessionCookieConfig.createSessionCookie(context, session.getIdInternal(), isSecure());
        // 11. Finally, write in the body of the response cookie
        response.addSessionCookieInternal(cookie);              
    }

    if (session == null) {
        return null;
    }
    // 12. session access Counter + 1
    session.access();                                          
    return session;
}

Let's look at manager.createSession(null);

public abstract class ManagerBase extends LifecycleMBeanBase implements Manager {
    //Manager Managing the present Context All session
    protected Map<String, Session> sessions = new ConcurrentHashMap<>();
    @Override
    public Session findSession(String id) throws IOException {
        if (id == null) {
            return null;
        }
        //adopt JssionId Obtain session
        return sessions.get(id);
    }
    
    public Session createSession(String sessionId) {
        // 1. Judging a single node Session Does the number exceed the limit?
        if ((maxActiveSessions >= 0) && (getActiveSessions() >= maxActiveSessions)) {      
            rejectedSessions++;
            throw new TooManyActiveSessionsException(
                    sm.getString("managerBase.createSession.ise"),
                    maxActiveSessions);
        }

        // Recycle or create a Session instance
        // Create an empty session
        // 2. Establish Session
        Session session = createEmptySession();                     

        // Initialize the properties of the new session and return it
        // Initialization empty session Attributes of
        session.setNew(true);
        session.setValid(true);
        session.setCreationTime(System.currentTimeMillis());
        // 3. StandardSession Maximum default Session Activation time
        session.setMaxInactiveInterval(this.maxInactiveInterval); 
        String id = sessionId;
        // If not client End read to jsessionId
        if (id == null) {      
            // 4. generate sessionId (Here it is generated by random numbers.)    
            id = generateSessionId();                              
        }
        //Here will be session Deposit in Map<String, Session> sessions = new ConcurrentHashMap<>();
        session.setId(id);
        sessionCounter++;

        SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
        synchronized (sessionCreationTiming) {
            // 5. Each creation Session They all create one SessionTiming, also push To list sessionCreationTiming Finally
            sessionCreationTiming.add(timing); 
            // 6. And delete the front node of the list        
            sessionCreationTiming.poll();                         
        }      
        // So this one sessionCreationTiming What role does it play?, actually sessionCreationTiming It's used for statistics. Session Frequency of new construction and failure (Be like Zookeeper There's also this statistic.)    
        return (session);
    }
    
    @Override
    public void add(Session session) {
        //What will be created Seesion Deposit in Map<String, Session> sessions = new ConcurrentHashMap<>();
        sessions.put(session.getIdInternal(), session);
        int size = getActiveSessions();
        if( size > maxActive ) {
            synchronized(maxActiveUpdateLock) {
                if( size > maxActive ) {
                    maxActive = size;
                }
            }
        }
    }
}

@Override
public void setId(String id) {
    setId(id, true);
}

@Override
public void setId(String id, boolean notify) {

    if ((this.id != null) && (manager != null))
        manager.remove(this);

    this.id = id;

    if (manager != null)
        manager.add(this);

    if (notify) {
        tellNew();
    }
}

The main steps are:

1. If request. Session!= null, it returns directly (indicating that other threads created Session before the same time and assigned it to request)

2. If requestedSessionId!= null, search directly through manager and determine whether it is valid or not.

3. Call manager.createSession to create the corresponding Session and store the Session in the Manager's Map

4. Create Cookies according to SessionId and put Cookies in Response

5. Return directly to Session

Session Cleaning

Background thread

We analyzed the process of creating Session, and Session session is timeliness. Now let's see how tomcat performs failure checking. Before analyzing, let's review the Background thread of the Container container.

All container components of tomcat are inherited from ContainerBase, including Standard Engine, Standard Host, Standard Context, Standard Wrapper, and ContainerBase will open ContainerBackgroundProcessor background threads if the backgroundProcessor Delay parameter is greater than 0 at startup, calling The backgroundProcess itself and its subcontainers do some backgroundlogic processing. Like Lifecycle, this action is transitive, so is it.

The key code is as follows:

ContainerBase.java

protected synchronized void startInternal() throws LifecycleException {
    // other code......
    // open ContainerBackgroundProcessor Threads are used to process subcontainers, by default backgroundProcessorDelay=-1,This thread will not be enabled
    threadStart();
}

protected class ContainerBackgroundProcessor implements Runnable {
    public void run() {
        // threadDone yes volatile Variables, controlled by external containers
        while (!threadDone) {
            try {
                Thread.sleep(backgroundProcessorDelay * 1000L);
            } catch (InterruptedException e) {
                // Ignore
            }
            if (!threadDone) {
                processChildren(ContainerBase.this);
            }
        }
    }

    protected void processChildren(Container container) {
        container.backgroundProcess();
        Container[] children = container.findChildren();
        for (int i = 0; i < children.length; i++) {
            // If the subcontainer backgroundProcessorDelay If the parameter is less than 0, the subcontainer is processed recursively
            // Because if the value is greater than 0, the child container opens thread processing itself, so the parent container does not need to do any more processing.
            if (children[i].getBackgroundProcessorDelay() <= 0) {
                processChildren(children[i]);
            }
        }
    }
}

Session check

The default value of the backgroundProcessor Delay parameter is -1 in seconds, i.e., no background threads are enabled by default, while the Container container of tomcat needs to open threads to handle some background tasks, such as monitoring jsp changes, tomcat configuration changes, Session expiration, etc., so Standard Engine will BAC in its construction method. The kgroundProcessor Delay parameter is set to 10 (which can be specified in server.xml, of course), which is executed every 10 seconds. So how does this thread control its life cycle? We noticed that ContainerBase has a threadDone variable, which is modified with volatile. If the stop method of Container container is called and the value is assigned false, the background thread will exit the loop and end its life cycle. In addition, there is a point to note that the parent container needs to determine the value of the backgroundProcessor Delay of the child container when dealing with the background tasks of the child container. Only when the value is less than or equal to 0 can it be processed, because if the value is greater than 0, the child container will open the thread to handle itself, and then the parent container will not need to do any more. Handled

After analyzing how the container's background threads are scheduled, let's focus on the webapp layer and how the Standard Manager cleans up expired sessions. StandardContext rewrites the backgroundProcess method, which cleans up some of the cached information in addition to processing the subcontainers. The key code is as follows:

 

StandardContext.java

@Override
public void backgroundProcess() {
    if (!getState().isAvailable())
        return;
    // Thermal loading class,perhaps jsp
    Loader loader = getLoader();
    if (loader != null) {
        loader.backgroundProcess();
    }
    // Clean up expired Session
    Manager manager = getManager();
    if (manager != null) {
        manager.backgroundProcess();
    }
    // Clean up the cache of resource files
    WebResourceRoot resources = getResources();
    if (resources != null) {
        resources.backgroundProcess();
    }
    // Clean up objects or class Information caching
    InstanceManager instanceManager = getInstanceManager();
    if (instanceManager instanceof DefaultInstanceManager) {
        ((DefaultInstanceManager)instanceManager).backgroundProcess();
    }
    // Calling subcontainers backgroundProcess task
    super.backgroundProcess();
}

StandardContext rewrites the backgroundProcess method. Before calling the background tasks of the subcontainer, we also call the background tasks of Loader, Manager, WebResourceRoot, InstanceManager. Here we only care about the background tasks of Manager. Having figured out the context of Standard Manager, let's analyze the specific logic.

Standard Manager inherits from ManagerBase, which implements the main logic, and the code for Session cleanup is shown below. backgroundProcess is called every 10 seconds by default, but it is modeled in ManagerBase and cleaned up in 60s by default. tomcat does not introduce a time round for cleaning up Sessions, because the timeliness requirements for Sessions are not so precise, except to notify SessionListener.

ManagerBase.java

public void backgroundProcess() {
    // processExpiresFrequency The default value is 6, and backgroundProcess Default every 10 s Call once, that is, every 60 minutes except for the time-consuming task execution s Execute once
    count = (count + 1) % processExpiresFrequency;
    if (count == 0) // Default every 60 s Execute once Session Clear
        processExpires();
}

/**
 * Single-threaded processing, thread security issues do not exist
 */
public void processExpires() {
    long timeNow = System.currentTimeMillis();
    Session sessions[] = findSessions();    // Get all Session
    int expireHere = 0 ;
    for (int i = 0; i < sessions.length; i++) {
        // Session The expiration date is isValid() What's in it?
        if (sessions[i]!=null && !sessions[i].isValid()) {
            expireHere++;
        }
    }
    long timeEnd = System.currentTimeMillis();
    // Record processing time
    processingTime += ( timeEnd - timeNow );
}

Clean up expired Session

In the above code, we do not see too much overdue processing, but call sessions[i].isValid(), the original clean-up action is handled in this method, quite obscure. In the StandardSession isValid () method, if the current - thisAccessedTime >= maxInactiveInterval determines that the current Session expires, and this thisAccessedTime parameter is updated every time it is accessed.

public boolean isValid() {
    // other code......
    // If the maximum inactivity time is specified, the cleaning will take place. This time is Context.getSessionTimeout(),The default is 30 minutes.
    if (maxInactiveInterval > 0) {
        int timeIdle = (int) (getIdleTimeInternal() / 1000L);
        if (timeIdle >= maxInactiveInterval) {
            expire(true);
        }
    }
    return this.isValid;
}

The logic dealt with by expire method is more complicated. Now I simply describe the core logic with pseudo code. Because this step may be operated by multiple threads, synchronized method is used to lock the current Session object, and double checks are made to avoid repeated processing of expired Session. It also sends event notifications to Container containers and calls HttpSessionListener for event notifications, which is our web application development HttpSessionListener. Since the Session object is maintained in the Manager, it is also removed from the Manager. The most important function of Session is to store data. There may be strong references, which makes Session impossible to be reclaimed by gc. Therefore, key/value data must be removed. This shows that tomcat coding is rigorous, and a little carelessness may lead to concurrency problems, as well as memory leaks.

public void expire(boolean notify) {
    //1,check isValid Value if false Return directly, indicating that it has been destroyed
    synchronized (this) {   // Lock up
        //2,Double Check isValid Value to avoid concurrency problems
        Context context = manager.getContext();
        if (notify) {   
            Object listeners[] = context.getApplicationLifecycleListeners();
            HttpSessionEvent event = new HttpSessionEvent(getSession());
            for (int i = 0; i < listeners.length; i++) {
            //3,Judge whether it is HttpSessionListener,If not, continue to cycle.
            //4,Send to container Destory Events, and calls HttpSessionListener.sessionDestroyed() Notification
            context.fireContainerEvent("beforeSessionDestroyed", listener);
            listener.sessionDestroyed(event);
            context.fireContainerEvent("afterSessionDestroyed", listener);
        }
        //5,from manager Remove the  session
        //6,towards tomcat Of SessionListener Send an event notification, no HttpSessionListener
        //7,Clear the interior key/value,Avoid unrecoverable due to strong references Session object
    }
}

From the previous analysis, we can see that tomcat will clean up the expired Session according to the timestamp. How does tomcat update the timestamp? After processing the request, tomcat collects the Request object and cleans up the Session information, which updates the thisAccessedTime and lastAccessedTime timestamps. In addition, we call the request.getSession() API, call the Session access () method when we return to Session, and update the thisAccessedTime timestamp. In this way, each request will update the timestamp, which can ensure the Session's live time.

org.apache.catalina.connector.Request.java

protected void recycleSessionInfo() {
    if (session != null) {  
        session.endAccess();    // Update Timestamp
    }
    // recovery Request Internal information of objects
    session = null;
    requestedSessionCookie = false;
    requestedSessionId = null;
    requestedSessionURL = false;
    requestedSessionSSL = false;
}

org.apache.catalina.session.StandardSession.java

public void endAccess() {
    isNew = false;
    if (LAST_ACCESS_AT_START) {     // This value can be changed by system parameters, default is false
        this.lastAccessedTime = this.thisAccessedTime;
        this.thisAccessedTime = System.currentTimeMillis();
    } else {
        this.thisAccessedTime = System.currentTimeMillis();
        this.lastAccessedTime = this.thisAccessedTime;
    }
}

public void access() {
    this.thisAccessedTime = System.currentTimeMillis();
}

Topics: Java Session Tomcat SSL Apache