• 【tomcat】sessionId学习(未完待续)


      这里主要研究tomcat中session的管理方式以及sessionId的原理,下文将研究sessionid存到redis中以及基于redis实现session共享。

      平时也就是了解session是基于cookie实现的,cookie是保存在客户端,而session是保存在服务端,对其原来也没有深入理解。下面将深入理解。

    1.什么是session

      对Tomcat而言,Session是一块在服务器开辟的内存空间,其内部的有一个ConcurrentHashMap,我们做setAttribute和removeAttribute的时候都操作的是此map。(补充一句,request对象的setAttribute也操作的是内部的一个Map)

    public class StandardSession implements HttpSession, Session, Serializable {
        private static final long serialVersionUID = 1L;
        protected static final boolean STRICT_SERVLET_COMPLIANCE;
        protected static final boolean ACTIVITY_CHECK;
        protected static final boolean LAST_ACCESS_AT_START;
        protected static final String[] EMPTY_ARRAY;
        protected ConcurrentMap<String, Object> attributes = new ConcurrentHashMap();
    ...
    }

    2.Session的目的

    Http协议是一种无状态协议,即每次服务端接收到客户端的请求时,都是一个全新的请求,服务器并不知道客户端的历史请求记录;

    Session的主要目的就是为了弥补Http的无状态特性。简单的说,就是服务器可以利用session存储客户端在同一个会话期间的一些操作记录;

    Session中有一个ConcurrentMap,我们向Session中setAttribute和removeAttribute的时候操作的是此map。

    3.简单的研究session的创建时机

    3.1实现机制

    先看两个问题,如下:
    1、服务器如何判断客户端发送过来的请求是属于同一个会话?

    答:用Session id区分,Session id相同的即认为是同一个会话,在Tomcat中Session id用JSESSIONID表示;

    2、服务器、客户端如何获取Session id?Session id在其之间是如何传输的呢?

    答:服务器第一次接收到请求时,开辟了一块Session空间(创建了Session对象),同时生成一个Session id,并通过响应头的Set-Cookie:“JSESSIONID=XXXXXXX”命令,向客户端发送要求设置cookie的响应;

    客户端收到响应后,在本机客户端设置了一个JSESSIONID=XXXXXXX的cookie信息,该cookie的过期时间为浏览器会话结束;

    接下来客户端每次向同一个网站发送请求时,请求头都会带上该cookie信息(包含Session id);

    然后,服务器通过读取请求头中的Cookie信息,获取名称为JSESSIONID的值,得到此次请求的Session id;

    ps:服务器只会在客户端第一次请求响应的时候,在响应头上添加Set-Cookie:“JSESSIONID=XXXXXXX”信息,接下来在同一个会话的第二第三次响应头里,是不会添加Set-Cookie:“JSESSIONID=XXXXXXX”信息的;

    而客户端是会在每次请求头的cookie中带上JSESSIONID信息;

    3.2创建时机简单研究

       我们知道request.getSession(boolean create)   获取session,并根据参数动态的获取session,如果传的参数是true的话不存在session就创建一个并返回一个session;如果传false,不存在session也不会创建,如下代码:

    package com.servlet;
    
    import java.io.IOException;
    import java.io.PrintWriter;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    public class TestServlet extends HttpServlet {
    
        private static final long serialVersionUID = 1L;
        private static final Logger LOGGER = LoggerFactory.getLogger(TestServlet.class);
    
        public TestServlet() {
            LOGGER.info("call servlet constructor!");
        }
    
        protected void doGet(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            HttpSession session = request.getSession(true);// 传true会创建一个并返回,false不会创建
            PrintWriter writer = response.getWriter();
            if (session == null) {
                writer.write("null");
            } else {
                writer.write(session.toString());
            }
        }
    
        protected void doPost(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            doGet(request, response);
        }
    
    }

    查看request.etSession(boolean create)的源码并分析:

      不带参数的getSession()里面调用的是getSession(true)方法。

        public HttpSession getSession() {
            return this.getSession(true);
        }
    
        public HttpSession getSession(boolean create) {
            if (this.crossContext) {
                if (this.context == null) {
                    return null;
                } else if (this.session != null && this.session.isValid()) {
                    return this.session.getSession();
                } else {
                    HttpSession other = super.getSession(false);
                    if (create && other == null) {
                        other = super.getSession(true);
                    }
    
                    if (other != null) {
                        Session localSession = null;
    
                        try {
                            localSession = this.context.getManager().findSession(other.getId());
                            if (localSession != null && !localSession.isValid()) {
                                localSession = null;
                            }
                        } catch (IOException arg4) {
                            ;
                        }
    
                        if (localSession == null && create) {
                            localSession = this.context.getManager().createSession(other.getId());
                        }
    
                        if (localSession != null) {
                            localSession.access();
                            this.session = localSession;
                            return this.session.getSession();
                        }
                    }
    
                    return null;
                }
            } else {
                return super.getSession(create);
            }
        }

    上述结构图:

     

    分析上面源码:

    (1)当前的session存在并且有效(根据session的过期时间以及内部的一些属性进行判断)的话返回session

    代码:

    else if (this.session != null && this.session.isValid()) {
                    return this.session.getSession();
                } 

    查看this.session.getSession()的源码:(返回真正的session的代码)

    下面是StandardSession中的代码:

        public HttpSession getSession() {
          if(this.facade == null) {
             if(SecurityUtil.isPackageProtectionEnabled()) {
                this.facade = (StandardSessionFacade)AccessController.doPrivileged(new 1(this, this));
             } else {
                this.facade = new StandardSessionFacade(this);
             }
          }
    
          return this.facade;
       }

    看到代码是初始化了一个facade(门面)并且返回去。facade又是什么?

        protected transient StandardSessionFacade facade = null;

    是一个实现HttpSession接口的类

    package org.apache.catalina.session;
    
    import java.util.Enumeration;
    import javax.servlet.ServletContext;
    import javax.servlet.http.HttpSession;
    import javax.servlet.http.HttpSessionContext;
    import org.apache.catalina.session.StandardSession;
    
    public class StandardSessionFacade implements HttpSession {
        private HttpSession session = null;
    
        public StandardSessionFacade(StandardSession session) {
            this.session = session;
    ...
    }

     (2)接下来研究session不存在的时候session创建并且返回的过程:

        创建session对象,session的创建是调用了ManagerBase的createSession方法来实现的

        @Override
        public Session createSession(String sessionId) {
            
            if ((maxActiveSessions >= 0) &&
                    (getActiveSessions() >= maxActiveSessions)) {
                rejectedSessions++;
                throw new TooManyActiveSessionsException(
                        sm.getString("managerBase.createSession.ise"),
                        maxActiveSessions);
            }
            
            // 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(((Context) getContainer()).getSessionTimeout() * 60);
            String id = sessionId;
            if (id == null) {
                id = generateSessionId();
            }
            session.setId(id);
            sessionCounter++;
    
            SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
            synchronized (sessionCreationTiming) {
                sessionCreationTiming.add(timing);
                sessionCreationTiming.poll();
            }
            return (session);
    
        }

    4. Session的管理机制

    Session管理器定义:Session管理器组件负责管理Session对象,例如,创建和销毁Session对象。

    首先看一张Session管理器的类继承结构图:

           简述:下面依次总结下每个类(参考官网信息):

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

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

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

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

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

    (6)    ClusterManager:实现了Manager接口,通过类名应该能猜到,这个就是管理集群session的管理器和上面那个 StandardManager单机版的session管理器是相对的概念。这个类定义类集群间session的复制共享接口。

    (7)    ClusterManagerBase:实现了ClusterManager接口,继承自ManagerBase。该类实现了session复制的基本操作。

    (8)    BackupManager:继承自ClusterManagerBase,        集群间session复制策略的一种实现,会话数据只有一个备份节点,这个备份节点的位置集群中所有节点都可见。这种设计使它有个优势就是支持异构部署。

    (9)    DeltaManager:继承自ClusterManagerBase,集群建session复制策略的一种实现,和BackupManager不同的是,会话数据会复制到集群中所有的成员节点,这也就要求集群中所有节点必须同构,必须部署相同的应用。

     补充:下面再具体总结一点就是在PersistentManagerBase类中有个成员变量Store:

     

    持久化session管理器的存储策略就是有这个Store对象定义的,这个Store的类继承结构如下:

     

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

    补充:Tomcat session相关的配置

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

     

     Tomcat7.x默认这个manager的配置是注释掉的。conf/context.xml文件:

    如果要指定的PersistentManager为默认管理器的话可以修改:

    正常关闭与开启之后会生成一个持久化的文件:(这里的正常关闭指的是通过startup.bat和shutdown.bat或者linux对应的sh文件进行的操作)

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

    五、Session共享的几种方式

    session会话在单台服务器下不会出现共享问题,现在应用部署方式都是分布式,或者集群部署,这样必然会面临一个问题,session共享。其主要方式也有以下几种:

    1.web服务器的粘性请求,比如采用 nginx 请求分发,使用ip_hash这种负载均衡方式,客户端请求只会被分发到相同的后台server,这样可以避免session共享的问题。但是缺点也很明显,如果一台服务器宕机了session会丢失。

    2.基于数据库存储(网站用户量大的情况下,频繁dml数据,对db压力大)

    3.基于cookie存储(安全问题、虽然可以加密存储、但是我觉得永远不能将敏感数据放在客户端,不信任啊O(∩_∩)O哈哈~)

    4.服务器内置的session复制域(比如was下提供session复制功能、但这个损耗服务器内存)

    5.基于nosql(memcache、redis都可以)

    最简单的就是1和5,下面研究基于1和5的session共享。

    1.nginx的 ip_hash 根据不同的ip分配到同一个sever

     关于nginx的安装以及同一台机器运行多个tomcat参考我之前的文章。

    在这里简单部署一个简单的JSP页面查看session的值与tomcat的目录:

    <%@ page language="java" contentType="text/html; charset=UTF-8"
        pageEncoding="UTF-8"%>
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Insert title here</title>
    </head>
    <body>
    
        jsessionid=${pageContext.session.id}
        <br />
        <%=request.getRealPath("/")%>
    </body>
    </html>

    单独启动两个tomcat部署之后查看效果:

    (1)研究简单的nginx集群==与session共享无关,只是实现负载均衡

    #user  nobody;
    worker_processes  1;
    
    #error_log  logs/error.log;
    #error_log  logs/error.log  notice;
    #error_log  logs/error.log  info;
    
    #pid        logs/nginx.pid;
    
    
    events {
        worker_connections  1024;
    }
    
    
    http {
        include       mime.types;
        default_type  application/octet-stream;
    
        #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
        #                  '$status $body_bytes_sent "$http_referer" '
        #                  '"$http_user_agent" "$http_x_forwarded_for"';
    
        #access_log  logs/access.log  main;
    
        sendfile        on;
        #tcp_nopush     on;
    
        #keepalive_timeout  0;
        keepalive_timeout  65;
    
        #gzip  on;
    
        server {
            listen       84;
            server_name  localhost;
    
            #charset koi8-r;
    
            #access_log  logs/host.access.log  main;
    
            location / {
                   proxy_connect_timeout   3;
                   proxy_send_timeout      30;
                   proxy_read_timeout      30;
                   proxy_pass http://clustername;
            }
    
            #error_page  404              /404.html;
    
            # redirect server error pages to the static page /50x.html
            #
            error_page   500 502 503 504  /50x.html;
            location = /50x.html {
                root   html;
            }
    
            # proxy the PHP scripts to Apache listening on 127.0.0.1:80
            #
            #location ~ .php$ {
            #    proxy_pass   http://127.0.0.1;
            #}
    
            # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
            #
            #location ~ .php$ {
            #    root           html;
            #    fastcgi_pass   127.0.0.1:9000;
            #    fastcgi_index  index.php;
            #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
            #    include        fastcgi_params;
            #}
    
            # deny access to .htaccess files, if Apache's document root
            # concurs with nginx's one
            #
            #location ~ /.ht {
            #    deny  all;
            #}
        }
    
    
        # another virtual host using mix of IP-, name-, and port-based configuration
        #
        #server {
        #    listen       8000;
        #    listen       somename:8080;
        #    server_name  somename  alias  another.alias;
    
        #    location / {
        #        root   html;
        #        index  index.html index.htm;
        #    }
        #}
    
        #集群配置:服务器列表
        upstream clustername {
          server 127.0.0.1:85 weight=1;#服务器配置   weight是权重的意思,权重越大,分配的概率越大。
          server 127.0.0.1:86 weight=1;#服务器配置   weight是权重的意思,权重越大,分配的概率越大。
        }
    
        # HTTPS server
        #
        #server {
        #    listen       443 ssl;
        #    server_name  localhost;
    
        #    ssl_certificate      cert.pem;
        #    ssl_certificate_key  cert.key;
    
        #    ssl_session_cache    shared:SSL:1m;
        #    ssl_session_timeout  5m;
    
        #    ssl_ciphers  HIGH:!aNULL:!MD5;
        #    ssl_prefer_server_ciphers  on;
    
        #    location / {
        #        root   html;
        #        index  index.html index.htm;
        #    }
        #}
    
    }

    (2)nginx的ip_hash实现根据ip分配到指定的服务器:(一种简单的session共享)

    只需要将上面的 权重分配改为 ip_hash即可,如下:

        #集群配置:服务器列表
        upstream clustername {
          ip_hash;
          server 127.0.0.1:85;#服务器配置
          server 127.0.0.1:86;#服务器配置
        }

     2. 基于redis实现session共享

      参考我的另一篇博客。https://www.cnblogs.com/qlqwjy/p/10375638.html

  • 相关阅读:
    [bzoj3224] 普通平衡树
    [总结] 三种常见的区间贪心问题
    [NOIP2014] 飞扬的小鸟
    POJ 1185 炮兵阵地
    WOJ 1538 Stones II 转化背包问题
    WOJ 1542 Countries 并查集转化新点+最短路
    UVA 11375 高精度Bign类
    2014_csu选拔1_B
    Codeforces 405D 数学问题
    Codeforces 400C 矩阵乘法 数学规律
  • 原文地址:https://www.cnblogs.com/qlqwjy/p/9833669.html
Copyright © 2020-2023  润新知