Http1.1特性
无状态的协议
HTTP 是一种不保存状态,即无状态(stateless)协议。 HTTP 协议自身不对请求和响应之间的通信状态进行保存。也就是说在 HTTP 这个级别,协议对于发送过的请求和响应都不做持久化处理。使用 HTTP 协议,每当有新的请求发送时,就会有对应的新响应产生。协议本身并不保留之前一切的请求或响应报文的信息。这是为了更快地处理大量事务,确保协议的可伸展性,而特意把 HTTP 协议设计成如此简单。我们来看下面这个例子:
【有状态】:
【无状态】:
可以看到如果是有状态协议,在【协议层】,本次请求会依赖也可以依赖上次请求的结果,而在使用无状态协议通信时,在【协议层】,本次请求不会依赖也无法依赖上次请求的结果,请注意,这里说得都是协议层!协议层是否有状态跟我们会话或服务是否有状态并没有必然联系,我们完全可以使用http这种无状态的协议搭建一个有状态的服务。
在使用http协议时,由于它是无状态的,换句话说,它的每个请求都是完全独立的,因此每个请求都应该包含处理这个请求所需的完整的数据。所以在使用无状态协议进行通信时为了完成前文中的对话,整个通信过程应该如下图所示:
Http状态管理
通过上文我们知道http协议是无状态的,而现如今我们在使用http协议跟服务器进行交互时,或者说我们通过http发起的会话基本都是有状态的,例如我们在网上购物时都需要保持用户登录的状态或记录用户购物车中的商品,举个例子:
假设我们正在在购物网站上买一个书包,流程如下:
- 输入账号密码登陆 【/login】 ======> 用户信息
- 选择一款你喜欢的书包加入到购物车中【/cart】 ======> 用户信息+产品信息
- 购买支付 【/pay】 ======> 用户信息+产品信息+金额信息
所谓的登录只是验证你是否是一个合法用户,若是合法则跳转到信息的页面,不合法则告知用户名密码错误。但是我们在第一步给服务器发完【/login】接口后,服务器就忘记了…,忘记了你这个人到底有没有经过认证。所以在添加商品时 你还是需要将你的账号密码和商品信息一起提交给【/cart】接口,再让服务器做验证。第三步同理。
可以看到无状态的http协议在交互的场景下使用起来非常繁琐,「HTTP本身是一个无状态的连接协议」,为了支持客户端与服务器之间的交互,「我们需要为交互存储状态」,因此Http协议需要提供一种「状态管理机制」。
Http的状态管理机制实际上依赖了两个头部字段
- 请求头:「Cookie」
- 响应头:「Set-Cookie」
这里我直接使用「RFC文档:https://datatracker.ietf.org/doc/html/rfc6265#page-3」中的几个例子来对这两个头部字段进行说明:
示例一:
== Server -> User Agent == Set-Cookie: SID=31d4d96e407aad42 == User Agent -> Server == Cookie: SID=31d4d96e407aad42
服务器在对客户端进行响应时可以添加一个响应头字段:「Set-Cookie」,其中的内容为 SID=31d4d96e407aad42,客户端在接受到响应信息后会将「Set-Cookie」中的内容保存起来,并在下次发送请求时,通过请求头部字段「Cookie」将信息发送到服务器。
示例二:
== Server -> User Agent == Set-Cookie: SID=31d4d96e407aad42; Path=/; Domain=example.com == User Agent -> Server == Cookie: SID=31d4d96e407aad42
如上例所示,Set-Cookie可以通过Path及Domain两个属性指定Cookie的作用域,客户端会根据Cookie的作用域决定向服务器发送请求时是否要携带此Cookie。
Domain决定Cookie在哪个域是有效的,也就是决定在向该域发送请求时是否携带此Cookie,Domain的设置是对子域生效的,如Doamin设置为 .a.com,则b.a.com和c.a.com均可使用该Cookie,但如果设置为b.a.com,则c.a.com不可使用该Cookie。Domain参数必须以点(“.”)开始。
Path是Cookie的有效路径,和Domain类似,也对子路径生效,如Cookie1和Cookie2的Domain均为a.com,但Path不同,Cookie1的Path为 /b/,而Cookie的Path为 /b/c/,则在a.com/b页面时只可以访问Cookie1,在a.com/b/c页面时,可访问Cookie1和Cookie2。Path属性需要使用符号“/”结尾。
示例三:
== Server -> User Agent == Set-Cookie: SID=31d4d96e407aad42; Path=/; Secure; HttpOnly Set-Cookie: lang=en-US; Path=/; Domain=example.com == User Agent -> Server == Cookie: SID=31d4d96e407aad42; lang=en-US
服务器可以向客户端设置多个Cookie。如上例所示,服务器通过第一个Set-Cookie向客户端设置了一个用户本次会话id,除此之外还通过Set-Cookie通知了客户端用户在会话过程中希望采用的语音是lang=en-US。
我们可以看到在第一个Set-Cookie中我们还指定了Cookie的两个熟悉Secure、HttpOnly。
Cookie的Secure属性,意味着保持Cookie通信只限于加密传输,指示浏览器仅仅在通过安全/加密连接才能使用该Cookie。如果一个Web服务器从一个非安全连接里设置了一个带有secure属性的Cookie,当Cookie被发送到客户端时,它仍然能通过中间人攻击来拦截。
Cookie的HttpOnly属性,指示浏览器不要在除HTTP(和 HTTPS)请求之外暴露Cookie。一个有HttpOnly属性的Cookie,不能通过非HTTP方式来访问,例如通过调用JavaScript(例如,引用 document.cookie),因此,不可能通过跨域脚本(一种非常普通的攻击技术)来偷走这种Cookie。尤其是Facebook 和 Google 正在广泛地使用HttpOnly属性。
示例四:
== Server -> User Agent == Set-Cookie: lang=en-US; Expires=Wed, 09 Jun 2021 10:18:14 GMT == User Agent -> Server == Cookie: SID=31d4d96e407aad42; lang=en-US
服务器向客户端设置Cookie时可以通过Cookie的Expires熟悉指定其有效时间。但是请注意,客户端也可能会因为存储容量等问题自行将Cookie删除。
示例五:
== Server -> User Agent == Set-Cookie: lang=; Expires=Sun, 06 Nov 1994 08:49:37 GMT == User Agent -> Server == Cookie: SID=31d4d96e407aad42
最后这个例子想要说明的是,如果服务器希望删除客户端上的某个Cookie可以通过Set-Cookie将其过期时间设置为过去的某个时间。
代码实现分析
在上篇文章中我们已经搭建了用来调试的服务端及客户端,基于我们添加调试Cookie相关代码,如下:
服务端
public class HttpServer { public static void main(String[] args) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.option(ChannelOption.SO_BACKLOG, 1024); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new HttpHelloWorldServerInitializer()); Channel ch = b.bind(8080).sync().channel(); ch.closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } } public class HttpHelloWorldServerInitializer extends ChannelInitializer<SocketChannel> { @Override public void initChannel(SocketChannel ch) { ChannelPipeline p = ch.pipeline(); p.addLast(new HttpServerCodec()); p.addLast(new HttpObjectAggregator(65535)); p.addLast(new HttpHelloWorldServerHandler()); } } /** 这个类是处理http请求的核心类,这里我们简单处理 不论收到什么信息我们都返回Hello World */ public class HttpHelloWorldServerHandler extends SimpleChannelInboundHandler<HttpObject> { private static final byte[] CONTENT = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd'}; @Override public void channelReadComplete(ChannelHandlerContext ctx) { ctx.flush(); } @Override public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) { if (msg instanceof HttpRequest) { HttpRequest req = (HttpRequest) msg; FullHttpResponse response = new DefaultFullHttpResponse( req.protocolVersion(), OK, Unpooled.wrappedBuffer(CONTENT)); // 1⃣️、在响应信息中添加Set-Cookie头 final String setCookie = ServerCookieEncoder.STRICT.encode("name", "dmz"); response.headers() .set(CONTENT_TYPE, TEXT_PLAIN) .setInt(CONTENT_LENGTH, CONTENT.length) .set(HttpHeaderNames.SET_COOKIE, setCookie); final HttpHeaders headers = req.headers(); // 2⃣️、解析客户端请求信息中的Cookie信息并打印 final String cookieUnDecode = headers.get(HttpHeaderNames.COOKIE); if (cookieUnDecode != null && !"".equals(cookieUnDecode)) { final Set<Cookie> decodes = ServerCookieDecoder.STRICT.decode(cookieUnDecode); if (decodes != null) { for (Cookie decode : decodes) { System.out.println(decode.name() + "=" + decode.value()); } } } ctx.write(response); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }
客户端
public class HttpClient { // 用于存储Cookie static final BasicCookieStore BASIC_COOKIE_STORE = new BasicCookieStore(); static final CloseableHttpClient HTTP_CLIENT = HttpClientBuilder.create() .setConnectionManager(new BasicHttpClientConnectionManager()) .setDefaultCookieStore(BASIC_COOKIE_STORE) // 指定客户端的Cookie规范 .setDefaultCookieSpecRegistry(new Lookup<CookieSpecProvider>() { @Override public CookieSpecProvider lookup(String s) { return new DefaultCookieSpecProvider(); } }) .build(); public static void main(String[] args) throws Exception { // 使用Scanner方便调试 Scanner scanner = new Scanner(System.in); while (true) { // 程序会一直阻塞,直到我们在控制台输入命令 // 没输入一次,会向服务器发送一个请求,避免多次重启,方便调试 final String command = scanner.nextLine(); if ("close".equals(command)) { HTTP_CLIENT.close(); break; } final HttpGet httpGet = new HttpGet("http://127.0.0.1:8080"); final CloseableHttpResponse execute = HTTP_CLIENT.execute(httpGet); for (Cookie cookie : BASIC_COOKIE_STORE.getCookies()) { System.out.println(cookie.getName() + "=" + cookie.getValue()); } // 保证连接释放到连接池 System.out.println(EntityUtils.toString(execute.getEntity())); } } }
服务端代码比较简单,不多做解释,我们看看客户端代码,可能有疑问的主要是两点
1.BasicCookieStore
我们通过浏览器发送请求时,浏览器会自动帮我们将Cookie进行持久化。通过设置==>隐私设置和安全性==>Cookie及其他网站数据,能查其持久化的Cookie信息,如下:
我们在使用Apache HttpClient发送请求时,为了调试Cookie相关代码,也需要提供一种Cookie持久化机制,这里我们直接使用它默认提供的BasicCookieStore,其内部是一个TreeSet。
2.DefaultCookieSpecProvider:
见名知意,这个类的作用是提供一个Cookie规范,主要包括对Set-Cookie的解析、解析后内容的校验等。
上面这段程序的作用在于
1.客户端每次请求后打印服务器设置的Cookie
2.服务器每次打印客户端请求时携带的Cookie
服务器的代码非常简单,不做过多分析,我直接看HttpClient在Cookie上做了哪些处理
1.解析响应头中的Set-Cookie,代码位于org.apache.http.client.protocol.ResponseProcessCookies#processCookies,如下:
2.每次请求时,携带服务器设置的Cookie,代码位于org.apache.http.client.protocol.RequestAddCookies#process,如下:
Cookie、Session
【Cookie】的工作原理我相信通过前文中的分析大家应该已经很清晰了,如下图所示:
【Session】是一个抽象概念,开发者为了实现中断和继续等操作,将客户端和服务器之间一对一的交互,抽象为“会话”,进而衍生出“会话状态”,也就是Session的概念。我们通常了解到的Session都是基于Cookie实现的,下面是servlet规范中的一段话
大意为:通过Cookie实现的Session机制是最常用的会话跟踪机制,所有的servlet容器都需要支持。
容器向客户端发送一个cookie。然后,客户端将在每次对服务器的后续请求中返回该cookie,明确地将请求与会话联系起来。会话跟踪cookie的标准名称必须是JSESSIONID。容器可以允许通过容器的特定配置来定制会话跟踪cookie的名称。整个Session的工作原理如下图所示: