关于tomcat session机制梳理
阅读原文时间:2021年04月20日阅读:1

 一道题目引起的思考:“ tomcat里如何禁止服务端自动创建session ”。

1背景知识:

要说tomcat的机制。先从session说起。

http是无状态协议(http具体可参考:http://www.bysocket.com/?p=282 ),每次请求都是独立的线程。所以为了维护上下文信息,追踪同一个用户,就是 session:保持用户会话状态。 目前有几种方式:cookie,URL重写,隐藏表单域。

Session代表着服务器和客户端一次会话的过程。直到session失效(服务端关闭),或者客户端关闭时结束。Session 是存储在服务端的,并针对每个客户端(客户),通过SessionID来区别不同用户的。Session是以Cookie技术或URL重写实现。默认以Cookie技术实现,服务端会给这次会话创造一个JSESSIONID的值。 这个id值会发送给客户端,客户端每次请求都会把这个id值放到http请求的头部发送给服务端,而这个id值在客户端会保存下来,保存的容器就是cookie。

2. tomcat 的session管理机制:

2.1 请求过程中session操作

简述:在请求过程中首先要解析请求中的sessionId信息,然后将sessionId存储到request的参数列表中。然后再从request获取session的时候,如果存在sessionId那么就根据Id从session池中获取session,如果sessionId不存在或者session失效,那么则新建session并且将session信息放入session池,供下次使用。

我们使用session一般这样使用:request. getSession().可以看下

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.getCookies() &&  
        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  
    if (connector.getEmptySessionPath()  
            && isRequestedSessionIdFromCookie()) {  
        session = manager.createSession(getRequestedSessionId());  
    } else {  
        session = manager.createSession(null);  
    }  

    // Creating a new session cookie based on that session  
    if ((session != null) && (getContext() != null)  
           && getContext().getCookies()) {  
        String scName = context.getSessionCookieName();  
        if (scName == null) {  
            scName = Globals.SESSION\_COOKIE\_NAME;  
        }  
        Cookie cookie = new Cookie(scName, session.getIdInternal());  
        configureSessionCookie(cookie);  
        response.addSessionCookieInternal(cookie, context.getUseHttpOnly());  
    }  

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

上面代码就是根据 requestedSessionId 去查找session,找不到就创建新的session。那么 requestedSessionId 怎么获取的呢?

Http11Processor解析封装在org.apache.coyote.Request然后传递给CoyoteAdapter,coyoteAdapter是一个适配器,将coyote框架封装的org.apache.coyote.Request适配给org.apache.catalina.connector.Request。过程就是 Http11Processor方法 process()。里面调用了 adapter.service(request, response); coyoteAdapter的service里面 postParseRequest(req, request, res, response).其中调用了  parsePathParameters(req, request) 方法去解析路径参数中的cookie信息 , parseSessionCookiesId(req, request) 从cookie中解析sessionId存到request ;

上面的 Http11Processor是怎么调用的.还没查到。待补充。

2.2 Servlet获取session的流程:

 

  appServlet是我们自己定义的一个Servlet,在通过Reqest获取session的时候,其实调用的这个 先看 javax.servlet.http.HttpServletRequest. getSession(boolean create)(这 是一个接口)其实是RequestFacade(封装了org.apache.catalina.connector.Request的一个门面),然后RequestFacade会调用真实的Request的getSession方法。Request具体的逻辑是调用Context容器的getManger方法获取Session管理器(session管理器详情下文介绍),然后如果SessionId如果已经被解析出来了,那么则会调用findSession方法从session对象池中获取对应的session,反之如果sessionId不存在,则需要重新创建一个Session,并放入session对象池中。

下面贴出关键代码:

类RequestFacade的getSession方法:

类Request的getSession方法:

org.apache.catalina.connector

doGetSession就是上面2.1贴出来的代码,主要逻辑就是一根据SessionId从session对象池中获取session信息: session = manager.findSession(requestedSessionId) 用于查找sessionid对应的session;,第二标记就是在没有解析到sessionId的情况下创建一个新的Session对象。

类ManagerBase的findSession方法:

 

 注意:  protected Map sessions = new ConcurrentHashMap();

也就是说tomcat是通过线程安全的 ConcurrentHashMap来实现的。

以上即是Servlet获取session的流程。

2.3. Session的管理机制

下面是tomcat6源码里面session包:

实现包的路径是: org.apache.catalina.session ,tomcat对外提供session调用的接口不在这个实现包里,对外接口是在包 javax.servlet.http 下的 HttpSession。 而实现包里的 StandardSession 是tomcat提供的标准实现,当然对外tomcat不希望用户直接操作 StandardSession ,而是提供了一个 StandardSessionFacade 类,tomcat容器里具体操作session的组件是servlet,而servlet操作session是通过 StandardSessionFacade 进行的,这样就可以防止程序员直接操作 StandardSession 所带来的安全问题。

session包下各类含义:

(1)    Manager:定义了关联到某一个容器的用来管理session池的基本接口。这是catalina包下的。

(2)    ManagerBase:实现了Manager接口,该类提供了Session管理器的常见功能的实现。

(3)    StandardManager:继承自ManagerBase,tomcat的默认Session管理器(不指定配置,默认使用这个),是tomcat处理session的非集群实现(也就说是单机版的),tomcat关闭时,内存session信息会持久化到磁盘保存为SESSION.ser,再次启动时恢复。

(4)    PersistentManagerBase:继承自ManagerBase,实现了和定义了session管理器持久化的基础功能。

(5)    PersistentManager:继承自PersistentManagerBase,主要实现的功能是会把空闲的会话对象(通过设定超时时间)交换到磁盘上。

(6)StoreBase:存放持久化有关store接口的基本实现。

持久化session管理器的存储策略就是有这个Store对象定义的,

 接口Store及其实例是为session管理器提供了一套存储策略,store定义了基本的接口,而StoreBase提供了基本的实现。其中FileStore类实现的策略是将session存储在以setDirectory()指定目录并以.session结尾的文件中的。JDBCStore类是将Session通过JDBC存入数据库中,因此需要使用JDBCStore,需要分别调用setDriverName()方法和setConnectionURL()方法来设置驱动程序名称和连接URL。

(7) StandardSession :tomcat的session实现类。

2.4 Tomcat session相关的配置

从两个层面总结一下session相关的配置和设置。首先是从配置文件层面,session是有过期时间的,这个默认的过期时间是 在$catalina_home/conf/web.xml有定义的。具体的默认配置如下(默认的过期时间是30min,即30min没有访 问,session就过期了):

还有一点就是session管理如果不配置就默认使用StandardManager,但如果要配置的话可以 在$catalina_home/conf/context.xml当中指定(其中从这个配置当中可以看到session管理器是和context容器关 联的,也就说每个web应用都会有一个session管理器)具体的配置如下:

Tomcat7.x默认这个manager的配置是注释掉的。如果要指定的PersistentManager为默认管理器的话可以这么指定:

其实看到这也就发现了,其实session管理器或者Store存储策略,只要实现了相关的接口,都是可以自定义的。自己写一个配置在这里就ok了。

另外在从代码层面总结一下:session的一些配置信息是写死在代码里的,比如SessionConfig这个类就定义了一些session的设 置信息。Session在cookie中的名字是JSESSION. Session通过URL重写的方式放在path里时,键值的名字是jsessionids,具体的代码如下:

还有一点就是sessionId默认指定的长度是16个字节,这个在SessionIdGenerator当中指定:

这个 SessionIdGenerator应该是tomcat7的。我在6上源码找不到。关于tomcat6的源码里面,有这个方法generateSessionId:

protected synchronized String generateSessionId() {

    byte random\[\] = new byte\[16\];  
    String jvmRoute = getJvmRoute();  
    String result = null;  

    // Render the result as a String of hexadecimal digits  
    StringBuffer buffer = new StringBuffer();  
    do {  
        int resultLenBytes = 0;  
        if (result != null) {  
            buffer = new StringBuffer();  
            duplicates++;  
        }  

        while (resultLenBytes < this.sessionIdLength) {  
            getRandomBytes(random);  
            random = getDigest().digest(random);  
            for (int j = 0;  
            j < random.length && resultLenBytes < this.sessionIdLength;  
            j++) {  
                byte b1 = (byte) ((random\[j\] & 0xf0) >> 4);  
                byte b2 = (byte) (random\[j\] & 0x0f);  
                if (b1 < 10)  
                    buffer.append((char) ('0' + b1));  
                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);  

}

实现原理:一个随机数加时间加上jvm的id值,jvm的id值会根据服务器的硬件信息计算得来,因此不同jvm的id值都是唯一的

再补充一点:session本身也是基于kv的hashmap实现的,比如:StandardSession 里面  

 protected Map attributes = new ConcurrentHashMap();

所有的会话信息的存取都是通过这个属性来实现的。另外上面web.xml的配置有效期tomcat是怎么实现的呢?

就是ManagerBase.processExpires,一直查询所有的session对象,每个检查是否有效,超期就删除。

具体代码参见

 其中session在expire会触发相应的listener事件,从而对session信息进行监控,下面是expire方法部分截图。

**********************总结*********************

tomcat的session管理机制基本上总结就是上面,这个源码分析师基于tomcat6的。7还是有一些差异跟优化的。

回答问题:如果不想让session被创建,则自定义ManagerBase的实现类,覆盖这些方法为空实现。

思考:

在实际运用中session所带来的问题

由上面所描述的session实现机制,我们会发现,为了弥补http协议的无状态的特点,服务端会占用一定的内存和cpu用来存储和处理session计算的开销,这也就是tomcat这个的web容器的并发连接那么低(tomcat官方文档里默认的连接数是200)原因之一。因此很多java语言编写的网站,在生产环境里web容器之前会加一个静态资源服务器,例如:apache服务器或nginx服务器,静态资源服务器没有解决http无状态问题的功能,因此部署静态资源的服务器也就不会让出内存或cpu计算资源专门去处理像session这样的功能,这些内存和cpu资源可以更有效的处理每个http请求,因此静态资源服务器的并发连接数更高,所以我们可以让那些没有状态保持要求的请求直接在静态服务器里处理,而要进行状态保持的请求则在java的web容器里进行处理,这样能更好的提升网站的效率

解决session相关问题的技术方案

由上所述,session一共有两个问题需要解决:

1) session的存储应该独立于web容器,也要独立于部署web容器的服务器;

2)如何进行高效的session同步。

因此最好的解决方案就是使用分布式缓存技术,例如: memcached 和 redis ,将 session 信息的存储独立出来也是解决 session 同步问题的方法。

下面是一篇用zookeeper实现的分布式session方案:

http://www.open-open.com/lib/view/open1378556537303.html

参考:

http://www.cnblogs.com/sharpxiajun/p/3395607.html

http://blog.csdn.net/it_man/article/details/26217143

http://www.cnblogs.com/interdrp/p/4935614.html