【Tomcat学习笔记】13-Session

说明,为了简洁,这里贴的代码可能有所删减。

1. Session入门

网上会经常看到介绍Session和Cookie区别的文章,当年我也傻傻的看过,然这两个根本不是一个东西,有啥好比较的,简直就好比问TCP和JAVA的区别。

Session 是什么?我就不去抄维基百科了,谢晞鸣的理解:HTTP协议本身是无状态的(所谓无状态,就是指前后两HTTP请求是独立的,后面的HTTP请求没法知道前面那次做了啥),但在很多场景中,我们需要有状态,比如用户的登录态,而 Session 就是这样一种让应用有状态的机制。

Session 最简单的用法是:
【1】在登录方法中

1
2
3
4
5
Boolean isLogin = login(userId, password)
if(isLogin) {
HttpSession session = request.getSession();
session.setAttribute("userId", userId);
}

【2】在需要判断登录态的方法中

1
2
3
4
5
6
7
HttpSession session = request.getSession(false);//false表示如果没有session不会新建
String userId = session.getAttribute("userId");
if(userId != null){
//用户登录态还在
} else {
//跳转到登录页面
}

怎么能够保证【2】中拿到的 session 就是 【1】中创建的 session 呢?是通过 sessionId 来标识的,sessionIs 可以放在URL中,可以放在cookie中,也可以是 SSL Session ID. So, 我们一起去看看 Tomcat 里是怎么玩的。

2. Tomcat中Session的创建

先来看下 org.apache.catalina.session 这个package,
300

  • FileStore 和 JDBCStore 是用于 Session 持久化的,分别用于持久化到文件和数据库中,在 PersistentManager 中使用
  • ManagerBase、StandardManager、PersistentManagerBase、PersistentManager 用于管理 Session 的
  • StandardSession 就是具体的 Session 类,实现了 HttpSession 接口

当应用代码中使用 request.getSession()获取 session 时,如果没有,就会去创建一个新的。Request#doGetSession 主要干了这个几件事:

  1. 如果当前Request中的session有效,直接返回session,否则就往下走
  2. 去Manager中根据sessionId查找session, 有就直接返回
  3. 到这一步还没有找到session, 判断是否需要新建一个, 不需要就返回null,否则就往下走
  4. 调用manager.createSession创建新的Session, 这里会考虑重用cookie中带过来的sessionId(sessionId的生成需要经过复杂的计算,比较耗时).
  5. 如果当前Context的SessionTrackingMode 是 COOKIE,则将新创建的session的相关信息种到cookie中去

2.1 org.apache.catalina.session.ManagerBase#createSession

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public Session createSession(String sessionId) {
//1. session 数超过限制就抛异常
if ((maxActiveSessions >= 0) && (getActiveSessions() >= maxActiveSessions)) {
rejectedSessions++;
throw new TooManyActiveSessionsException(sm.getString("managerBase.createSession.ise"),maxActiveSessions);
}

//2. 创建空Session
Session session = createEmptySession();

//3. 设置Session属性,比较关键的是session的maxInactiveInterval,这个值是可以在web.xml中配置的
session.setNew(true);
session.setValid(true);
session.setCreationTime(System.currentTimeMillis());
session.setMaxInactiveInterval(this.maxInactiveInterval);
String id = sessionId;
if (id == null) {
// 3.1 创建SessionId,有兴趣可以单独研究,这里不展开了
id = generateSessionId();
}
// 3.2 Manager 中用一个 Map 来保存所有活跃的session, key 是 sessionId.
// protected Map<String, Session> sessions = new ConcurrentHashMap<>();
// setId 这个方法里会把当前session添加到 sessions里
session.setId(id);
sessionCounter++;

//4. 记下创建时间,用于统计session的创建频率
SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
synchronized (sessionCreationTiming) {
sessionCreationTiming.add(timing);
sessionCreationTiming.poll();
}
return (session);
}

2.2 SessionTrackingMode

1
2
3
4
5
public enum SessionTrackingMode {
COOKIE,
URL,
SSL
}

Tomcat支持三种Session追踪模式,SSL 的方式我暂时还不了解,就不瞎BB了。我们来看下 COOKIE 方式的,在创建完Session后:

1
2
3
4
5
6
if (session != null&& context.getServletContext()
.getEffectiveSessionTrackingModes()
.contains(SessionTrackingMode.COOKIE)) {
Cookie cookie = ApplicationSessionCookieConfig.createSessionCookie(context, session.getIdInternal(), isSecure());
response.addSessionCookieInternal(cookie);
}

会创建SessionCookie用来保存sessionId. 这个 Cookie 的属性(生存时间、httpOnly等)是可以在web.xml中配置的(这里要说明下,得益于组件的层级结构,Tomcat中很多配置都可以配在各个级别,越是子容器的配置,优先级越高),比如:

1
2
3
4
5
<session-config>
<cookie-config>
<http-only>true</http-only>
</cookie-config>
</session-config>

如果没有配置就是默认的值. 我们来看下默认SessionCookie是怎样的,下面是springmvc-demo的cookie
300
可以看到JSESSIONID这个cookie,过期时间是浏览会话结束时。在 web.xml 中配置 Session maxInactiveInterval 和 Cookie maxAge 时要注意,如果前者配的很大,后者却很小,没有任何意义

3. Tomcat中Session的使用

在 org.apache.catalina.connector.CoyoteAdapter#postParseRequest 中,会去解析请求,获取sessionId,并放到request一路带下去

1
2
3
4
5
6
7
8
9
10
11
12
//1. URL SessionTrackingMode
if (request.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.URL)) {
sessionID = request.getPathParameter(SessionConfig.getSessionUriParamName(request.getContext()));
if (sessionID != null) {
request.setRequestedSessionId(sessionID);
request.setRequestedSessionURL(true);
}
}
//2. COOKIE SessionTrackingMode
parseSessionCookiesId(request);
//3. SSH SessionTrackingMode
parseSessionSslId(request);

4. Tomcat中Session的过期机制

【Tomcat学习笔记】10-代码变更时自动部署 中有介绍过 Tomcat 每个容器都有一个后台线程,以及它们是如何工作的。 org.apache.catalina.core.StandardContext#backgroundProcess会去调用Manager的backgroundProcess.

org.apache.catalina.session.ManagerBase#backgroundProcess,它会去遍历所有Session, 移除失效的session.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void backgroundProcess() {
count = (count + 1) % processExpiresFrequency;
if (count == 0)
processExpires();
}

public void processExpires() {
long timeNow = System.currentTimeMillis();
Session sessions[] = findSessions();
int expireHere = 0 ;

for (int i = 0; i < sessions.length; i++) {
// isValid 这个方法里会去判断session是否失效,如果失效了就会从sessions中移除
if (sessions[i]!=null && !sessions[i].isValid()) {
expireHere++;
}
}
long timeEnd = System.currentTimeMillis();
processingTime += ( timeEnd - timeNow );
}

5. Tomcat中Session的持久化

前面说过,session 是保存在内存中,protected Map<String, Session> sessions = new ConcurrentHashMap<>();
那么如果Tomcat重启后,sessions还在吗?在的,StandardManager 在关闭的时候会调用org.apache.catalina.session.StandardManager#doUnload 将session相关的数据持久化到“SESSIONS.ser”文件中,并在启动的时候调用org.apache.catalina.session.StandardManager#doLoad 根据文件中的数据重新加载数据。

PersistentManager 则提供了更灵活的持久化方案,可以配置任何实现了Store接口的类来做持久化,Tomcat提供了FileStore和JDBCStore.

使用PersistentManager时,后台线程在处理processExpiress时还会调用下面的方法,把不活跃的线程持久化,把活跃的线程swap到内存中来

1
2
3
4
5
public void processPersistenceChecks() {
processMaxIdleSwaps();
processMaxActiveSwaps();
processMaxIdleBackups();
}

6. 分布式系统中自己实现Session

当系统达到一定规模后,将session放在JVM 内存中肯定是不行,存储在文件或DB中性能也不够。简单点的方案是可以自己实现Store接口,整个RedisStore之类的,将session放到缓存中,然后在web.xml中配置使用该Store。

更多的做法是自己实现一个分布式session应用, 这样可以更灵活,更高可用,想怎么玩玩怎么玩(比如sessionId 就不一定放在 cookie 中啊,后端可以吐给前端,它们拿去自己存储,放到sessionStorage、localStorages甚至indexeddb中,只要下次请求带过来就行。当然,不知道这么做有什么意义,纯粹瞎YY。但很多东西,知道原理之后不就可以脑洞大开了吗)

.

以上皆是阅读源码 https://github.com/fdx321/tomcat8.0-source-research 所得