网络编程之化身一个请求感受浏览器输入URL后奇妙的网络之旅!

引言

在浏览器上输入一个URL后发生了什么? 这也是面试中老生常谈的话题,包括网上也有大量关于这块的内容:

image.png

从百度的搜索结果来看,能够搜到七千多万条记录,因此本篇不会再以那种前篇一律的方式赘述,而是以目前较新的网络内容,结合系统中的大部分服务,将自己类比成一个请求,切身感受到每个技术栈的具体细节,彻底从“根儿上”理解客户端请求-服务端响应的全过程。

本篇以https://www.juejin.cn/为例进行分析,当然,这里假设掘金后端是Java做的(实际上掘金好像是基于Node做的后端)。

一、地址栏输入后本地会发生的事情

当我们在浏览器的地址栏中,输入xxx内容后,浏览器的进程首先会判断输入的内容:

  • 如果是普通的字符,那浏览器会使用默认的搜索引擎去对于输入的xxx生成URL
  • 如若输入的是网址,那浏览器会拼接协议名形成完整的URL

当然,在地址栏中输入某个内容后,也会进行一些额外操作,例如:安全检查、访问限制等,但总归而言,浏览器做的第一件工作则是生成URL,当按下回车后,浏览器进程会将生成的完整URL发送到网络进程:

image.png

当网络进程收到传过来的URL后,首先并不会直接发出网络请求,而是会先查询本地缓存:

image.png

观察上述流程,当网络进程收到传来的URL后,会首先通过URL作为Key,在本地缓存中进行查询:

  • ①如果本地中是否有缓存:
    • 没有:发起网络请求去服务器获取资源,成功后将结果渲染页面并写入缓存。
    • 有:继续判断本地中的缓存内容是否已经过期,没有则直接使用本地缓存。
  • ②如果本地中的缓存已经过期,则会携带If-Modified-Since、If-None-Match等标识向服务器发起请求,先判断服务器中的资源是否更新过:
    • 未更新:服务器返回304状态码,并继续读取之前的缓存内容使用。
  • ③如若服务器的资源更新过,那么也会向服务器发起请求获取资源。

如果在本地缓存中,无法命中缓存,或者本地缓存已过期并服务器资源更新过,那么此刻网络进程才会真正向目标网站发起网络请求。

二、一个全新的“我”诞生过程与前期的经历

当客户端的网络进程,在查询缓存无果后,会真正开始发送网络请求,但要牢记:客户端的网络进程并非直接向目标网站发起请求的,前期还需经过一些细节处理。

当然,为了能够更直观的感受整个过程,在这里我们将自己“化身”为一个请求,站在请求的角度切身体验一段奇特的“网络旅途”。

2.1、“我”诞生前的准备 - 解析URL

在网络进程发起请求之前,会首先对浏览器进程传过来的URL进行解析,一般来说完整的URL结构如下:

image.png

但上述结构使用较少,通常情况下,浏览器会使用的URL的常用结构如下:

image.png

URL中每个字段的释义如下:

  • scheme:表示使用的协议类型,例如http、https、ftp、chrome等。
  • ://:协议类型与后续描述符之间的分隔符。
  • domainName:网站域名,经DNS解析后会得到具体服务器IP
  • /path:请求路径,代表客户端请求的资源所在位置,不同层级目录之间用/区分。
  • ?query1=value:请求参数,?后面表示请求的参数,采用K-V键值对形式。
  • &query2=value:多个请求参数,不同的参数之间用&分割。
  • #fragment:表示所定位资源的一个锚点,浏览器可根据这个锚点跳转对应的资源位置。

网络进程会根据URL的结构对目标URL进行解析,其中有两个关键信息:

  • 首先会解析得到协议名,例如http、https,这关乎到后续默认使用的端口号。
  • 然后会解析得到域名,这个将关乎到后续具体请求的服务器地址。

假设浏览器传输过来的URLhttps://juejin.cn/user/862486453028888/posts,那么在这个阶段会确定后续请求的服务器端口号为443,请求的目标域名为www.juejin.cn。其实在这里主要是根据浏览器的输入信息,去解析出一些“诞生我(请求)”的前置要素。

2.2、“我”该去往的具体位置 - DNS域名解析

在上个阶段已经大概知道“我”该去往何处啦!但我具体地址该到那里呢?“我”好像不大清楚,要不找个人问问吧^_^。我记得好像有个叫做DNS的“大家族”是专门负责这个的!我要去找它们问问看~

image.png

不过在问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大叔后,获得了目的地址的我,此时已经知道该去往何处啦!但在正式出发前,由于前路坎坷,途中会存在各类危机(网络阻塞、网络延迟、第三方劫持等),因此为了我的安全出行,首先还需为我建立一条安全的通道,所以我还需要等一会儿才能出发,俺们一起来瞅瞅建立安全通道的过程是什么样的:

image.png

看着好复杂啊~,但似乎大体就分为了两个过程:

首先是TCP的三次握手过程,听说这个阶段是为了确保目的地能够正常接收我、也是为了给我建立出一条可靠的出行通道、并且为我计算一下出行失败之后多久重新出发的时间等目的(也就是为了测试双方是否能正常通信、建立可靠连接以及预测超时时间等)。

其实按照之前的“交通规则”,在建立好TCP连接之后,我就可以继续走下一步啦,但现在有很多坏人,在我们出行的道路上劫持我们,然后窃取、篡改俺们携带的数据,所以如今出行变得很不安全,因此还需要还需要建立一条安全的出行通道,就是TLS大叔的安全连接~(HTTP+TLS=HTTPS):

TLS握手阶段,在这个阶段中,TLS大叔为了俺的安全出行,会通过很多手段:非对称加密、对称加密、第三方授权等,先和俺的目的地交换一个密钥,然后再通过这个密钥对我加密一下,确保我被坏人抓到了也无法得到俺护送的数据^_^

详细且专业性的过程请参考之前的:《计网基础TCP/IP综述-TCP三次握手》《全解HTTP/HTTPS-SLL、TLS详解》

2.4、诞生“我的身体” - 构建请求报文

经历上述过程后,安全的出行道路已经建立好啦!但此刻的我还不算完整,所以需要先构建一个“身体”,也就是HTTP请求报文:

image.png

“我的身体”主要由请求行、请求头、空行以及请求主体四部分组成,里面包含了“我本次出远门的需要护送的数据和一些其他信息”。同时,为了我能够在“出行的道路上(传输介质)”安全且正常传输,我还需要经过层层封装:

image.png

首先为了确保俺护送的数据安全,TLS大叔会先对我的数据进行一次加密,把我原本携带的明文数据转变为看都看不懂的密文,类似下面这个样子:

image.png

经过加密后的我会紧接着来到传输层,传输层会在我的脑袋上再贴上一个传输头,如果是TCP大哥的话,它会给我贴上一个TCP头,但如果传输层的UDP大哥在的话,它给我贴的就是UDP头。但不管是谁贴的,在这个传输头内,为了防止我迷路和走丢,TCP、UDP两位大哥哥都会细心的在里面写清楚“我来自哪里,该去往何处”,也就是源地址和目的地址:

image.png

偷偷吐槽一句:TCP大哥贴的传输头里面,放了好多好多东西,让我感觉脑袋沉沉的。

过了传输层这一站之后,我又来到了网络层,果不其然,网络层里面最常见的还是IP大叔,IP大叔看到我之后,又在我的脑袋上贴上了一个网络头,也就是给我又加了一个IP头。

哒哒哒~,我出了网络层这关之后,又来到了数据链路层,这关则是由大名鼎鼎的“以太网家族”驻守,在这里我和之前两关不同,除开在我脑袋上贴了一个链路头之外,还给我在尾巴上多加了一个链路尾。

不过刚刚出链路层的时候,好像有个人跟我说:你这个样子是无法在介质上行走的,你要记得改变一下啊!

我还没听的太清楚,就来到了物理层这关,这层和之前我“家里”以及之前的关卡环境都不一样,物理层的小伙伴们好像都有实际的形态,但之前接触所有内容都是虚拟的概念形态哎~。

在我对比物理层大哥们的异样差距时,一不愣神发现我的身体好像发生了“翻天覆地”的变化,整个我似乎都变为了0、1构成了,正当纳闷时,物理层的某个大哥哥告诉我说:“只有变成这样子,你才可以在出行的道路上行走哈,所以我们给你转换了一下形态,你现在已经可以出发了”。

原来是这样呀,好像链路层的时候有人跟我说过哎~

同样对于更为专业、详细的过程可参考之前的:《HTTP/HTTPS-HTTP报文组成》《计网基础之TCP/IP-网络分层模型》等内容。

2.5、踏上路途的我 - 数据传输

GO~GO~GO~,终于出发啦!我终于踏上了网络之旅!呼呼呼~

image.png

咔!我来到了第一个中转站,听别人说,好像它的名字叫做路由器,首先路由器大哥把我的身体按照之前封装的步骤层层解封了,但解封到传输层的时候,看到了我脑袋上的传输头,似乎路由器大哥发现了TCP哥哥写的目的地址,发现我的目的地还在更远的位置,然后路由器大哥又按照原本的步骤把我的身体封装回去了,然后还亲切的给我指出了接下来该往那条路走,我又该继续前行啦....

我一边走着,一边在思考:好像路由器大哥就是负责给俺们指路的,防止俺们走丢~
具体可参考:《TCP/IP-IP寻址与路由控制》

三、“我”在后端服务器中多姿多彩的历程

啊!路途好遥远呀,我一路走了很久很久,也遇到了很多很多的中转站,每次当我不知道怎么走时,路由器大哥都会温馨的给我指出接下来该走的路途。期间我也走过很多很多路,曾踩着双绞铜线、同轴电缆、光纤前行,当然,可不要小看俺,就算没有物理连接的情况下,我也可以通过无线电技术,通过空气前行呢!

再次声明,文中所谓的道路,就是指数据传输的介质。

3.1、东跑西颠的经历 - 接入层转发

走着走着,突然前方遇到一个叫做CDN的老爷爷,它问我说要去哪里,我说要去xx地方办事,和蔼的CDN老爷爷跟我说,我来看看我这里有没有你要的东西,如果有的话,就不用麻烦你这个小家伙一直跑下去了。可是很遗憾,CDN老爷爷说它哪儿没有我要的东西,因此我只能继续前行下去。

记不清过了多久,一路跌跌撞撞,在迷迷糊糊中我来到了一个地方,但当我还在分辨时,刷的一下,很快啊,我就被丢到了其他地方,当我回头看的时候,发现刚刚哪个地方,大写着LVS

LVS一般会作为大型网站的网关接入层,负责提供更高的并发性能,具体可参考《亿级流量架构设计-LVS篇》

再直视前方,前方有一个东西很眼熟,难道这就是当初听说过的服务器吗?带着一脸疑惑的我慢慢走了进去,我发现内部空间很大,上面漂浮着一块大陆,名为Linux大陆,上面有好多好多的“城市(进程)”林立着,那我该去哪一座呢?让我想想!

对了,记起来了好像!!当时出门的时候有人跟我说过:如果你到了目的地之后,不知道该找谁,那么可以根据默认的编号(端口号)去找!

让我回想一下,HTTP的默认端口是80HTTPS的默认端口是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:不可以噢!按照规则的话,你就应该去我给你的地址哈。
  • :好吧,那我去啦!

这里的规则是什么呢?其实就是Nginxlocation路由匹配规则、upstream代理集群列表以及负载均衡算法,具体可参考:《Nginx篇:反向代理与负载均衡》《负载均衡算法原理篇》

顺着Nginx大叔给的地址,我又来到了另外一台服务器,上面同样有一块Linux大陆,然后根据地址在上面找到了一个名为Gateway的东东,听它自己介绍,好像属于系统网关。但当我找它办事时,它却跟我说:“我不负责具体的业务处理,根据你的目标/user/....,你应该去找Nacos注册局,问它们要一下USER-SERVICE的具体地址,所以,小家伙你还得继续奔波哦”!

好的好的,感谢Gateway叔叔指路,那我现在就去啦!

哒哒哒~,迈着愉快的步伐我来到了Nacos注册局,然后将Gateway叔叔给我的名字:USER-SERVICE交给了它们的工作人员,它们的工作人员经过一番查询之后告诉我,这个“品牌”多有个分部,你可以去其中任意一处分部处理你的任务,你可以去:xxx.xxx.xxx.xxx:8080这个地址噢!

这里的“品牌”是指后端的具体服务,分部是指服务集群中的每个节点。

好的好的,那我就去你说的这个xxx.xxx.xxx.xxx:8080地址啦!

我一边在路上走着,一边想了一下刚刚过程发生的事情,然后把这个经历画成了一副逻辑图,如下:

image.png

回去的时候我一定要跟小伙伴们分享一下这个有趣的经历,耶!

3.2、我遇到了一只大猫咪-叫作Tomcat

根据Nacos给我的地址,我又来到了一台新的服务器面前,我记得Nacos给了我一个端口号,要我来到这里之后找编号为8080的位置,我顺着这个编号慢慢找着,突然在我的前方,出现了一只大老虎,哦不,应该是一只大猫咪,它长这个样子:

image.png

它的长相似乎有些报看,但在它的脑门上正好写着我要找到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的原理图:

image.png

观察如上流程图,其实看起来难免有些生涩,那此刻咱们换成简单一点的方式叙述,不再通过这种源码性的流程去理解。

不知诸位是否还记得,最开始学习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程序的所有请求全部转入DispatcherServletdoGet、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中配置了一条/匹配规则,所有的请求都会被转入到DispatcherServletdoGet、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,它长这个样子:

image.png

原来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层依次解封的过程,主要是为了将我从后端带回来的数据解析出来。网络进程在解析到数据后,我的使命就此完成啦!紧接着网络进程会将数据交给浏览器老大,然后老大会派遣一个小弟(渲染进程)对数据进行处理,我瞅了几眼,大体过程是这样的:

image.png

  • 首先渲染小弟会根据HTML、CSS数据生成DOM结构树和CSS规则树。
  • 然后结合结构树和规则树生成渲染树,再根据渲染树计算每一个节点的布局。
  • 最后根据计算好的布局绘制页面,绘制完成后通知另一个小弟(呈现器)显示内容。

最后,因为我至此已经正常返航了,所以为了节省资源开销,会将我出发前构建的安全通道(TCP、TLS连接)关闭,这个过程会由TCP大哥去经过四次挥手完成,如下:

image.png

具体过程可参考:《计网基础与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后究竟发生了什么?》

鲸之声为您拼命加载中...