在分布式架构中,有一个很重要的环节,就是分布式网络中的计算机节点彼此之间需要通信,无论是计算机还是人之间,通信变的越来越重要。我们每天都在用浏览器访问各种网站,我们只需要需要输入一个网址就能跳转到相应的服务器,那么这个响应背后的整体流程是什么样的呢?
从一个请求的发出,到服务器的处理,在再到结果数据的返回,这里我把它大致分为三个阶段:
-
第一阶段:发送请求阶段;
-
第二阶段: 请求数据处理阶段;
-
第三阶段: 结果返回阶段;
这里简单分析一下这三个阶段的情况。
从用户在浏览器输入一个http://www.spring.com/doc (
http://192.168.1.2:8080/doc
)
请求开始:
@RestController
public class HelloController {
//请求url对应:http://192.168.1.2:8080/doc
@RequestMapping("/doc")
public String springDocInfo() {
System.out.println("Spring doc info");
return "Hello, SpringMVC" ;
}
}
1、发送请求阶段
第一步:负责域名解析的DNS服务
首先,用户访问一个域名,会经过DNS解析
DNS
(
Domain Name System
),它和http协议一样是位于应用层的协议,主要提供域名到IP的解析服务。我们不用域名也可以访问目标主机的服务,但是IP本身不是那么容易记,所以使用域名进行替换使得用户更容易记住。
-
人:记住域名;
-
机器:记住数字ip;
域名被解析成功后,客户端于服务端之间怎么建立连接呢?
这里就用到了TCP和UDP这两种通信协议,http协议的通信是基于TCP/IP协议之上的一个应用层协议。
涉及到网络协议,我们一定要了解OSI (Open System Interface)的5-7层模型:
1.1、分层设计的思想
-
TCP/IP的分层管理:按照层次分为4层:应用层/传输层/网络层/数据链路层。
-
一般复杂的系统都需要分层,这是一个良好的设计,每一层专注于当前领域的事情。
-
比如我们的分布式服务一般分为:网关层/业务层/服务层/基础支撑层/数据层。
-
分层后,架构清晰,职责清晰。如果某些地方需要修改,我们只需要把变动的层替换掉就行,一方面改动影响较少,另一方面整个架构的灵活性也更高,可扩展性强。
这里继续:
请求经过DNS之后,在TCP/IP四层网络模型中所做的事情:
当应用程序用TCP传送数据时,数据被送入协议栈中,然后逐个通过每一层逐层处理直到完成数据格式的封装后,其中每一层对收到的数据都要增加一些首部信息(有时还要增加尾部信息),最后作为一串比特流送入网络。具体流程实现如下:
第二步:选择协议传输;如果是TCP,则加入当前的协议http头;(类似于寄快递先选择一个快递公司)
第三步:增加ip头;ip地址是一个网卡在网络中的通讯地址;(类似于填写包裹要寄送的地址门牌号,平时遇到的网络地址冲突不能上网的情况,一般就是ip地址冲突导致。相当于一个小区所有的门牌号都一样的时候,这个快递也不知道送往哪里)
第四步:增加MAC头;表示这个数据包要发送到的网卡地址,MAC地址是全局唯一的;(类似于填写包裹要寄的具体的人)
第五步:转换为比特流经网卡进行网络传输;此时,数据被格式化封装完毕,来到数据链路层,转化为bit流经过网卡网络传输,开始寻找目标服务器;(类似于快递打包运输,开始派送),具体示意图如下:
2、请求的接受及处理阶段
2.1、服务端请求接受
接收端收到数据包以后的处理过程就是请求发送的反向过程:
当目标主机收到一个以太网数据帧时
TCP
+
端口(IP四源组),数据就开始从协议栈中由底向上升,同时去掉各层协议加上去的报文首部。每层协议都要去检查报文首部中的协议标示,以确定接受数据是否是发送给自己的。
第一步:客户端找到目标服务
在客户端发起请求的时候,我们会在数据链路层去组装目标机器的MAC地址,目标机器的mac地址怎么得到呢?
这里涉及到一个
ARP
(
Address Resolution Protocal
)协议,这个协议简单的来说就是已知目标机器的ip,需要获得目标机器的mac地址。
具体过程:发送一个广播消息,这个ip是谁的,请来认领。认领ip的机器会发送一个mac地址的响应。
有了这个目标mac地址,数据包在链路上广播,第二层的链路层mac的网卡才能确认,这个包是给它的。
第二步:第三层确认ip是自己的
Mac的网卡把包收进来,然后打开IP包,发现ip是自己的,
第三步:第四层确认端口也是自己的
再打开tcp包,发现端口是8080,而这个时候有一个服务器的进程刚好是监听的8080端口;示意图如下:
终于,请求找到我们的web服务器tomcat,但其实在真实的应用架构中,请求来到我们tomcat之前还要经过一步,那就是负载均衡。
2.2、负载均衡
在微服务分布式的互联网时代,服务端一般都会采用集群的方式,所以请求到达真实目标服务器之前,一般请求会先到达例如Nginx服务器,这里就涉及到负载均衡问题。负载均衡一般也是通过网络的7层来划分设计的,也叫分层负载。一个http请求过来,一定是从应用层到传输层,完成整个交互。只要在网络上跑的数据包,都是完整的。可以有下层没有上层,但是绝对不可以有上层没有下层。常见的方案如下:
二层负载:网络接口层二层负载均衡会通过提供一个虚拟的
MAC
地址,然后分配到真实的 mac 地址;
二层负载是针对mac,负载均衡服务器对外提供一个VIP(虚ip),集群中不同的机器采用相同的ip地址,但是机器的mac地址不一样。当负载均衡服务器接收到的请求之后,通过改写报文的目标mac地址的方式将请求转发到目标机器实现负载均衡。
三层负载均衡:网络层三层负载均衡通过提供一个
虚拟的
ip
地址,然后再分配到真实的集群IP地址。
三层负载时针对 ip,和二层负载均衡类似,负载均衡服务器对外依然提供一个VIP(虚ip),但是集群中不同的机器采用不同的ip地址。当负载均衡服务器接收到请求之后,根据不同的负载均衡算法,通过ip将请求转发至不同的真实服务器。
四层负载均衡:传输层 四层通过
虚
ip +
端口接受请求,然后再分配到真实的服务器;
四层负载均衡工作在osi模型的传输层,由于在传输层,只有TCP/IP协议,这两种协议中除了包含源ip/目标ip外,还包含源端口及目标端口。四层负载均衡服务器在接受到客户端请求后,以后通过修改数据包的地址信息(ip + 端口号)将流量转发到应用服务器。常用的4层负载有:f5和lvs。
七层负载均衡:应用层七层通过
URL
或主机名经过处理后转发到真实的服务器。
七层负载均衡工作在osi模型的应用层,应用层协议多,常用http/radius/dns等,七层负载就可以基于这些协议来负载。这些应用程协议中会包含很多有意义的内容。比如同一个web服务器的负载均衡,除了根据 ip加端口进行负载外,还可以根据七层的 URL/请求的内容来决定是否要进行负载均衡,更加智能。常用的7层负载有:A
pache-Nginx.
2.3、请求数据处理
经历了负载均衡后请求终于可以连接我们的tomcat了,但是浏览器是不能直接调用我们的java代码的,
这里需要了解一些容器的作用,
先明确一下职责:
-
Tomcat :是一个能独立支持Servlet和JSP运行的容器,经常作为一个WEB服务器使用,负责接收和返回http请求;
-
Servlet : 人称 江湖一哥地位, SpringMVC:Dispatchservlet 和 S truts/Sp ring的最底层其实都是基于 servlet来改造的;
-
SpringMVC :在tomcat看来:SpringMVC实现了HttpServlet,其实就是一个servlet;
当请求
(
http://192.168.1.2:8080/doc
)经过层层路由与校验筛选,穿过负载均衡,到达tomcat后的流程如下:
-
servlet实例部署在 web服务器tomcat中,然后浏览器通过 ip+端口 向 tomcat 发出 http 请求;
-
Tomcat 分析校验 http 请求报文信息,将 http请求报文 HttpS ervletRequest解析封装为servlet的标准接口的 ServletRequest,
-
然后通过 java servlet API接口service() 将ServletRequest传递给servlet;
-
servlet 通过请求信息到 HandlerMapping 中去查找key= url 相对对应的 Handler处理器 ;
-
Handle处理完后,servlet 生成一个 ServletResponse 对象交给 tomcat ;
-
tomcat 生成一个 HttpServletResponse 返回给客户端;
3、结果返回阶段
服务器返回结果及客户端接受结果过程同前面的请求发送和接受过程一样,具体不在赘述。因为来的时候有MAC地址,返回的时候,源mac地址就变成了目标mac地址,再返给请求的机器。为了避免每次都用ARP请求,机器本地也会进行ARP缓存。当然机器会不断上下线,ip也会变,所以ARP的mac地址缓存过一段时间就会过期。
到此,一个url完整工作流程分析结束,打卡下班。
4、SpringMVC之HandlerMapping处理器源码解析
这里再补充一个服务器请求处理的细节问题,请求通过Nginx后,这之后就会直接转发到我们真实的服务器上。
思考一:
为什么一个http请求(
http://192.168.1.2:8080/doc
)过来了,SpringMVC会迅速的找到对应的springDocInfo()这个方法调用处理呢?
@RestController
public class HelloController {
//请求url:http://192.168.1.2:8080/doc
@RequestMapping("/doc")
public String springDocInfo() {
System.out.println("Spring doc info");
return "Hello, SpringMVC" ;
}
}
这个问题也可以转化为下面这个思考题。
思考二:为什么加了@C
ontroller和@RequestMapping这两个注解就可以定位到method方法?
-
@RequestMapping
-
@RestController
简单原因解释:
在Spring容器启动的时候,其实
做了很多初始化的工作
它会扫描我们的jar包或者是war包中的所有class Bean文件,所有加了RestController和RequestMapping注解的Bean,spring都会去遍历它们的方法,遍历方法的时候将它们的路径url和对应的方法method
放到一个map中去,所以当一个请求过来的时候,就可以迅速在map中定位到该url对应的处理器,也就是url对应的处理方法method。
主要有两个过程:
4.1、url和method的注册过程
-
在容器的初始化的时候,这里面有个非常重要的 handlerMapping组件-请求映射器:
-
它实现了 InitializingBean这个接口,这里面有个 afterPropertiesSet()方法:
-
随着spring容器的启动,先扫描war包后,会执行一个initHandlerMethod方法;
-
通过 isAnnotationPresent(Controller.class)判断这个类是否加了:@RestController和@RequestMapping注解
-
如果加了这两个注解,则遍历出类的所有方法,将加了RequestMapping的路径url和方法名method缓存到一个map注册中心,即: m apingLookup(url,method);
4.2、请求的调用过程
-
当一个请求进来之后:通过map.get(urlPath)得到url对应的处理器method,也就是方法名称;
-
这样通过处理器逻辑处理后,得到数据结果返回给客户端;
4.3、HandlerMapping源码解析
这里有个很重要的方法:afterPropertiesSet(),它的主要执行时机是:
-
1.spring容器启动的时候,先根据beanDefinition调用createBean()创建出了bean的实例;
-
2.接着执行了populateBean()方法 把属性和依赖都注入完成;
-
3.之后执行initializeBean(beanName, exposedObject, mbd);方法 这里会调用Aware相关方法以及afterPropertiesSet和initMethod方法;
可见afterPropertiesSet()方法调用都是在bean实例已经创建好,且属性值和依赖的其他bean实例都已经注入以后执行的。
下面先来分析一下url和method的注册过程:在容器启动初始化的过程中,在spring中有一个很重要的组件叫做:
HandlerMapping。
public interface HandlerMapping {
HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}
/**
它有一个抽象子类:
*/
public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {
/**
* Bean name prefix for target beans behind scoped proxies. Used to exclude those
* targets from handler method detection, in favor of the corresponding proxies.
*/
//在容器完成Bean属性赋值初始化init之后,postProcessAfterInitialization之前就会执行这个方法
@Override
public void afterPropertiesSet() {
initHandlerMethods(); //spring容器在启动完成之前会执行这段代码
}
}
容器启动的时候开始初始化url和method的关系:
//初始化这个方法url和method的关系
protected void initHandlerMethods() {
//1、将所有的bean的名字加载到数组
String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :
obtainApplicationContext().getBeanNamesForType(Object.class));
//得到所有bean的名字
for (String beanName : beanNames) {
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
Class<?> beanType = null;
try {
beanType = obtainApplicationContext().getType(beanName);
} catch (Throwable ex) {
// An unresolvable bean type, probably from a lazy bean - let's ignore it.
if (logger.isDebugEnabled()) {
logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);
}
}
//2、判断这个bean是不是加了某个注解
if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
}
}
handlerMethodsInitialized(getHandlerMethods());
}
@Override
protected boolean isHandler(Class<?> beanType) {
//3、判断这个class是不是有Controller注解和RequestMapping注解
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}
public static boolean hasAnnotation(AnnotatedElement element, Class<? extends Annotation> annotationType) {
//4、判断该类上面是不是有Controller或者RequestMapping注解
if (element.isAnnotationPresent(annotationType)) {
return true;
}
return Boolean.TRUE.equals(searchWithFindSemantics(element, annotationType, null, alwaysTrueAnnotationProcessor));
}
如果加了Controller或者是 RequestMapping注解:
//4.1 处理加了注解的类requestmapping
protected void detectHandlerMethods(final Object handler) {
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());
if (handlerType != null) {
final Class<?> userType = ClassUtils.getUserClass(handlerType);
//得到所有加了controller或者是RequestMapping注解的方法名称;
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
return getMappingForMethod(method, userType);
}
});
if (logger.isDebugEnabled()) {
logger.debug(methods.size() + " request handler methods found on " + userType + ": " + methods);
}
for (Map.Entry<Method, T> entry : methods.entrySet()) {
Method invocableMethod = AopUtils.selectInvocableMethod(entry.getKey(), userType);
T mapping = entry.getValue();
//5、如果加了controller或者是RequestMapping注解后,找出这些方法,将其注册到map中去
registerHandlerMethod(handler, invocableMethod, mapping);
}
}
}
//6、将url和method注册到注册中心;
protected void registerHandlerMethod(Object handler, Method method, T mapping) {
//注册中心: registor (路径, 类,加了注解的方法)
this.mappingRegistry.register(mapping, handler, method);
}
public void register(T mapping, Object handler, Method method) {
this.readWriteLock.writeLock().lock();
try {
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
assertUniqueMethodMapping(handlerMethod, mapping);
if (logger.isInfoEnabled()) {
logger.info("Mapped \"" + mapping + "\" onto " + handlerMethod);
}
//6.1 这里就完成了url和method关系的注册
this.mappingLookup.put(mapping, handlerMethod);
List<String> directUrls = getDirectUrls(mapping);
for (String url : directUrls) {
this.urlLookup.add(url, mapping);
}
//很多注册中心,所谓的注册中心就是一堆的map;
class MappingRegistry {
private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
// T 访问路径 HandlerMethod: 方法名称,这里就存储了url和method的注册关系;
private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();
请求调用处理过程二:那么当一个请求过来后就会进行相应的方法的调用,本质就是:method = map.get(url)
@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
//1、获取请求的url
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
if (logger.isDebugEnabled()) {
logger.debug("Looking up handler method for path " + lookupPath);
}
this.mappingRegistry.acquireReadLock();
try {
//2、通过url在lookup map中查找具体处理请求的handler;
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
if (logger.isDebugEnabled()) {
if (handlerMethod != null) {
logger.debug("Returning handler method [" + handlerMethod + "]");
}
else {
logger.debug("Did not find handler method for [" + lookupPath + "]");
}
}
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
finally {
this.mappingRegistry.releaseReadLock();
}
}
@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<>();
//2.1 到注册中心去通过url来查找相对应的method处理器;
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
addMatchingMappings(directPathMatches, matches, request);
}
@Nullable
public List<T> getMappingsByUrl(String urlPath) {
//2.2 从lookup中去查找到method处理器然后返回;
return this.urlLookup.get(urlPath);
}
OK,到这里为止,一个url如何快速定位到一个controller的流程就结束了。
愿出走半生,归来仍是少年。
5、小结
总结起来,其实干的事情也很简单:请求-处理-返回。但是看似一个小小的请求,却隐藏串联着大故事。
水滴石穿,积少成多。学习笔记,内容简单,用于复习,梳理巩固。