引言
在浏览器上输入一个URL
后发生了什么? 这也是面试中老生常谈的话题,包括网上也有大量关于这块的内容:
从百度的搜索结果来看,能够搜到七千多万条记录,因此本篇不会再以那种前篇一律的方式赘述,而是以目前较新的网络内容,结合系统中的大部分服务,将自己类比成一个请求,切身感受到每个技术栈的具体细节,彻底从“根儿上”理解客户端请求-服务端响应的全过程。
本篇以
https://www.juejin.cn/
为例进行分析,当然,这里假设掘金后端是Java做的(实际上掘金好像是基于Node
做的后端)。
分享一个趣事,我发现掘金貌似使用的是
.cn
后缀的域名,并非通常使用的.com
域名,好像www.juejin.com
被人抢注了....^_^
一、地址栏输入后本地会发生的事情
当我们在浏览器的地址栏中,输入xxx
内容后,浏览器的进程首先会判断输入的内容:
- 如果是普通的字符,那浏览器会使用默认的搜索引擎去对于输入的
xxx
生成URL
。 - 如若输入的是网址,那浏览器会拼接协议名形成完整的
URL
。
当然,在地址栏中输入某个内容后,也会进行一些额外操作,例如:安全检查、访问限制等,但总归而言,浏览器做的第一件工作则是生成URL
,当按下回车后,浏览器进程会将生成的完整URL
发送到网络进程:
当网络进程收到传过来的
URL
后,首先并不会直接发出网络请求,而是会先查询本地缓存:观察上述流程,当网络进程收到传来的
URL
后,会首先通过URL
作为Key
,在本地缓存中进行查询:
- ①如果本地中是否有缓存:
- 没有:发起网络请求去服务器获取资源,成功后将结果渲染页面并写入缓存。
- 有:继续判断本地中的缓存内容是否已经过期,没有则直接使用本地缓存。
- ②如果本地中的缓存已经过期,则会携带
If-Modified-Since、If-None-Match
等标识向服务器发起请求,先判断服务器中的资源是否更新过:- 未更新:服务器返回
304
状态码,并继续读取之前的缓存内容使用。
- 未更新:服务器返回
- ③如若服务器的资源更新过,那么也会向服务器发起请求获取资源。
如果在本地缓存中,无法命中缓存,或者本地缓存已过期并服务器资源更新过,那么此刻网络进程才会真正向目标网站发起网络请求。
二、一个全新的“我”诞生过程与前期的经历
当客户端的网络进程,在查询缓存无果后,会真正开始发送网络请求,但要牢记:客户端的网络进程并非直接向目标网站发起请求的,前期还需经过一些细节处理。
当然,为了能够更直观的感受整个过程,在这里我们将自己“化身”为一个请求,站在请求的角度切身体验一段奇特的“网络旅途”。
2.1、“我”诞生前的准备 - 解析URL
在网络进程发起请求之前,会首先对浏览器进程传过来的URL
进行解析,一般来说完整的URL
结构如下:
但上述结构使用较少,通常情况下,浏览器会使用的
URL
的常用结构如下:URL
中每个字段的释义如下:
-
scheme
:表示使用的协议类型,例如http、https、ftp、chrome
等。 -
://
:协议类型与后续描述符之间的分隔符。 -
domainName
:网站域名,经DNS
解析后会得到具体服务器IP
。 -
/path
:请求路径,代表客户端请求的资源所在位置,不同层级目录之间用/
区分。 -
?query1=value
:请求参数,?
后面表示请求的参数,采用K-V
键值对形式。 -
&query2=value
:多个请求参数,不同的参数之间用&
分割。 -
#fragment
:表示所定位资源的一个锚点,浏览器可根据这个锚点跳转对应的资源位置。
网络进程会根据URL
的结构对目标URL
进行解析,其中有两个关键信息:
- 首先会解析得到协议名,例如
http、https
,这关乎到后续默认使用的端口号。 - 然后会解析得到域名,这个将关乎到后续具体请求的服务器地址。
假设浏览器传输过来的URL
为https://juejin.cn/user/862486453028888/posts
,那么在这个阶段会确定后续请求的服务器端口号为443
,请求的目标域名为www.juejin.cn
。其实在这里主要是根据浏览器的输入信息,去解析出一些“诞生我(请求)”的前置要素。
2.2、“我”该去往的具体位置 - DNS域名解析
在上个阶段已经大概知道“我”该去往何处啦!但我具体地址该到那里呢?“我”好像不大清楚,要不找个人问问吧^_^
。我记得好像有个叫做DNS
的“大家族”是专门负责这个的!我要去找它们问问看~
不过在问
DNS
之前,我先来看看本地有没有域名与IP
的映射缓存,好像没有~,那我只能去找DNS
了(-_-)
,我首先找到了「本地DNS
大叔」,把我要查找的域名交给了它,它让我稍等片刻,它给我找一下,让我们一起来看看「本地DNS
大叔」是怎么查找的:
- ①首先「本地
DNS
大叔」找了它的「根DNS
族长」,族长告诉它应该去找「顶级DNS
长老」。 - ②「本地
DNS
大叔」根据族长的示意去找了「顶级DNS
长老」,然而长老又告诉它应该去找「授权DNS
执事」。 - ③「本地
DNS
大叔」又根据长老的示意找到了「授权DNS
执事」,最终在「授权DNS
执事」那里查到了我手里域名对应着的具体IP
地址。 - ④「本地
DNS
大叔」拿着从「授权DNS
执事」那里查到的IP
,最终把它交给了我,为了下次不麻烦大叔,所以我获取了IP
后,将其缓存在了本地。
呼~,我终于知道我该去哪儿啦!准备出发咯!
更为详细且专业性的查询过程请参考:《HTTP/HTTPS-DNS域名解析系统》。
2.3、确保路途安全 - TCP与TLS握手
问过DNS
大叔后,获得了目的地址的我,此时已经知道该去往何处啦!但在正式出发前,由于前路坎坷,途中会存在各类危机(网络阻塞、网络延迟、第三方劫持等),因此为了我的安全出行,首先还需为我建立一条安全的通道,所以我还需要等一会儿才能出发,俺们一起来瞅瞅建立安全通道的过程是什么样的:
看着好复杂啊~,但似乎大体就分为了两个过程:
首先是
TCP
的三次握手过程,听说这个阶段是为了确保目的地能够正常接收我、也是为了给我建立出一条可靠的出行通道、并且为我计算一下出行失败之后多久重新出发的时间等目的(也就是为了测试双方是否能正常通信、建立可靠连接以及预测超时时间等)。
其实按照之前的“交通规则”,在建立好TCP
连接之后,我就可以继续走下一步啦,但现在有很多坏人,在我们出行的道路上劫持我们,然后窃取、篡改俺们携带的数据,所以如今出行变得很不安全,因此还需要还需要建立一条安全的出行通道,就是TLS
大叔的安全连接~(HTTP+TLS=HTTPS
):
TLS
握手阶段,在这个阶段中,TLS
大叔为了俺的安全出行,会通过很多手段:非对称加密、对称加密、第三方授权等,先和俺的目的地交换一个密钥,然后再通过这个密钥对我加密一下,确保我被坏人抓到了也无法得到俺护送的数据^_^
!
详细且专业性的过程请参考之前的:《计网基础TCP/IP综述-TCP三次握手》、《全解HTTP/HTTPS-SLL、TLS详解》。
2.4、诞生“我的身体” - 构建请求报文
经历上述过程后,安全的出行道路已经建立好啦!但此刻的我还不算完整,所以需要先构建一个“身体”,也就是HTTP
请求报文:
“我的身体”主要由请求行、请求头、空行以及请求主体四部分组成,里面包含了“我本次出远门的需要护送的数据和一些其他信息”。同时,为了我能够在“出行的道路上(传输介质)”安全且正常传输,我还需要经过层层封装:
首先为了确保俺护送的数据安全,
TLS
大叔会先对我的数据进行一次加密,把我原本携带的明文数据转变为看都看不懂的密文,类似下面这个样子:经过加密后的我会紧接着来到传输层,传输层会在我的脑袋上再贴上一个传输头,如果是TCP
大哥的话,它会给我贴上一个TCP
头,但如果传输层的UDP
大哥在的话,它给我贴的就是UDP
头。但不管是谁贴的,在这个传输头内,为了防止我迷路和走丢,TCP、UDP
两位大哥哥都会细心的在里面写清楚“我来自哪里,该去往何处”,也就是源地址和目的地址:
偷偷吐槽一句:
TCP
大哥贴的传输头里面,放了好多好多东西,让我感觉脑袋沉沉的。
过了传输层这一站之后,我又来到了网络层,果不其然,网络层里面最常见的还是IP
大叔,IP
大叔看到我之后,又在我的脑袋上贴上了一个网络头,也就是给我又加了一个IP
头。
哒哒哒~,我出了网络层这关之后,又来到了数据链路层,这关则是由大名鼎鼎的“以太网家族”驻守,在这里我和之前两关不同,除开在我脑袋上贴了一个链路头之外,还给我在尾巴上多加了一个链路尾。
不过刚刚出链路层的时候,好像有个人跟我说:你这个样子是无法在介质上行走的,你要记得改变一下啊!
我还没听的太清楚,就来到了物理层这关,这层和之前我“家里”以及之前的关卡环境都不一样,物理层的小伙伴们好像都有实际的形态,但之前接触所有内容都是虚拟的概念形态哎~。
在我对比物理层大哥们的异样差距时,一不愣神发现我的身体好像发生了“翻天覆地”的变化,整个我似乎都变为了0、1
构成了,正当纳闷时,物理层的某个大哥哥告诉我说:“只有变成这样子,你才可以在出行的道路上行走哈,所以我们给你转换了一下形态,你现在已经可以出发了”。
原来是这样呀,好像链路层的时候有人跟我说过哎~
同样对于更为专业、详细的过程可参考之前的:《HTTP/HTTPS-HTTP报文组成》、《计网基础之TCP/IP-网络分层模型》等内容。
2.5、踏上路途的我 - 数据传输
GO~GO~GO~
,终于出发啦!我终于踏上了网络之旅!呼呼呼~
咔!我来到了第一个中转站,听别人说,好像它的名字叫做路由器,首先路由器大哥把我的身体按照之前封装的步骤层层解封了,但解封到传输层的时候,看到了我脑袋上的传输头,似乎路由器大哥发现了
TCP
哥哥写的目的地址,发现我的目的地还在更远的位置,然后路由器大哥又按照原本的步骤把我的身体封装回去了,然后还亲切的给我指出了接下来该往那条路走,我又该继续前行啦....
我一边走着,一边在思考:好像路由器大哥就是负责给俺们指路的,防止俺们走丢~
具体可参考:《TCP/IP-IP寻址与路由控制》
三、“我”在后端服务器中多姿多彩的历程
啊!路途好遥远呀,我一路走了很久很久,也遇到了很多很多的中转站,每次当我不知道怎么走时,路由器大哥都会温馨的给我指出接下来该走的路途。期间我也走过很多很多路,曾踩着双绞铜线、同轴电缆、光纤前行,当然,可不要小看俺,就算没有物理连接的情况下,我也可以通过无线电技术,通过空气前行呢!
再次声明,文中所谓的道路,就是指数据传输的介质。
3.1、东跑西颠的经历 - 接入层转发
走着走着,突然前方遇到一个叫做CDN
的老爷爷,它问我说要去哪里,我说要去xx
地方办事,和蔼的CDN
老爷爷跟我说,我来看看我这里有没有你要的东西,如果有的话,就不用麻烦你这个小家伙一直跑下去了。可是很遗憾,CDN
老爷爷说它哪儿没有我要的东西,因此我只能继续前行下去。
记不清过了多久,一路跌跌撞撞,在迷迷糊糊中我来到了一个地方,但当我还在分辨时,刷的一下,很快啊,我就被丢到了其他地方,当我回头看的时候,发现刚刚哪个地方,大写着LVS。
LVS
一般会作为大型网站的网关接入层,负责提供更高的并发性能,具体可参考《亿级流量架构设计-LVS篇》。
再直视前方,前方有一个东西很眼熟,难道这就是当初听说过的服务器吗?带着一脸疑惑的我慢慢走了进去,我发现内部空间很大,上面漂浮着一块大陆,名为Linux
大陆,上面有好多好多的“城市(进程)”林立着,那我该去哪一座呢?让我想想!
对了,记起来了好像!!当时出门的时候有人跟我说过:如果你到了目的地之后,不知道该找谁,那么可以根据默认的编号(端口号)去找!
让我回想一下,HTTP
的默认端口是80
,HTTPS
的默认端口是443
,我目前属于HTTPS
派别的请求,那么我应该去找编号为443
的城市!出发出发~
顺着我的推理,我来到了编号443
城市的城门口,当我迈进城门后,嗖的一下,我被一个叫做Nginx
的大叔抓了过去....
- Nginx:小家伙,你是来干嘛的?
-
我:我带了一些数据过来找地址为
IP:443
的地方办事! -
Nginx:噢~,原来是这样啊,我就是负责监听
443
编号的守门将。 - Nginx:小家伙,你过来让我看看....
话音刚落,Nginx
三下五除二的就把我的身体拆开了,然后得到了HTTP
报文,然后从HTTP
报文的请求行中,发现了我本次旅途的具体目标:/user/862486453028888/posts
,然后Nginx
大叔又把我组装了回去,然后根据它内部配置的规则,然后道:
-
Nginx:小家伙,我刚才看了一下,你应该要去的具体位置是
xxx.xxx.xxx.xxx:xx
,快去吧。 - 我:你怎么知道我要去的是这里?
-
Nginx:我刚刚看了一下,你要去的具体位置为
IP:443/user/....
,根据目前的规则以及我代理的地址,你就应该去这里! - 我:大叔大叔,给我看看你代理了那些地址呗。
- Nginx:你可以过来看看。
- 我:哇,为什么这么多!我可不可以去找其他的地址,找其他人帮我办事呀?
- Nginx:不可以噢!按照规则的话,你就应该去我给你的地址哈。
- 我:好吧,那我去啦!
这里的规则是什么呢?其实就是
Nginx
的location
路由匹配规则、upstream
代理集群列表以及负载均衡算法,具体可参考:《Nginx篇:反向代理与负载均衡》、《负载均衡算法原理篇》。
顺着Nginx
大叔给的地址,我又来到了另外一台服务器,上面同样有一块Linux
大陆,然后根据地址在上面找到了一个名为Gateway
的东东,听它自己介绍,好像属于系统网关。但当我找它办事时,它却跟我说:“我不负责具体的业务处理,根据你的目标/user/....
,你应该去找Nacos
注册局,问它们要一下USER-SERVICE
的具体地址,所以,小家伙你还得继续奔波哦”!
好的好的,感谢
Gateway
叔叔指路,那我现在就去啦!
哒哒哒~,迈着愉快的步伐我来到了Nacos
注册局,然后将Gateway
叔叔给我的名字:USER-SERVICE
交给了它们的工作人员,它们的工作人员经过一番查询之后告诉我,这个“品牌”多有个分部,你可以去其中任意一处分部处理你的任务,你可以去:xxx.xxx.xxx.xxx:8080
这个地址噢!
这里的“品牌”是指后端的具体服务,分部是指服务集群中的每个节点。
好的好的,那我就去你说的这个xxx.xxx.xxx.xxx:8080
地址啦!
我一边在路上走着,一边想了一下刚刚过程发生的事情,然后把这个经历画成了一副逻辑图,如下:
回去的时候我一定要跟小伙伴们分享一下这个有趣的经历,耶!
3.2、我遇到了一只大猫咪-叫作Tomcat
根据Nacos
给我的地址,我又来到了一台新的服务器面前,我记得Nacos
给了我一个端口号,要我来到这里之后找编号为8080
的位置,我顺着这个编号慢慢找着,突然在我的前方,出现了一只大老虎,哦不,应该是一只大猫咪,它长这个样子:
它的长相似乎有些报看,但在它的脑门上正好写着我要找到
8080
地址,那我要找的应该就是它了吧!终于到了!我慢慢靠近了这只大猫咪,然后跟它说要找它办事,Tomcat
说要看看我的数据,然后又把我的身体按照之前封装的方式逆向拆开了,从而还原了我最初的身体-HTTP
请求报文,最后Tomcat
说:“我确实是你本次要找的最终目标,不过要办你这件事情得到我肚子里面去噢”!
说罢,
Tomcat
张开了它的血盆大口,一口将我吞了下去.....,正当我以为我完蛋的时候,我却发现Tomcat
内部别有乾坤,上面似乎也有一块小陆地漂浮着,当我凑近的时候才看清楚,原来上面写的是JVM
呀!
我二话不说,一脚踏上了这块陆地,正当我看着上面密密麻麻的“屋子(Java
方法)”迷茫时,此时我正前方就走来了一个人,然后对我做了一个自我介绍:
来自远方的尊敬客人,您好呀,欢迎光临
JVM
神州,我叫Thread-xxx
,是线程家族的一员,您接下来的整个旅途,我终将陪伴在您左右,您需要办的所有事情,都会由我代劳,客官这边请(45
度鞠身)~
然后我一边走着,一边跟Thread-xxx
聊着:
- 我:为什么是你来接我呀?
- 线程:因为每位从远方到来的客人,我们线程家族都会派遣一位子弟迎接。
- 线程:本次轮到我了,因而由我为您本次的旅途提供服务。
- 我:噢噢噢,那我们接下来该去哪儿呢?
- 线程:这需要看客官您本次的目的啦!可以让我看看您本次的旅程吗?
- 我:可以呀,看吧,[我将请求请求行中的资源地址摆了出来]。
-
线程:
/user/....
,原来您是要去这里呀,这边请~。 -
线程:我们首先要去找
DispatcherServlet
办事处,才能继续前行。
PS:接下来是讲述
Java-SpringMVC
框架的执行过程,非Java开发可忽略细节。
随着Thread-xxx
的步伐,我们找到了线程口中所说的DispatcherServlet
办事处,该办事处的工作人员首先看了一下我本次的具体目的地(资源地址),然后说:您需要先去问一下HandlerMapping
管理局,让它给你找一下具体负责这块业务的工作室。
紧接着线程
Thread-xxx
又带我来到了HandlerMapping
管理局找到了其中的管理人员,该管理人员让我先把要找的资源位置给它,然后只见它拿着我的目标地址作为条件,然后输入进了查询器,一瞬间便查出来了我本次的最终目的地:UserController
工作室!
线程Thread-xxx
道:这就是负责您本次任务的最终工作室啦!我这就带您过去。
这其实本质上就是
SpringMVC
中,请求定位具体Java
方法的逻辑,但由于之前没出过《SpringMVC
的原理篇》,因此接下来从专业性的角度简单叙述一下SpringMVC
的核心原理。
先上一张SpringMVC
的原理图:
观察如上流程图,其实看起来难免有些生涩,那此刻咱们换成简单一点的方式叙述,不再通过这种源码性的流程去理解。
不知诸位是否还记得,最开始学习SpringMVC
时的配置过程,接下来我们简单回忆一下:
①配置
springmvc-servlet.xml
文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 通过context:component-scan元素扫描指定包下的控制器-->
<!-- 扫描com.xxx.xxx及子子孙孙包下的控制器(扫描范围过大,耗时)-->
<context:component-scan base-package="com.xxx.xxx"/>
<!-- ViewResolver -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!-- viewClass需要在pom中引入两个包:standard.jar and jstl.jar -->
<property name="viewClass"
value="org.springframework.web.servlet.view.JstlView"></property>
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>
在这第一步中,最重要的就是配置一下扫描包的位置,以及配置一下视图解析器。
②配置
web.xml
:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<!-- Spring MVC servlet -->
<servlet>
<servlet-name>SpringMVC</servlet-name>
<!--配置一下DispatcherServlet的位置-->
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--指定springMVC的初始化文件位置,默认值为:/WEB-INF/springmvc-servlet.xml-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/springmvc-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<!--web.xml 3.0的新特性,是否支持异步-->
<!--<async-supported>true</async-supported>-->
</servlet>
<!--关键!!!配置一条请求路径映射,"/"代表匹配所有路径的请求-->
<!--也就是当有请求到来时,都会被进入前面servlet-name=SpringMVC的servlet中-->
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
在第二步中,主要会配置一条请求路径的映射位置,将进入WEB
程序的所有请求全部转入DispatcherServlet
的doGet、doPost
方法中。
同时由于web.xml
中配置了一个servlet:DispatcherServlet
,所以在程序启动时,首先会加载DispatcherServlet
,加载时会执行初始化操作,会调用initStrategies()
方法:
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
重点看其中的第四个初始化操作:调用initHandlerMappings()
方法,由于之前在web.xml
中指定了初始化文件的位置:/WEB-INF/springmvc-servlet.xml
,那么紧接着SpringMVC
会去读取该配置文件中的base-package
扫描包路径,然后会开始扫描这个包路径下的所有类:
- 首先会扫描出该包中带有
@Controller
注解的类。 - 然后会对扫描出的所有类进一步做扫描,会扫描到所有方法上存在
@RequestMapping
注解的方法。 - 最后会以两个注解上配置的值组合起来做为
Key
,然后通过反射机制,将方法变为一个Method
对象,封装成InvocationHandler
实例作为Value
,一起加入到一个大的Map
容器中。
上述流程下来大致诸位有些晕乎乎的,那么简单举个例子:
@Controller("/user")
public class UserController {
@RequestMapping("/get")
public String get(User user) {
......
}
@RequestMapping("/add")
public String add(User user) {
......
}
}
上述这个案例中,最终在初始化之后,会被以下面这种形式加入Map
容器:
// 这里是伪代码,主要是为了阐述逻辑
Map<String,InvocationHandler> map = new HashMap<>();
// 后面的UserController#get()就是以反射获取到的Method方法实例
map.put("/user/get",InvocationHandler(UserController#get()));
map.put("/user/add",InvocationHandler(UserController#add()));
最终当请求到来时,由于之前web.xml
中配置了一条/
匹配规则,所有的请求都会被转入到DispatcherServlet
的doGet、doPost
中,在该方法内首先会以HTTP
请求报文-请求行中的资源路径作为Key
,然后在这个Map
容器里面进行匹配,从而定位到具体的Java
方法并执行。
OK,最后在简单的把完整流程叙述一遍:
- 其实在咱们把一个
JavaWeb
程序打成war
包丢入Tomcat
并启动时,Tomcat
就会先去加载web.xml
文件。 - 在加载
web.xml
配置文件时,会碰到DispacherServlet
需要被加载。 - 当加载
DispacherServlet
时,其实就是把SpringMVC
的组件初始化,以及将所有Controller
中的URL
资源全部映射到容器中存储。 - 然后当请求进入
Tomcat
经过DispacherServlet
时,DispacherServlet
就去容器中找到这个请求的URL
资源。 - 找到请求的资源路径对应的
Java
方法后,会调用组件通过反射机制去执行具体的Controller
方法。 - 当执行完毕之后,又会回到
DispacherServlet
,此时DispacherServlet
又会去调用相关组件处理执行后的结果。 - 最后当结果处理完成后,才会将渲染后的结果响应回客户端。
OK~,话接前文,前面经过
HandlerMapping
管理局的管理人员查询后,我们已经找到了本次任务处理的具体工作室了...
- 线程:客官,咱们到了!
- 线程:这个工作室中已经写明了您本次任务如何处理的具体步骤,接下的事情都将由我为您效劳。
- 线程:您要随我一起去看看具体的处理过程嘛?
- 我:好呀,好呀,一起去!
随着线程的工作开始,我们一路走过了service
层、dao/mapper
层,在service
层办事时,我们遇到了强大的Redis
哥哥,Redis
哥哥看到我们之后,问清楚了我们本次到来的目的,然后它说:“来自远方的贵客,请稍等,让我先看看我这里有没有您需要的东西!”
这个场景似曾相识哎,我记得来的路上也有个
CDN
老爷爷跟我说过同样的话~
- Redis:来自远方的客人,很抱歉我这里没有您要的东西。
-
Redis:您本次的路途还需继续前行,您可以去找一下
MyBatis
哪小子,它也许能够帮到您。
根据Redis
的指示,线程Thread-xxx
领着我最终见到了MyBatis
,它长这个样子:
原来
Redis
哥哥口中的MyBatis
竟然是个鸟叔叔[吐舌~]
MyBatis
简单看了一下我本次的任务:
- 鸟叔:来自远方的贵客,这件事我确实可以帮到您,请稍等。
- 然后“鸟叔”一顿操作,竟鼓捣出了一个我看不懂的东西,然后递给了我。
-
鸟叔:这个叫做
SQL
代码,是你您次任务的必须之物。 -
鸟叔:你现在可以拿着它,让
Thread-xxx
去带您找一下JDBC
哪个老家伙。
慢慢的,线程又带我找到了“鸟叔”口中所说的JDBC
老爷爷,JDBC
老爷爷见到我的到来,眼神中并没有丝毫的意外之情,似乎早已经习以为然,只见JDBC
老爷爷抬起消瘦的右手,指着一个地址:
jdbc:mysql://xxx.xxx.xxx.xxx:3306/db_xxxxx
然后道:“小家伙,你又需要再跑一段远路咯,而且只能你去,Thread-xxx
只能在这里等你”。
我:好吧好吧,那我去啦!
又是孤身一人的旅途,难免有些孤独感袭来,但还好我早已习惯啦!随着一路奔波,我来到了JDBC
老爷爷给出的地址,这里同样是位于另外一台服务器的Linux
大陆上,我通过3306
这个编号找到了一座叫做MySQL
的城池,当我踏入之后发现,与之前踏上JVM
神州相同,在我刚踏入MySQL
这座大城的时候,有一个自称为DB
连接家族的弟子接待了我。
-
DB连接:您好呀,是
JVM
神州上那位JDBC
前辈介绍过来办事的,对吗? - 我:对对对,是的,是的。
-
DB连接:好的,那请把您手中的
SQL
给我噢。 - DB连接:那是您本次需要做的任务清单,麻烦交给我一下,由我帮你代劳。
- 我:昂,那给给你啦[递过去]~
- DB连接:好的,这边有冰阔乐和西瓜,请您稍等片刻,我去去便回。
这里不再展开叙述
SQL
的执行细节,因为MySQL
也是一门较庞大的内容,在开设的《全解MySQL数据库》专栏中,之后会出一篇:《一条SQL具体是如何执行的?》文章去详细阐述。
正当我吃完一块西瓜、喝完一瓶冰阔乐时,DB
连接家族的哪位弟子便回来了,同时怀里抱着一大堆东西(数据),然后丢给了我,道:“这便是您本次需要的数据啦,您本次的任务我都按照清单(SQL
)上的记录,给您一一处理了噢”。
我:好的,万分感谢,那我走啦!
顺着来时的原路,我飞速的赶回了JVM
神州所在的位置,然后映入眼帘的第一眼就是:Thread-xxx
哪个家伙在原地站着,老老实实的等候着我的回归,我悄悄的绕到了Thread-xxx
身后,然后从背后拍了一巴掌:
- 我:嘿,我回来啦!等了我这么久,有没有想我~
- 线程:并未,我是在履行线程家族该有的职责。
- 我:额....,无趣。
- 我:我事情已经办好了,我要走了噢。
- 线程:好的,那由我来送您。
一路跟随着Thread-xxx
的脚步,兜兜转转的我们最终又回到了DispatcherServlet
办事处,经过它们内部人员的一顿操作之后,我就打算返航啦!一路走走停停,我走到了JVM
神州的边缘。
- 线程:远方的客人,我只能送您到这里啦。
- 我:就要说再见了吗?
-
线程:是的,按照我们
Java
线程家族的规则,正常情况下我是不能踏出JVM
神州的。 -
我:好吧好吧,那就再见啦,
Thread-xxx
~,我会记得你的。 - 线程:好的,那祝您归途一路顺风,期待您的下次光临!再见啦!
- 我:拜拜[挥手]~
我告别了Thread-xxx
,也从此离开了JVM
神州,最终我从Tomcat
这只大猫咪的口中飞了出来,正式踏上了归途。
四、大功告成的我该返航咯 - 服务器响应
诸多经历过后,现在的我携带着本次任务的结果踏上了回家之路,首先我又路过了Gateway
叔叔那里,然后我又回到了Nginx
大叔所在的城池,不过Nginx
大叔把我的身体改为了应答报文结构,并且往其中还写入了一些东西,听说是让我回去交给浏览器老大的。
然而在我返航之前,似乎这边也有加密层、传输层、网络层、链路层、物理层这些关卡,和我当时出发的过程一样,我身上被一层一层的贴了很多东西,并且最终也被改为了0、1
组成的身体结构,这个过程是多么的熟悉呐!
我又踏上了哪不知有多遥远的路途,与来时的路一样,其中也遇到了很多中转站,也走过各种各样的道路,当然,为了防止我迷路,在
Nginx
大叔那里,也在我的脑袋上贴了一个TCP
头,里面写清楚了我来自那里,该去向何方.....
在迷迷糊糊中不断前行,终于看到了我的出生地,看到了网络进程和浏览器老大~,哦豁!我回来啦!
在进入家门之前,我又会经历物理层、链路层、网络层、传输层、TLS
层依次解封的过程,主要是为了将我从后端带回来的数据解析出来。网络进程在解析到数据后,我的使命就此完成啦!紧接着网络进程会将数据交给浏览器老大,然后老大会派遣一个小弟(渲染进程)对数据进行处理,我瞅了几眼,大体过程是这样的:
- 首先渲染小弟会根据
HTML、CSS
数据生成DOM
结构树和CSS
规则树。 - 然后结合结构树和规则树生成渲染树,再根据渲染树计算每一个节点的布局。
- 最后根据计算好的布局绘制页面,绘制完成后通知另一个小弟(呈现器)显示内容。
最后,因为我至此已经正常返航了,所以为了节省资源开销,会将我出发前构建的安全通道(TCP、TLS
连接)关闭,这个过程会由TCP
大哥去经过四次挥手完成,如下:
具体过程可参考:《计网基础与TCP/IP-TCP四次挥手》
五、网络之旅篇总结
综上所述,用户在浏览器地址栏输入内容后,我们站在一个“网络请求”的角度,切身感受了一场奇妙的网络之旅,从客户端发送请求到服务端返回响应,整个流程咱们都“亲身”体验了一回,最后写个流程总结:
- ①用户在地址栏输入内容,浏览器判断后生成相应的
URL
并传给网络进程。 - ②网络进程先查询本地缓存,没有则解析
URL
并向DNS
发送请求,得到IP
。 - ③网络进程先与目标服务器进行
TCP、TLS
多次握手,建立TCP、TLS
安全连接。 - ④紧接着组装请求报文,并由各个分层对数据进行封装,最终转为
0、1
格式。 - ⑤基于建立好的连接,利用物理介质传输数据,通过路由器控制数据的传输方向。
- ⑥请求会先去到
CDN
查询是否有缓存的内容,如果没有则继续向下请求。 - ⑦请求来到
LVS
后被转发到Nginx
,再由Nginx
转发到Gateway
网关。 - ⑧
Gateway
网关根据配置好的API
分发规则,将请求分发到具体服务。 - ⑨紧接着再从
Nacos
注册中心内,查询出该服务的具体服务实例IP
。 - ⑩请求来到具体的服务器后,先通过端口号找到具体的
WEB
服务进程Tomcat
。 - ⑪
Tomcat
基于SpringMVC
的工作流程为请求定位到具体的Java
后端方法。 - ⑫线程执行
Java
方法时,先去Redis
中查询是否有数据,没有则查询MySQL
。 - ⑬查询
DB
前先通过MyBatis
生成SQL
语句,然后再通过DB
连接执行SQL
。 - ⑭请求根据已配置的数据源地址,来到
MySQL
并执行SQL
语句,从而获得数据。 - ⑮经过报文组装、数据封装、请求转发等操作,向客户端响应数据(原路返回)。
- ⑯应答报文经物理介质传输后,最终抵达客户端网络进程(可能会将数据加入缓存)。
- ⑰网络进程将数据交给浏览器之后,根据情况准备做
TCP
四次挥手,断开连接。 - ⑱浏览器创建渲染子进程,然后根据数据生成渲染树,最后绘制并显示页面。
至此整个流程结束,当然,这个过程中并未涉及到太多的技术栈,也包括对于整个前/后端系统内部的执行细节并未阐述,这是由于整个系统的全细节执行流程较为庞大,展开叙述之后难以收尾,因而在本篇中则抓住核心点去叙说。
最后,对于请求执行的完整经历,也画成了一副流程图,但由于文件过大会失真,因而可点击链接在线访问:《浏览器输入URL后究竟发生了什么?》