原本是想写aop设计机制的,可是最近被session这个东西搞得有点头大,因此就抽点时间来整理下关于session的一些东西。html
HTTP是一种无状态协议。关于这个无状态以前我也不太理解,由于HTTP底层是TCP,既然是TCP,就是长链接,这个过程是保持链接状态的,又为何说http是无状态的呢?先来搞清楚这两个概念:java
无链接nginx
每次链接只处理一个请求,服务端处理完客户端一次请求,等到客户端做出回应以后便断开链接;web
无状态ajax
是指服务端对于客户端每次发送的请求都认为它是一个新的请求,上一次会话和下一次会话没有联系;redis
无链接的维度是链接,无状态的维度是请求;http是基于tcp的,而从http1.1开始默认使用持久链接;在这个链接过程当中,客户端能够向服务端发送屡次请求,可是各个请求之间的并无什么联系;这样来考虑,就很好理解无状态这个概念了。数据库
持久链接,本质上是客户端与服务器通讯的时候,创建一个持久化的TCP链接,这个链接不会随着请求结束而关闭,一般会保持链接一段时间。
json
现有的持久链接类型有两种:HTTP/1.0+的keep-alive和HTTP/1.1的persistent。api
先来开一张图:跨域
connection: keep-alive
复制代码
咱们每次发送一个HTTP请求,会附带一个connection:keep-alive,这个参数就是声明一个持久链接。
HTTP/1.1的持久链接默认是开启的,只有首部中包含connection:close,才会事务结束以后关闭链接。固然服务器和客户端仍能够随时关闭持久链接。
当发送了connection:close首部以后客户端就没有办法在那条链接上发送更多的请求了。固然根据持久链接的特性,必定要传输正确的content-length。
还有根据HTTP/1.1的特性,是不该该和HTTP/1.0客户端创建持久链接的。最后,必定要作好重发的准备。
OK,首先来明确下,这个状态的主体指的是什么?应该是信息,这些信息是由服务端所维护的与客户端交互的信息(也称为状态信息); 由于HTTP自己是不保存任何用户的状态信息的,因此HTTP是无状态的协议。
在聊这个这个问题以前,咱们来考虑下为何http本身不来作这个事情:也就是让http变成有状态的。
http自己来实现状态维护
从上面关于无状态的理解,若是如今须要让http本身变成有状态的,就意味着http协议须要保存交互的状态信息;暂且不说这种方式是否合适,但从维护状态信息这一点来讲,代价就很高,由于既然保存了状态信息,那后续的一些行为一定也会受到状态信息的影响。
从历史角度来讲,最初的http协议只是用来浏览静态文件的,无状态协议已经足够,这样实现的负担也很轻。可是随着web技术的不断发展,愈来愈多的场景须要状态信息可以得以保存;一方面是http自己不会去改变它的这种无状态的特性(至少目前是这样的),另外一方面业务场景又迫切的须要保持状态;那么这个时候就须要来“装饰”一下http,引入一些其余机制来实现有状态。
cookie和session体系
经过引入cookie和session体系机制来维护状态信息。即用户第一次访问服务器的时候,服务器响应报头一般会出现一个Set-Cookie响应头,这里其实就是在本地设置一个cookie,当用户再次访问服务器的时候,http会附带这个cookie过去,cookie中存有sessionId这样的信息来到服务器这边确认是否属于同一次会话。
cookie是由服务器发送给客户端(浏览器)的小量信息,以{key:value}的形式存在。
客户端请求服务器时,若是服务器须要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。而客户端浏览器会把Cookie保存起来。当浏览器再请求 服务器时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器经过检查该Cookie来获取用户状态。
咱们经过看下servlet-api中Cookie类的定义及属性,来更加具体的了解Cookie。
public class Cookie implements Cloneable, Serializable {
private static final long serialVersionUID = -6454587001725327448L;
private static final String TSPECIALS;
private static final String LSTRING_FILE =
"javax.servlet.http.LocalStrings";
private static ResourceBundle lStrings =
ResourceBundle.getBundle("javax.servlet.http.LocalStrings");
private String name;
private String value;
private String comment;
private String domain;
private int maxAge = -1;
private String path;
private boolean secure;
private int version = 0;
private boolean isHttpOnly = false;
//....省略其余方法
}
复制代码
name
cookie的名字,Cookie一旦建立,名称便不可更改
value
cookie值
comment
该Cookie的用处说明。浏览器显示Cookie信息的时候显示该说明
domain
能够访问该Cookie的域名。若是设置为“.baidu.com”,则全部以“baidu.com”结尾的域名均可以访问该Cookie;第一个字符必须为“.”
maxAge
Cookie失效的时间,单位秒。
path
该Cookie的使用路径。例如:
path设置时,其以“/”结尾.
secure
该Cookie是否仅被使用安全协议传输。这里的安全协议包括HTTPS,SSL等。默认为false。
version
该Cookie使用的版本号。
在servlet规范中默认是0;
isHttpOnly
HttpOnly属性是用来限制非HTTP协议程序接口对客户端Cookie进行访问;也就是说若是想要在客户端取到httponly的Cookie的惟一方法就是使用AJAX,将取Cookie的操做放到服务端,接收客户端发送的ajax请求后将取值结果经过HTTP返回客户端。这样能有效的防止XSS攻击。
上述的这些属性,除了name与value属性会被提交外,其余的属性对于客户端来讲都是不可读的,也是不可被提交的。
Cookie cookie = new Cookie("cookieSessionId","qwertyuiop");
cookie.setDomain(".baidu.com"); // 设置域名
cookie.setPath("/"); // 设置路径
cookie.setMaxAge(Integer.MAX_VALUE); // 设置有效期为永久
response.addCookie(cookie); // 回写到客户端
复制代码
建立Cookie只能经过上述方式来建立,由于在Cookie类中只提供了这样一个构造函数。
//Cookie的构造函数
public Cookie(String name, String value) {
if (name != null && name.length() != 0) {
//判断下是否是token
//判断是否是和Cookie的属性字段重复
if (this.isToken(name) && !name.equalsIgnoreCase("Comment") &&
!name.equalsIgnoreCase("Discard") &&
!name.equalsIgnoreCase("Domain") &&
!name.equalsIgnoreCase("Expires") &&
!name.equalsIgnoreCase("Max-Age") &&
!name.equalsIgnoreCase("Path") &&
!name.equalsIgnoreCase("Secure") &&
!name.equalsIgnoreCase("Version") && !name.startsWith("$")) {
this.name = name;
this.value = value;
} else {
String errMsg =
lStrings.getString("err.cookie_name_is_token");
Object[] errArgs = new Object[]{name};
errMsg = MessageFormat.format(errMsg, errArgs);
throw new IllegalArgumentException(errMsg);
}
} else {
throw new IllegalArgumentException(lStrings.getString
("err.cookie_name_blank"));
}
}
复制代码
在源码中能够知道,Cookie自己并无提供修改的方法;在实际应用中,通常经过使用相同name的Cookie来覆盖原来的Cookie,以达到更新的目的。
可是这个修改的前提是须要具备相同domain,path的 Set-Cookie 消息头
Cookie cookie = new Cookie("cookieSessionId","new-qwertyuiop");
response.addCookie(cookie);
复制代码
与Cookie更新同样,Cookie自己也没有提供删除的方法;可是从前面分析Cookie属性时了解到,删除Cookie能够经过将maxAge设置为0便可。
Cookie cookie = new Cookie("cookieSessionId","new-qwertyuiop");
cookie.setMaxAge(0);
response.addCookie(cookie);
复制代码
上面的删除是咱们本身可控的;可是也存在一些咱们不可控或者说无心识状况下的删除操做:
其实不少状况下,咱们关注的都是后者。关于数量上限后面会说到。
Cookie[] cookies = request.getCookies();
复制代码
咱们知道浏览器的同源策略:
URL由协议、域名、端口和路径组成,若是两个URL的协议、域名和端口相同,则表示他们同源。浏览器的同源策略,限制了来自不一样源的"document"或脚本,对当前"document"读取或设置某些属性。
对于Cookie来讲,Cookie的同源只关注域名,是忽略协议和端口的。因此通常状况下,https://localhost:80/和http://localhost:8080/的Cookie是共享的。
Cookie是不可跨域的;在没有通过任何处理的状况下,二级域名不一样也是不行的。(wenku.baidu.com和baike.baidu.com)。
IE6.0 | IE7.0/8.0 | Opera | FF | Safari | Chrome | |
---|---|---|---|---|---|---|
个数/个 | 20/域 | 50/域 | 30/域 | 50/域 | 无限制 | 53/域 |
大小/Byte | 4095 | 4095 | 4096 | 4097 | 4097 | 4097 |
注:数据来自网络,仅供参考
由于浏览器对于Cookie在数量上是有限制的,若是超过了天然会有一些剔除策略。在这篇文章中Browser cookie restrictions提到的剔除策略以下:
The least recently used (LRU) approach automatically kicks out the oldest cookie when the cookie limit has been reached in order to allow the newest cookie some space. Internet Explorer and Opera use this approach.
最近最少使用(LRU)方法:在达到cookie限制时自动地剔除最老的cookie,以便腾出空间给许最新的cookie。Internet Explorer和Opera使用这种方法。
Firefox does something strange: it seems to randomly decide which cookies to keep although the last cookie set is always kept. There doesn’t seem to be any scheme it’s following at all. The takeaway? Don’t go above the cookie limit in Firefox.
Firefox决定随机删除Cookie集中的一个Cookie,并无什么章法。因此最好不要超过Firefox中的Cookie限制。
超过大小长度的话就是直接被截取丢弃;
Cookie机制弥补了HTTP协议无状态的不足。在Session出现以前,基本上全部的网站都采用Cookie来跟踪会话。
与Cookie不一样的是,session是以服务端保存状态的。
当客户端请求建立一个session的时候,服务器会先检查这个客户端的请求里是否已包含了一个session标识 - sessionId,
sessionId的值通常是一个既不会重复,又不容易被仿造的字符串,这个sessionId将被在本次响应中返回给客户端保存。保存sessionId的方式大多状况下用的是cookie。
HttpSession和Cookie同样,都是javax.servlet.http下面的;Cookie是一个类,它描述了Cookie的不少内部细节。而HttpSession是一个接口,它为session的实现提供了一些行为约束。
public interface HttpSession {
/** * 返回session的建立时间 */
public long getCreationTime();
/** * 返回一个sessionId,惟一标识 */
public String getId();
/** *返回客户端最后一次发送与该 session 会话相关的请求的时间 *自格林尼治标准时间 1970 年 1 月 1 日午夜算起,以毫秒为单位。 */
public long getLastAccessedTime();
/** * 返回当前session所在的ServletContext */
public ServletContext getServletContext();
public void setMaxInactiveInterval(int interval);
/** * 返回 Servlet 容器在客户端访问时保持 session * 会话打开的最大时间间隔 */
public int getMaxInactiveInterval();
public HttpSessionContext getSessionContext();
/** * 返回在该 session会话中具备指定名称的对象, * 若是没有指定名称的对象,则返回 null。 */
public Object getAttribute(String name);
public Object getValue(String name);
/** * 返回 String 对象的枚举,String 对象包含全部绑定到该 session * 会话的对象的名称。 */
public Enumeration<String> getAttributeNames();
public String[] getValueNames();
public void setAttribute(String name, Object value);
public void putValue(String name, Object value);
public void removeAttribute(String name);
public void removeValue(String name);
/** * 指示该 session 会话无效,并解除绑定到它上面的任何对象。 */
public void invalidate();
/** * 若是客户端不知道该 session 会话,或者若是客户选择不参入该 * session 会话,则该方法返回 true。 */
public boolean isNew();
}
复制代码
建立session的方式是经过request来建立;
// 一、建立Session对象
HttpSession session = request.getSession();
// 二、建立Session对象
HttpSession session = request.getSession(true);
复制代码
这两种是同样的;若是session不存在,就新建一个;若是是false的话,标识若是不存在就返回null;
session的生命周期指的是从Servlet容器建立session对象到销毁的过程。Servlet容器会依据session对象设置的存活时间,在达到session时间后将session对象销毁。session生成后,只要用户继续访问,服务器就会更新session的最后访问时间,并维护该session。
以前在单进程应用中,session我通常是存在内存中的,不会作持久化操做或者说使用三方的服务来存session信息,如redis。可是在分布式场景下,这种存在本机内存中的方式显然是不适用的,由于session没法共享。这个后面说。
session通常在内存中存放,内存空间自己大小就有必定的局限性,所以session须要采用一种过时删除的机制来确保session信息不会一直累积,来防止内存溢出的发生。
session的超时时间能够经过maxInactiveInterval属性来设置。
若是咱们想让session失效的话,也能够当经过调用session的invalidate()来完成。
首先是为何会有这样的概念出现?
先考虑这样一个问题,如今个人应用须要部署在3台机器上。是否是出现这样一种状况,我第一次登录,请求去了机器1,而后再机器1上建立了一个session;可是我第二次访问时,请求被路由到机器2了,可是机器2上并无个人session信息,因此得从新登陆。固然这种能够经过nginx的IP HASH负载策略来解决。对于同一个IP请求都会去同一个机器。
可是业务发展的愈来愈大,拆分的愈来愈多,机器数不断增长;很显然那种方案就不行了。那么这个时候就须要考虑是否是应该将session信息放在一个独立的机器上,因此分布式session要解决的问题其实就是分布式环境下的session共享的问题。
上图中的关于session独立部署的方式有不少种,能够是一个独立的数据库服务,也能够是一个缓存服务(redis,目前比较经常使用的一种方式,即便用Redis来做为session缓存服务器)。