——“串联简单基本原理,分享微服务实践经验“之一。
本系列文章主要面向类似我带的项目组中的初中级开发者,串联基本知识,分享开发微服务架构项目的实践经验。
因特网在全球范围内可实现快速的信息交互、使世界变小,HTTP做为全球因特网的公共语言则是功不可没的——浏览器和服务器端的WEB应用程序大多是通过HTTP相互通信的(也有WebSocket)。大多数程序员在开发WEB应用程序时都会用到HTTP协议,而WEB应用框架和APP Server中间件很好的屏蔽了相关的处理细节、简化了开发,所以无需了解其机制就能做开发。程序员要养成“知其然,并其所以然”的习惯,才能在遇到问题时解决问题。
本文会涉及到Http协议的基本概念;浏览器和服务器之间的状态保持:Cookie和Session ;有状态和无状态应用的区别;服务端无状态的身份信息保留机制、共享Session的处理机制,以及一个SpringCloud项目实例的简单说明。
HTTP(超文本传输协议)属于TCP/IP参考模型(主机到网络层、网络互联层、传输层、应用层)的第4层应用层,通常是建立在TCP连接之上的,主要处理Client端和Server端之间的请求和响应。
HTTP 1.0规定浏览器与服务器只保持短暂的连接,每一个TCP连接只处理一个请求,服务器完成请求处理后立即断开TCP连接,不支持断点续传,服务器不跟踪每个客户也不记录过去的请求。1.0协议主要是考虑一个web站点可能会一个时间段内接受上百万来自不同客户端的请求,短链接可以提高连接的重复利用率,但是对于一次访问包含多个资源的情况就比较低效了。
HTTP 1.1支持持久连接,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。此外,HTTP 1.1中增加了Host请求头字段、与身份认证、状态管理和Cache缓存等机制相关的请求头和响应头,支持断点续传,目前绝大多数WEB服务器都采用HTTP/1.1协议。
HTTP协议本身是无状态的,浏览器和服务器之间可以通过Cookie和Session两种机制进行状态保持。
浏览器端基于cookie的状态保存机制:
cookie主要由名、值、过期时间、路径和域几部分组成。若不设置过期时间,则表示这个cookie的生命期为浏览器会话期间,关闭浏览器cookie就消失。这种生命期为浏览器会话期的cookie一般不存储在硬盘上而是保存在内存里。若设置了过期时间,浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie仍然有效直到超过设定的过期时间。路径与域一起构成cookie的作用范围。
如图示Cookie采用的是在客户端保持状态的方案,是通过扩展HTTP Header实现的。
Session是以Hash键值对的形式在服务器端保存浏览器和服务器之间交互会话状态信息。在服务器端可以设置Session的超时时间,若在一定时间段内无访问,则服务器端会按照一定的超时机制使得Session无效。
WEB应用运行在无状态的HTTP协议之上的,一般来说为了便于实现提供给用户便捷的交互体验,通常开发者会WEB应用的多个请求和应答之间存一些公用信息(例如用户信息、流程状态等)的情况,这种情况下WEB应用就是有状态的。这些公用信息可以存在客户端浏览器的Cookie里也可以存在服务器端浏览器的Session里。
有状态的应用系统在分布式部署运维时就相对复杂,例如在负载均衡及容错处理方面,就需要实现session共享机制,使得多台应用服务器之间会话统一。而无状态的RESTful(Representational State Transfer)架构恰好能够避免这些问题。
无状态是指任何一个WEB请求之间是相互隔离的,请求本身包含了所需的全部信息,服务器端不保存任何当前请求相关的资源。这一特性有利于分布式系统的可伸缩性、可靠性维护。
RESTful架构对状态有两种解释:
应用状态(Application State):指的是与某一特定请求相关的状态信息;
资源状态(Resource State):反映了某一存储在服务器端资源在某一时刻的特定状态,该状态与用户无关,任何用户在同一时刻对该资源的请求都会获得这一状态的表现(Representation)。
RESTful架构要求服务器端不保存任何与特定HTTP请求相关的资源,应用状态必须由请求方在请求过程中提供。
越来越多的移动APP是运行和分布式应用都采用无状态架构了。
对于无状态的应用,一般是基于token的机制进行身份认证、获取用户信息。Token在客户端和服务器端交互的过程中会在Header中传递,服务器端会验证每个请求携带的token有效性、并获取到用户基本信息以处理相应的业务逻辑。
1、如图示,用户从浏览器端发起身份认证请求(例如登录),从服务器端获取到Token信息。
2、服务器端身份认证成功后,在Response中将Token写入Cookie,若浏览器端不支持Cookie,则可以Response中的其他Header的信息中返回Token。
3、若token存在Cookie里,则后续请求都自动带着token,若浏览器端不支持Cookie,则需要客户端程序在提交请求时将token写入request的Header中。
4、服务器端收到请求,会验证token有效性并且基于Token获取用户信息用于处理该请求相关的业务逻辑,若Token过期了,则刷新token,并把新的Token以Response消息的Header的形式返回给客户端。
有状态应用在负载均衡的分布式环境下,要想使得在多个服务器上运行的WEB应用保持状态一致,有若干种经前人实践的解决方案。
有一些开源的应用服务器如Tomcat提供的sessionmanager就支持支持Session共享,并存储在Redis或Memcached中。而Spring Session提供了一种不依赖于任何应用服务器的方案,在Servlet规范之内配置可插拔的session数据存储。Spring Session是一种比较全面的Session解决方案,它不仅能支持WEB请求的Session保存和共享还能支持非WEB请求的Session状态保存和共享。本小节重点说明一下Spring-Session-Redis的实现机制。
Spring Session是通过标准的servlet filter来实现的对HTTP请求的包装实现Session共享的机制,这个filter必须要配置为拦截所有的web应用请求,并且它应该是filter链中的第一个filter。Spring Session filter会确保随后调用javax.servlet.http.HttpServletRequest的getSession()方法时,都会返回Spring Session的HttpSession实例,而不是应用服务器默认的HttpSession。
在SpringBoot项目中使用时,配置注解如下:
@Configuration
@EnableRedisHttpSession//配置开启RedisHttpSession
public class SpringSessionConfiguration {
@Bean
public HttpSessionStrategy httpSessionStrategy(Environment env) {
//设置Session策略,HttpSessionStrategy接口有两个默认实现,即CookieHttpSessionStrategy和HeaderHttpSessionStrategy
//前者用写在cookie里的sessionid关联,而后者用写在HTTP header里的标识ID关联请求和session
HeaderHttpSessionStrategy hhss = new HeaderHttpSessionStrategy();
String sessionHeaderName = env.getProperty("server.session.header.name");
if (StringUtils.isNotBlank(sessionHeaderName)) {
hhss.setHeaderName(sessionHeaderName);
}
return hhss;
}
}
可以看到EnableRedisHttpSession实际上是引入了RedisHttpSessionConfiguration
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({RedisHttpSessionConfiguration.class})
@Configuration
public @interface EnableRedisHttpSession {
int maxInactiveIntervalInSeconds() default 1800;
String redisNamespace() default "";
RedisFlushMode redisFlushMode() default RedisFlushMode.ON_SAVE;
}
RedisHttpSessionConfiguration继承自SpringHttpSessionConfiguration,在该类里创建了一个SessionRepositoryFilter对象
@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration implements EmbeddedValueResolverAware, ImportAware
//此处在SpringHttpSessionConfiguration里创建了SessionRepositoryFilter对象
@Bean
public <S extends ExpiringSession> SessionRepositoryFilter extends ExpiringSession> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) {
SessionRepositoryFilter sessionRepositoryFilter = new SessionRepositoryFilter(sessionRepository);
sessionRepositoryFilter.setServletContext(this.servletContext);
if(this.httpSessionStrategy instanceof MultiHttpSessionStrategy) {
sessionRepositoryFilter.setHttpSessionStrategy((MultiHttpSessionStrategy)this.httpSessionStrategy);
} else {
sessionRepositoryFilter.setHttpSessionStrategy(this.httpSessionStrategy);
}
return sessionRepositoryFilter;
}
sessionRepositoryFilter实际上就是一个ServletFilter,经过Filter的Request和Response都被利用Decorator模式进行了包装,附加了共享Session获取及存储的机制,也就是说系统获取到的Session是Spring-Session包装过的Session。
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
SessionRepositoryFilter.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response, this.servletContext, null);
SessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);
HttpServletRequest strategyRequest = this.httpSessionStrategy.wrapRequest(wrappedRequest, wrappedResponse);
HttpServletResponse strategyResponse = this.httpSessionStrategy.wrapResponse(wrappedRequest, wrappedResponse);
try {
filterChain.doFilter(strategyRequest, strategyResponse);
} finally {
wrappedRequest.commitSession();
}
}
//getSession得到的是HttpSessionWrapper
@Override
public HttpSessionWrapper getSession() {
return getSession(true);
}
如图示,项目分为网关、静态页、Open Service层和Internal Service层。
本项目实例是采用Spring-Session-Redis的HeaderHttpSessionStrategy实现的Session共享及保存机制,具体过程和实现机制图示应该说的比较清楚了,时间关系有空再补充细节。