源起:
一天,我的同事阿勇问我:你说如果tomcat session过期了,websocket还会继续往页面推送数据吗?
听到该问题后,尽管当时我的表面冷静沉着,但内心做着飞快的如下盘算。
首先我们常用的session是http请求通过cookie所存的sessionId在内存中寻找的,所以cookie/session机制是基于http协议的。
然后websocket只有在初始握手时期才使用http协议,之后就升级为ws协议了。
而ws协议和http协议同属在tcp协议之上的应用层协议,并没有隶属关系。
所以websocket应该是无法知道tomcat session过期的。
那么答案就很明显了:还会推送!
就在我即将脱口而出的时候,一个理智的声音在脑海中提醒我,等等!
如果session都失效了,websocket还可以推送消息,这不是bug吗?tomcat会允许这样的极不合理的现象存在吗?
理智告诉我,久经沙场的tomcat应该不会有这么明显的问题。
此时,我想到了一个叫狄仁杰的胖老头的口头禅:其中必有蹊跷。
于是,我缓缓对阿勇说,这个我研究一下,回头告诉你。
来都来了,先看下tomcat session的实现机制吧
首先要说的是tomcat从8.5版本之后就抛弃了BIO线程模型,默认采用NIO线程模型。
所以前期的流程是:Selector 线程将socket传递给Worker线程, 在Worker线程中,会完成从socket中读取http request,解析成HttpServletRequest对象,分派到相应的servlet并完成逻辑,然后将response通过socket发回client。
故事就发生在Request对象这里。
package org.apache.catalina.connector;
public class Request implements HttpServletRequest {
protected Session session = null;
protected String requestedSessionId = null;
protected Cookie[] cookies = null;
。。。。。。
}
request对象持有Session,Cookies,RequestSessionId等多个对象。
那么session是什么时候生成的呢?
在 Session在用户第一次访问服务器Servlet,jsp等动态资源就会被自动创建。创建流程如下:
首先会Request.getSession()方法会被调用。
Request.java
public HttpSession getSession() {
//不存在session时会创建
Session session = doGetSession(true);
if (session == null) {
return null;
}
return session.getSession();
}
//重点分析部分,session的创建过程
protected Session doGetSession(boolean create) {
//省略部分代码
......
//如果session 已经存在,判断是否过期。
if ((session != null) && !session.isValid()) {
//如果已经过期,则置为null
session = null;
}
//存在且没有过期,直接返回
if (session != null) {
return session;
}
//省略部分代码
......
//判断request中的sessionId是否存在,该值是从request请求里的cookie或者url中解析出来
if (requestedSessionId != null) {
try {
//根据requestSessionId 查找session
session = manager.findSession(requestedSessionId);
} catch (IOException e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("request.session.failed", requestedSessionId, e.getMessage()), e);
} else {
log.info(sm.getString("request.session.failed", requestedSessionId, e.getMessage()));
}
session = null;
}
//判断session是否过期
if ((session != null) && !session.isValid()) {
session = null;
}
if (session != null) {
session.access();
return session;
}
}
//省略部分代码
......
//根据sessionId创建session,如sessionId不存在,则在内部自动生成sessionId。
session = manager.createSession(sessionId);
if (session != null && trackModesIncludesCookie) {
Cookie cookie = ApplicationSessionCookieConfig.createSessionCookie(
context, session.getIdInternal(), isSecure());
//session成功创建,将session对应sessionId 写入cookie
response.addSessionCookieInternal(cookie);
}
return session;
}
session是什么时候过期的呢?
在上面代码中我们看到有session.isValid()方法多次被调用,我们进去看看。
StandardSession.java
public boolean isValid() {
//根据标识位快速判断
if (!this.isValid) {
return false;
}
//根据标识位快速判断
if (this.expiring) {
return true;
}
//根据标识位和使用次数快速判断
if (ACTIVITY_CHECK && accessCount.get() > 0) {
return true;
}
//配置中的session过期时间
if (maxInactiveInterval > 0) {
//计算从最后一次session被请求到现在的时间,为闲置时间
int timeIdle = (int) (getIdleTimeInternal() / 1000L);
//如果限制时间大于配置的session过期时间
if (timeIdle >= maxInactiveInterval) {
//进行过期操作。
expire(true);
}
}
return this.isValid;
}
我们再看下session过期操作的内容
StandardSession.java
public void expire(boolean notify) {
//双检锁
if (!isValid) {
return;
}
synchronized (this) {
if (expiring || !isValid) {
return;
}
if (manager == null) {
return;
}
expiring = true;
Context context = manager.getContext();
//这里对session观察者进行通知session失效事件的通知
if (notify) {
ClassLoader oldContextClassLoader = null;
try {
oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null);
Object listeners[] = context.getApplicationLifecycleListeners();
if (listeners != null && listeners.length > 0) {
HttpSessionEvent event =
new HttpSessionEvent(getSession());
for (int i = 0; i < listeners.length; i++) {
int j = (listeners.length - 1) - i;
if (!(listeners[j] instanceof HttpSessionListener)) {
continue;
}
HttpSessionListener listener =
(HttpSessionListener) listeners[j];
try {
context.fireContainerEvent("beforeSessionDestroyed",
listener);
listener.sessionDestroyed(event);
context.fireContainerEvent("afterSessionDestroyed",
listener);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
try {
context.fireContainerEvent(
"afterSessionDestroyed", listener);
} catch (Exception e) {
// Ignore
}
manager.getContext().getLogger().error
(sm.getString("standardSession.sessionEvent"), t);
}
}
}
} finally {
context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader);
}
}
if (ACTIVITY_CHECK) {
accessCount.set(0);
}
//从manager中移出该session
manager.remove(this, true);
if (notify) {
fireSessionEvent(Session.SESSION_DESTROYED_EVENT, null);
}
//设置标识位
setValid(false);
expiring = false;
// 解除相关绑定
String keys[] = keys();
ClassLoader oldContextClassLoader = null;
try {
oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null);
for (String key : keys) {
removeAttributeInternal(key, notify);
}
} finally {
context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader);
}
}
}
总的来说,做了三件事,一是通知观察者session失效事件,二是从manager中移出失效session,三是解除该session的相关关联关系。
还有个疑问,这次session过期操作是通过http请求触发的,那如果该session过期用户再也不请求,session不就没有办法从内存中移出了嘛,这不就内存泄漏了吗?
所以此处肯定另有机关。
果不其然。
在tomcat启动时会启动一个background线程,对过期session进行清理。
ManagerBase.java
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 (Session session : sessions) {
if (session != null && !session.isValid()) {
expireHere++;
}
}
//省略部分代码
......
}
这就说的通了,当请求触发时会判断session是否过期,如果过期则删除。另外还另起一个线程,对过期session进行以10秒一次的定期删除。
session搞清楚了,下面有请我们的今天的主角websocket登场。
websocket的会话保持用的也是session,不过这个session是WsSession,跟httpSession的关系呢,就是WsSession持有了HttpSession的Id。
代码如下:
WsSession.java
//父类Session并不是前文的org.apache.catalina.Session,而是javax.websocket.Session。两者没有关系
public class WsSession implements Session {
//省略部分代码
......
private final WsWebSocketContainer webSocketContainer;
private final URI requestUri;
private final Map<String, List<String>> requestParameterMap;
private final String queryString;
private final Principal userPrincipal;
private final Map<String, String> pathParameters;
//持有httpSessionId
private final String httpSessionId;
//session状态标识位
private volatile State state = State.OPEN;
//校验session状态表示位
private void checkState() {
if (state == State.CLOSED) {
/*
* As per RFC 6455, a WebSocket connection is considered to be
* closed once a peer has sent and received a WebSocket close frame.
*/
throw new IllegalStateException(sm.getString("wsSession.closed", id));
}
}
在websocket client和server端交互的过程中,会多次调用checkState方法,而checkState方法是判断state属性。
那么我们看下state属性什么时候会发现变化,最终我们可以找到一个WsSessionListener。
//实现了HttpSessionListener接口
public class WsSessionListener implements HttpSessionListener{
private final WsServerContainer wsServerContainer;
public WsSessionListener(WsServerContainer wsServerContainer) {
this.wsServerContainer = wsServerContainer;
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
wsServerContainer.closeAuthenticatedSession(se.getSession().getId());
}
}
WsServerContainer.java
...省略其他方法
//
public void closeAuthenticatedSession(String httpSessionId) {
Set<WsSession> wsSessions = authenticatedSessions.remove(httpSessionId);
if (wsSessions != null && !wsSessions.isEmpty()) {
for (WsSession wsSession : wsSessions) {
try {
//wsSession关闭
wsSession.close(AUTHENTICATED_HTTP_SESSION_CLOSED);
} catch (IOException e) {
// Any IOExceptions during close will have been caught and the
// onError method called.
}
}
}
}
WsSession.java
public void close(CloseReason closeReason) throws IOException {
doClose(closeReason, closeReason);
}
public void doClose(CloseReason closeReasonMessage, CloseReason closeReasonLocal) {
doClose(closeReasonMessage, closeReasonLocal, false);
}
public void doClose(CloseReason closeReasonMessage, CloseReason closeReasonLocal,
boolean closeSocket) {
//双检锁
if (state != State.OPEN) {
return;
}
synchronized (stateLock) {
//已关闭,直接返回
if (state != State.OPEN) {
return;
}
if (log.isDebugEnabled()) {
log.debug(sm.getString("wsSession.doClose", id));
}
//清理该WsSession绑定的相关的信息
try {
wsRemoteEndpoint.setBatchingAllowed(false);
} catch (IOException e) {
log.warn(sm.getString("wsSession.flushFailOnClose"), e);
fireEndpointOnError(e);
}
//将state属性值为关闭
if (state != State.OUTPUT_CLOSED) {
state = State.OUTPUT_CLOSED;
sendCloseMessage(closeReasonMessage);
if (closeSocket) {
wsRemoteEndpoint.close();
}
fireEndpointOnClose(closeReasonLocal);
}
}
。。。
}
那么我们看到state属性在WsSessionListener中进行了关闭,那么WsSessionListener又是什么时候触发的呢?
前面代码中我们看到WsSessionListener实现了HttpSessionListener接口。
等等,这个HttpSessionListener很眼熟嘛,听口音像是老乡,有点印象。
果然,往前一翻,就发现了在之前的StandardSession.java的分析中,就有过一面之缘。
让我们还原一下当时初次见面时的的情景。
StandardSession.java
public void expire(boolean notify) {
//双检锁
if (!isValid) {
return;
}
synchronized (this) {
if (expiring || !isValid) {
return;
}
if (manager == null) {
return;
}
expiring = true;
Context context = manager.getContext();
//这里对session观察者进行通知session失效事件的通知
if (notify) {
ClassLoader oldContextClassLoader = null;
try {
oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null);
Object listeners[] = context.getApplicationLifecycleListeners();
if (listeners != null && listeners.length > 0) {
HttpSessionEvent event =
new HttpSessionEvent(getSession());
for (int i = 0; i < listeners.length; i++) {
int j = (listeners.length - 1) - i;
if (!(listeners[j] instanceof HttpSessionListener)) {
continue;
}
//就是这里,在http Session过期时,对HttpSessionListener进行了通知
HttpSessionListener listener =
(HttpSessionListener) listeners[j];
try {
context.fireContainerEvent("beforeSessionDestroyed",
listener);
listener.sessionDestroyed(event);
context.fireContainerEvent("afterSessionDestroyed",
listener);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
try {
context.fireContainerEvent(
"afterSessionDestroyed", listener);
} catch (Exception e) {
// Ignore
}
manager.getContext().getLogger().error
(sm.getString("standardSession.sessionEvent"), t);
}
}
}
} finally {
context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader);
}
}
。。。
}
这样情况就明了了,http Session 失效时会通过观察者模式通知 WsSessionListener对对应httpSessionId的WsSession进行过期处理,这样WsSession也就跟随着HttpSession一同失效了。
所以开始的问题的答案也就浮出了水面:一旦http Session 过期了,webService就会停止信息的推送。
镜头转回来,我缓缓对阿勇说:事情的来龙去脉就是这样。