目录

  • Cookie
  • Session

从http协议的无状态性说起

HTTP是一种无状态协议。关于这个无状态之前我也不太理解,因为HTTP底层是TCP,既然是TCP,就是长连接,这个过程是保持连接状态的,又为什么说http是无状态的呢?先来搞清楚这两个概念:

无连接和无状态

  • 无连接

    每次连接只处理一个请求,服务端处理完客户端一次请求,等到客户端作出回应之后便断开连接;

  • 无状态

    是指服务端对于客户端每次发送的请求都认为它是一个新的请求,上一次会话和下一次会话没有联系;

无连接的维度是连接,无状态的维度是请求;http是基于tcp的,而从http1.1开始默认使用持久连接;在这个连接过程中,客户端可以向服务端发送多次请求,但是各个请求之间的并没有什么联系;这样来考虑,就很好理解无状态这个概念了。

持久连接

持久连接,本质上是客户端与服务器通信的时候,建立一个持久化的TCP连接,这个连接不会随着请求结束而关闭,通常会保持连接一段时间。

现有的持久连接类型有两种:HTTP/1.0+的keep-alive和HTTP/1.1的persistent。

  • HTTP/1.0+的keep-alive

先来开一张图:

session和cookie-LMLPHP

这张图是请求www.baidu.com时的请求头信息。这里面我们需要注意的是:

connection: keep-alive
复制代码

我们每次发送一个HTTP请求,会附带一个connection:keep-alive,这个参数就是声明一个持久连接。

  • HTTP/1.1的persistent

HTTP/1.1的持久连接默认是开启的,只有首部中包含connection:close,才会事务结束之后关闭连接。当然服务器和客户端仍可以随时关闭持久连接。

当发送了connection:close首部之后客户端就没有办法在那条连接上发送更多的请求了。当然根据持久连接的特性,一定要传输正确的content-length。

还有根据HTTP/1.1的特性,是不应该和HTTP/1.0客户端建立持久连接的。最后,一定要做好重发的准备。

http无状态

OK,首先来明确下,这个状态的主体指的是什么?应该是信息,这些信息是由服务端所维护的与客户端交互的信息(也称为状态信息); 因为HTTP本身是不保存任何用户的状态信息的,所以HTTP是无状态的协议。

如何保持状态信息

在聊这个这个问题之前,我们来考虑下为什么http自己不来做这个事情:也就是让http变成有状态的。

  • http本身来实现状态维护

    从上面关于无状态的理解,如果现在需要让http自己变成有状态的,就意味着http协议需要保存交互的状态信息;暂且不说这种方式是否合适,但从维护状态信息这一点来说,代价就很高,因为既然保存了状态信息,那后续的一些行为必定也会受到状态信息的影响。

    从历史角度来说,最初的http协议只是用来浏览静态文件的,无状态协议已经足够,这样实现的负担也很轻。但是随着web技术的不断发展,越来越多的场景需要状态信息能够得以保存;一方面是http本身不会去改变它的这种无状态的特性(至少目前是这样的),另一方面业务场景又迫切的需要保持状态;那么这个时候就需要来“装饰”一下http,引入一些其他机制来实现有状态。

  • cookie和session体系

    通过引入cookie和session体系机制来维护状态信息。即用户第一次访问服务器的时候,服务器响应报头通常会出现一个Set-Cookie响应头,这里其实就是在本地设置一个cookie,当用户再次访问服务器的时候,http会附带这个cookie过去,cookie中存有sessionId这样的信息来到服务器这边确认是否属于同一次会话。

Cookie

cookie是由服务器发送给客户端(浏览器)的小量信息,以{key:value}的形式存在。

Cookie机制原理

客户端请求服务器时,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。而客户端浏览器会把Cookie保存起来。当浏览器再请求 服务器时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器通过检查该Cookie来获取用户状态。

我们通过看下servlet-api中Cookie类的定义及属性,来更加具体的了解Cookie。

Cookie在servlet-api中的定义

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;
    //....省略其他方法
}
复制代码

Cookie属性

  • name

    cookie的名字,Cookie一旦创建,名称便不可更改

  • value

    cookie值

  • comment

    该Cookie的用处说明。浏览器显示Cookie信息的时候显示该说明

  • domain

    可以访问该Cookie的域名。如果设置为“.baidu.com”,则所有以“baidu.com”结尾的域名都可以访问该Cookie;第一个字符必须为“.”

  • maxAge

    Cookie失效的时间,单位秒。

    • 正数,则超过maxAge秒之后失效。
    • 负数,该Cookie为临时Cookie,关闭浏览器即失效,浏览器也不会以任何形式保存该Cookie。
    • 为0,表示删除该Cookie。
  • path

    该Cookie的使用路径。例如:

    • path=/,说明本域名下contextPath都可以访问该Cookie。
    • path=/app/,则只有contextPath为“/app”的程序可以访问该Cookie

    path设置时,其以“/”结尾.

  • secure

    该Cookie是否仅被使用安全协议传输。这里的安全协议包括HTTPS,SSL等。默认为false。

  • version

    该Cookie使用的版本号。

    • 0 表示遵循Netscape的Cookie规范,目前大多数用的都是这种规范;
    • 1 表示遵循W3C的RFC2109规范;规范过于严格,实施起来很难。

    在servlet规范中默认是0;

  • isHttpOnly

    HttpOnly属性是用来限制非HTTP协议程序接口对客户端Cookie进行访问;也就是说如果想要在客户端取到httponly的Cookie的唯一方法就是使用AJAX,将取Cookie的操作放到服务端,接收客户端发送的ajax请求后将取值结果通过HTTP返回客户端。这样能有效的防止XSS攻击。

上述的这些属性,除了name与value属性会被提交外,其他的属性对于客户端来说都是不可读的,也是不可被提交的。

创建Cookie

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更新

在源码中可以知道,Cookie本身并没有提供修改的方法;在实际应用中,一般通过使用相同name的Cookie来覆盖原来的Cookie,以达到更新的目的。

但是这个修改的前提是需要具有相同domain,path的 Set-Cookie 消息头

Cookie cookie = new Cookie("cookieSessionId","new-qwertyuiop");
response.addCookie(cookie);
复制代码

Cookie删除

与Cookie更新一样,Cookie本身也没有提供删除的方法;但是从前面分析Cookie属性时了解到,删除Cookie可以通过将maxAge设置为0即可。

Cookie cookie = new Cookie("cookieSessionId","new-qwertyuiop");
cookie.setMaxAge(0);
response.addCookie(cookie);
复制代码

上面的删除是我们自己可控的;但是也存在一些我们不可控或者说无意识情况下的删除操作:

  • 如果maxAge是负值,则cookie在浏览器关闭时被删除
  • 持久化cookie在到达失效日期时会被删除
  • 浏览器中的 cookie 数量达到上限,那么 cookie 会被删除以为新建的 cookie 创建空间。

其实很多情况下,我们关注的都是后者。关于数量上限后面会说到。

从请求中获取Cookie

 Cookie[] cookies = request.getCookies();
复制代码

Cookie同源与跨域

我们知道浏览器的同源策略:

对于Cookie来说,Cookie的同源只关注域名,是忽略协议和端口的。所以一般情况下,https://localhost:80/和http://localhost:8080/的Cookie是共享的。

Cookie是不可跨域的;在没有经过任何处理的情况下,二级域名不同也是不行的。(wenku.baidu.com和baike.baidu.com)。

Cookie数量&大小限制及处理策略

注:数据来自网络,仅供参考

因为浏览器对于Cookie在数量上是有限制的,如果超过了自然会有一些剔除策略。在这篇文章中Browser cookie restrictions提到的剔除策略如下:

最近最少使用(LRU)方法:在达到cookie限制时自动地剔除最老的cookie,以便腾出空间给许最新的cookie。Internet Explorer和Opera使用这种方法。

Firefox决定随机删除Cookie集中的一个Cookie,并没有什么章法。所以最好不要超过Firefox中的Cookie限制。

超过大小长度的话就是直接被截取丢弃;

Session

Cookie机制弥补了HTTP协议无状态的不足。在Session出现之前,基本上所有的网站都采用Cookie来跟踪会话。

与Cookie不同的是,session是以服务端保存状态的。

session机制原理

当客户端请求创建一个session的时候,服务器会先检查这个客户端的请求里是否已包含了一个session标识 - sessionId,

  • 如果已包含这个sessionId,则说明以前已经为此客户端创建过session,服务器就按照sessionId把这个session检索出来使用(如果检索不到,可能会新建一个)
  • 如果客户端请求不包含sessionId,则为此客户端创建一个session并且生成一个与此session相关联的sessionId

sessionId的值一般是一个既不会重复,又不容易被仿造的字符串,这个sessionId将被在本次响应中返回给客户端保存。保存sessionId的方式大多情况下用的是cookie。

HttpSession

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

创建session的方式是通过request来创建;

// 1、创建Session对象
HttpSession session = request.getSession();
// 2、创建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信息不会一直累积,来防止内存溢出的发生。

session的超时时间可以通过maxInactiveInterval属性来设置。

如果我们想让session失效的话,也可以当通过调用session的invalidate()来完成。

分布式session

首先是为什么会有这样的概念出现?

先考虑这样一个问题,现在我的应用需要部署在3台机器上。是不是出现这样一种情况,我第一次登陆,请求去了机器1,然后再机器1上创建了一个session;但是我第二次访问时,请求被路由到机器2了,但是机器2上并没有我的session信息,所以得重新登录。当然这种可以通过nginx的IP HASH负载策略来解决。对于同一个IP请求都会去同一个机器。

但是业务发展的越来越大,拆分的越来越多,机器数不断增加;很显然那种方案就不行了。那么这个时候就需要考虑是不是应该将session信息放在一个独立的机器上,所以分布式session要解决的问题其实就是分布式环境下的session共享的问题。

session和cookie-LMLPHP

上图中的关于session独立部署的方式有很多种,可以是一个独立的数据库服务,也可以是一个缓存服务(redis,目前比较常用的一种方式,即使用Redis来作为session缓存服务器)。


作者:glmapper
链接:https://juejin.im/post/5aede266f265da0ba266e0ef
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

02-25 04:15