HTTP is a stateless communication protocol. Each request is independent of the other, and the server cannot recognize a request that has come before. For Web applications, the activity is dependent on a state, such as user login, and HTTP requires the ability to provide login information for subsequent requests after one login request. This article was first published in the public number Epiphany source.

The solution is to use cookies, which are returned by the server to the browser, which caches and submits the Cookie data to the server on each request. Cookies are transmitted in clear text in the request and have a size limit of 4KB. Obviously, it is not feasible to save all state data in the browser. The mainstream approach is:

  1. When the browser makes the first request, the server assigns a unique identifier to the user, returns it and stores it in the browser’s Cookies
  2. The server maintains a global request status library internally and associates each request status information with a generated unique identifier
  3. Subsequent requests from the browser submit unique identifiers to the server to retrieve status information from previous requests

For ease of management, the server calls the entire process a Session and abstracts it into a Session class that identifies and stores information or state about the user.

Next, the source code implementation of Tomcat will be analyzed through the analysis and generation of Session identifiers, Session creation, destruction and persistence, etc. The version is 6.0.53.

1. Parse session identifiers

As the most commonly used session tracking mechanism, cookies are supported by all Servlet containers, and Tomcat is no exception. In Tomcat, the standard name for cookies that store session identifiers is JSESSIONID.

If the browser does not support cookies, you can also use the following method to record the identifier:

  • URL rewriting: include the URL as a path parameter, such as /path; JSESSIONID=xxx
  • URL request parameters: Add session unique identifiers as query parameters to all links on the page, such as /path? JSESSIONID=xxx
  • FORM Hidden Field: A hidden field is used in the FORM to store unique values, which are submitted to the server with the FORM

Tomcat implements extracting jsessionids from URL rewrite paths and cookies. Before analyzing the source code, let’s first look at the key information about the header fields of the response and request with Cookie:

/ / set the Cookie
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=56AE5B92C272EA4F5E0FBFEFE6936C91; Path=/examples
Date: Sun, 12 May 2019 01:40:35 GMT

/ / submit a Cookie
GET /examples/servlets/servlet/SessionExample HTTP/1.1
Host: localhost:8080
Cookie: JSESSIONID=56AE5B92C272EA4F5E0FBFEFE6936C91
Copy the code

1.1 Rewrite paths from urls

A URL containing the session ID path parameter looks like this:

http://localhost:8080/examples/SessionExample; JSESSIONID=1234; n=v/? x=xCopy the code

Simplified, is looking for matching the semicolon and finally a diagonal lines between the JSESSIONID, that is the case, only the Tomcat operation is a byte, the core code in CoyoteAdapter. ParsePathParameters () method, which is not posted here.

1.2 From the Cookie header field

The method call that triggers Cookie resolution is as follows:

CoyoteAdapter. Service (Request, Response) └ ─ CoyoteAdapter. PostParseRequest (Request, Request, Response, The Response) └ ─ CoyoteAdapter. ParseSessionCookiesId (Request, Request) └ ─ Cookies. GetCookieCount () └ ─ Cookies, processCookies (MimeHeaders) └ ─ Cookies, processCookieHeader (byte[].int.int)
Copy the code

The processCookieHeader operates on bytes, and parsing may seem unintuitive. There is also a deprecated string parsing method inside Tomcat that helps you understand:

private void processCookieHeader( String cookieString ){
  // Multiple cookie values are separated by commas
  StringTokenizer tok = new StringTokenizer(cookieString, ";".false);
  while (tok.hasMoreTokens()) {
    String token = tok.nextToken();
    // Get the position of the equal sign
    int i = token.indexOf("=");
    if (i > -1) {
      // Get name and value and remove Spaces
      String name = token.substring(0, i).trim();
      String value = token.substring(i+1, token.length()).trim();
      // RFC 2109 and bug"
      value=stripQuote( value );
      // Get a ServerCookie object from the internal cookie cache pool
      ServerCookie cookie = addCookie();
      // Set name and value
      cookie.getName().setString(name);
      cookie.getValue().setString(value);
    } else {
      // we have a bad cookie.... just let it go}}}Copy the code

Once parsed, the next step is to traverse the parseSessionCookiesId method and try to match the Cookie named JSESSIONID, if present, to requestedSessionId of Request. Associated with an internal Session object.

2. Generate session cookies

Session related cookies are generated internally by Tomcat. When the Servlet uses Request.getSession() to obtain the session object, the execution will be triggered. Core code:

protected Session doGetSession(boolean create) {...// Create Session instance
  if (connector.getEmptySessionPath() && isRequestedSessionIdFromCookie()) {
    // If the session ID comes from a cookie, reuse the ID; if it comes from a URL, do not
    // Reuse the session ID to prevent possible phishing attacks
    session = manager.createSession(getRequestedSessionId());
  } else {
    session = manager.createSession(null);
  }
  // Create a new Session cookie based on the Session
  if((session ! =null) && (getContext() ! =null)
       && getContext().getCookies()) {
    String scName = context.getSessionCookieName();
    if (scName == null) {
      / / the default JSESSIONID
      scName = Globals.SESSION_COOKIE_NAME;
    }
    / / new cookies
    Cookie cookie = new Cookie(scName, session.getIdInternal());
    // Set path Domain Secure
    configureSessionCookie(cookie);
    // Add to the response header field
    response.addSessionCookieInternal(cookie, context.getUseHttpOnly());
  }
  if(session ! =null) {
    session.access();
    return (session);
  } else {
    return (null); }}Copy the code

To add to the response header field, the Cookie object is generated in the format described at the beginning.

3. Session

Session is an internal Tomcat interface, an HttpSession facade class, used to maintain state information between requests from specific users of web applications. Related class diagram design is as follows:

Key classes or interfaces do the following:

  • Manager – Manages a pool of sessions, with different implementations providing specific capabilities such as persistence and distribution
  • ManagerBase – Implements basic features such as Session pooling, unique ID generation algorithms, and easy inheritance extensions
  • StandardManager – Standard implementation that provides simple session persistence when this component is restarted (for example, when an entire server is shut down and restarted, or when a specific Web application is reloaded)
  • PersistentManagerBase – Provides many different ways to manage persistent storage, such as files and databases
  • Store – Provides persistent storage and loading of session and user information
  • ClusterManager – Cluster Session management interface responsible for the replication mode of sessions
  • DeltaManager – Replicates session data increments to all members of the cluster
  • BackupManager – Copies data to only one backup node that is visible to all members of the cluster

This article does not analyze the mechanism of cluster replication, but only the management of single Session.

3.1 create a Session

When request.getSession () is first used in the Servlet to retrieve the session object, a StandardSession instance is created:

public Session createSession(String sessionId) {
  // New StandardSession(this) is returned by default
  Session session = createEmptySession();
  // Initialize the property
  session.setNew(true);
  session.setValid(true);
  session.setCreationTime(System.currentTimeMillis());
  // Set the session validity period, in seconds. The default value is 30 minutes. A negative value indicates that the session never expires
  session.setMaxInactiveInterval(((Context) getContainer()).getSessionTimeout() * 60);
  if (sessionId == null) {
    // Generate a session ID
    sessionId = generateSessionId();
  
  session.setId(sessionId);
  sessionCounter++;

  SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
  synchronized (sessionCreationTiming) {
    sessionCreationTiming.add(timing);
    sessionCreationTiming.poll();
  }
  return (session);
}
Copy the code

The key lies in the generation of unique session identifiers. Let’s take a look at Tomcat’s generation algorithm:

  1. Get 16 bytes randomly
  2. Using MD5 to encrypt these bytes, you get a 16-byte array again
  3. Iterate over the new byte array, generating a hexadecimal character using the high and low four bits of each byte
  4. You end up with a 32-bit hexadecimal string

The core code is as follows:

protected String generateSessionId(a) {
  byte random[] = new byte[16];
  String jvmRoute = getJvmRoute();
  String result = null;
  // Render the result as a string of hexadecimal numbers
  StringBuffer buffer = new StringBuffer();
  do {
    int resultLenBytes = 0;
    if(result ! =null) { // repeat, regenerate
      buffer = new StringBuffer();
      duplicates++;
    }
    / / sessionIdLength is 16
    while (resultLenBytes < this.sessionIdLength) {
      getRandomBytes(random);// Randomly get 16 bytes
      // Get the 16-byte digest. MD5 is used by default
      random = getDigest().digest(random);
      // Iterate over the byte array to generate a 32-bit hexadecimal string
      for (int j = 0;
      j < random.length && resultLenBytes < this.sessionIdLength;
      j++) {
        // Generates a hexadecimal character using the high and low four bits of the specified byte
        byte b1 = (byte) ((random[j] & 0xf0) > >4);
        byte b2 = (byte) (random[j] & 0x0f);
        // Convert to hexadecimal numeric characters
        if (b1 < 10) {buffer.append((char) ('0'+ b1)); }// Convert to uppercase hexadecimal characters
        else {buffer.append((char) ('A' + (b1 - 10))); }if (b2 < 10) {buffer.append((char) ('0'+ b2)); }else {buffer.append((char) ('A' + (b2 - 10)));}
        resultLenBytes++;
      }
    }
    if(jvmRoute ! =null) {buffer.append('. ').append(jvmRoute); } result = buffer.toString(); }while (sessions.containsKey(result));
  return (result);
}
Copy the code

3.2 Session Expiration Check

One Web application corresponds to one session Manager, which means StandardContext has an instance of Manager within it. Each container component starts a background thread that periodically calls itself and the internal component’s backgroundProcess() method. The Manager daemon checks whether the Session has expired.

The logic is to get all sessions using isValid to determine whether they are expired or not, as shown in the following code:

public boolean isValid(a) {...// Whether to check for activity. Default is false
  if (ACTIVITY_CHECK && accessCount.get() > 0) {
    return true;
  }
  // Check whether the time has expired
  if (maxInactiveInterval >= 0) { 
    long timeNow = System.currentTimeMillis();
    int timeIdle = (int) ((timeNow - thisAccessedTime) / 1000L);
    if (timeIdle >= maxInactiveInterval) {
      // If it expires, do some internal processing
      // 主要是通知对过期事件感兴趣的 listeners
      expire(true); }}// Plural never expires
  return (this.isValid);
}
Copy the code

3.3 Session Persistence

Persistence is the serialization of an active Session object in memory to a file or to a database. If the session manager component complies with and enables persistence, then the storage is executed in its lifecycle event stop method; Load is performed in the start method.

Persist to file, StandardManager also provides the ability to persist to file, It writes all active SESSIONS from the session pool to CATALINA_HOME/work/Catalina/

/< webApp >/SESSIONS. Ser in its doUnload method.

FileStore also provides persistence to files, which differs from StandardManager in that it writes each session to a single file named < ID >.session.

Persist to database, store session-related data respectively in a table, including binary data after serialization, table field information is as follows:

create table tomcat_sessions (
  session_id     varchar(100) not null primary key,
  valid_session  char(1) not null.-- Whether it is effective
  max_inactive   int not null.-- Maximum valid time
  last_access    bigint not null.-- Last access time
  app_name       varchar(255), -- Application name in the format of /Engine/Host/Context
  session_data   mediumblob, -- Binary data
  KEY kapp_name(app_name)
);
Copy the code

Note: You need to place the jar file for the database driver in the $CATALINA_HOME/lib directory to make it visible to the class loader inside Tomcat.

4. Summary

This article briefly analyzes the Tomcat Session management, of course, ignored a lot of details, interested in the source code can be in-depth, the follow-up will be the implementation of Tomcat cluster Session analysis. If you have any questions, please leave a message.

Search the wechat public account “Epiphany source code” to get more source code analysis and build the wheel.