【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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM