完整的一次 HTTP 请求响应过程(一)

因特网无疑是人类有史以来最伟大的设计,它互联了全球数亿台计算机、通信设备,即使位于地球两端的用户也可在顷刻间完成通信。java

能够说『协议』是支撑这么一个庞大而复杂的系统有条不紊运做的核心,而所谓『协议』就是通信双方所必须遵照的规则,在这种规则下,不一样的数据报可能被解析为不一样的响应动做。git

简而言之,『协议』就是指若是发送和接收方按照这个规则进行数据报文的发送,便可在基本的数据传输之上获得某些特殊的功能或服务,不然你的数据别人是不认识的。例如:遵循 TCP 协议的两端,能够在不可靠的网络传输中获得可靠的数据传输能力。github

整个计算机网络是分层的,有七层模型,也有五层模型,我的以为五层模型更利于理解。咱们从上至下的介绍这五个层,它们分别是,应用层,运输层,网络层,数据链路层和物理层算法

应用层

『应用层』算是距离用户最近的一层了,主机上的一个个的进程就构成了『应用层』。好比你在你的浏览器地址栏输入了 「www.baidu.com」,你的浏览器在应用层会作哪些事情呢?浏览器

首先浏览器会使用 DNS 协议返回域名「www.baidu.com」所对应的 IP 地址,关于 DNS 咱们待会详细介绍。缓存

接着,应用层决定建立一个『TCP 套接字』,而后将这个请求动做封装成一个 Http 数据报并推入套接字中。服务器

套接字分为两种类型,『TCP 套接字』和『UDP 套接字』,应用层同时可能会有几十个数据报的发出,而运输层也会收到全部的响应报文,那么它该如何区分这些报文究竟是谁的响应报文呢?微信

而套接字就是用于区分各个应用层应用的,每每由端口号和 IP 地址进行标识,运输层只要查看响应报文的源端口号和 IP 地址就可以知道该将报文推送给哪一个套接字了。markdown

当一个应用层数据报被推进进套接字以后,应用层的全部工做也算是所有完成了,关于后续报文的去向,它已经不用管了。网络

这里还要说明一点的是,『TCP 套接字』和『UDP 套接字』二者本质上的区别在于,前者保证数据报可靠地到达目的地,可是必然耗时,然后者不保证数据报必定能到达目的地,可是速度快,这也是应用层协议在选择运输层协议的时候须要考虑的一点。

关于 TCP 和 UDP,咱们后续还会继续说,下面咱们看看域名解析协议 DNS 是如何运做的,它是如何将一个域名解析返回它的 IP 地址的。

DNS 原理

首先明确一点的是,DNS 是一个应用层协议,而且它选择的运输层协议是 UDP,因此你的域名解析过程通常会很快,但也会常常出现解析失败的状况,然而刷新一下又好了。

image

在 DNS 服务器上,域名和它所对应的 IP 地址存储为一条记录,而全部的记录都不可能只存储在一台服务器上,我相信不管多么强大的服务器都扛不住全球上亿次的并发量吧。

大体来讲,有三种类型的 DNS 服务器,根 DNS 服务器,顶级域 DNS 服务器和权威 DNS 服务器。

其中,顶级域 DNS 服务器主要负责诸如 com、org、net、edu、gov 等顶级域名。

根 DNS 服务器存储了全部顶级域 DNS 服务器的 IP 地址,也就是说你能够经过根服务器找到顶级域服务器。例如:「www.baidu.com」,根服务器会返回全部维护 com 这个顶级域服务器的 IP 地址。

而后你任意选择其中一个顶级域服务器,请求该顶级域服务器,该顶级域服务器拿到域名后应当可以作出判断并给出负责当前域的权威服务器地址,以百度为例的话,顶级域服务器将返回全部负责 baidu 这个域的权威服务器地址。

因而你能够任意选择其中一个权威服务器地址,向它继续查询 「www.baidu.com」 的具体 IP 地址,最终权威服务器会返回给你具体的 IP 地址。

至此,咱们简单描述了一个域名解析的大体过程,还有一些细节之处并未说起,咱们等会会经过一个实例来完整的看一下,下面描述一个很是重要的概念。

整个 DNS 解析过程当中,有一个很是核心的人物咱们一直没介绍它,它就像主机的『助理』同样,帮助主机查询域名的 IP 地址。它叫作『本地 DNS 服务器』。

image

你们每次经过 DHCP 动态获取 IP 地址的时候,这一点后文会说。其实路由器不只给你返回了 IP 地址,还会告诉你一个 DNS 服务器地址,这个就是你的本地 DNS 服务器地址,也就是说,你的全部域名解析请求只要告诉它就好了,它会帮你查并返回结果给你的。

除此以外,本地 DNS 服务器每每是具备缓存功能的,一般两天内的记录都会被缓存,因此大部分时候你是感受不到域名解析过程的,由于每每就是从缓存里拿的,很是快。

下面咱们看一个简单的案例:

网上找的一个图,本身画实在太费时间了,但足以说明问题,如今假设请求 「www.xx.com」 。

image

  • ①:主机向负责本身的本地 DNS 发送查询报文,若是本地服务器缓存中有,将直接返回结果
  • ②:本地服务器发现缓存中没有,因而从内置在内部的根服务器列表中选一个发送查询报文
  • ③:根服务器解析一下后缀名,告诉本地服务器负责 .com 的全部顶级服务器列表
  • ④:本地服务器选择一个顶级域服务器继续查询,.com 域服务器拿到域名后继续解析,返回负责 .xx 域的全部权威服务器列表
  • ⑥:本地服务器从返回的权威服务器之一再次发送查询报文,最终会从某一个权威服务器上获得具体的 IP 地址
  • ⑧:向主机返回结果

其实整个 DNS 报文的发送与响应过程都是要走咱们的五层协议的,只是这里重点在于理解 DNS 协议自己,因此并未说起其余层的具体细节,这里的强调是提醒你 DNS 只是一个应用层协议。

运输层

运输层的任务就是将应用层推出套接字的全部数据报收集起来,而且按照应用层指定的运输层协议,TCP 或 UDP,从新封装应用层数据报,并推给网络层等待发送。

TCP 和 UDP 是运输层的两个协议,前者是基于链接的可靠传输协议,后者是无链接的不可靠传输协议,因此前者更适合于一些对数据完整性要求高的场合,后者则适合于那种能够容许数据丢失但对传输速率要求特别高的场景,例如:语音电话,视频等,丢一两个包最多卡顿一下,无伤大雅。

UDP

UDP 不一样于 TCP 那样复杂,它既不保证数据可靠的传输到目的地,也不保证数据按序到达目的地,仅仅提供了简单的差错检验。报文格式以下:

image

其中,数据就是应用层推出来的数据,源端口号用于响应报文的交付,目的端口号用于向目的进程交付数据,校验和用于检查传输过程当中数据是否受损,若是受损,UDP 将直接丢弃该报文。

TCP

TCP 要稍微复杂些,它是面向链接的,而且基于链接提供了可靠的数据传输服务,它的数据报文格式以下:

image

单纯的解释报文格式中各个字段的含义并无太过实际的意义,你也很难理解了,在咱们介绍 TCP 是如何『三次握手』,『四次挥手』以及『丢包重传』等动做时,不间断的会说明这些动做时如何使用报文中的相关字段的。

首先咱们来看耳熟能详的『三次握手』,这基本上是 TCP 的代名词了,不管懂不懂具体原理的人,提到 TCP,基本上都是知道『三次握手』的。

而自己,TCP 的三次握手就是为了确保通信双方可以稳定的创建链接并完成数据报文的请求与响应动做,至于为何是三次握手而不是四次五次,这是一个哲学问题,这里就不作讨论了。

第一步:

客户端向服务端发送一份特殊的 TCP 报文,该报文并不包含应用层的数据,是一份特殊的报文,它的 TCP 首部中 SYN 字段值为 1 (参见上述报文格式)。

除此以外,客户端还会随机生成一个初始序号,填在报文的「序号」字段,表明当前报文的序号是这个,而且我后续的分组会基于这个序号递增。

而后该报文将会经网络层、链路层、物理层发送到服务端。

第二步:

若是分组丢失了,那么客户端会通过某个时间间隔再次尝试发送。

而若是分组准确的到达服务端了,服务端拆开 TCP 首部会看到,这是一个特殊的 SYN 握手报文,因而为这次链接分配缓存等资源。

接着服务端开始构建响应报文,SYN 是一个用于同步须要的字段,响应报文中依然会被置为 1,而且服务端也将随机生成一个初始序号放置的响应报文的序号字段中。

最后,服务端还会为响应报文中的确认字段赋值,这个值就是客户端发过来的那个序号值加一。

总体上的意思就是说,「我赞成你的链接请求,个人初始序号为 xxx,你的初始序号我收到了,我等着你的下一个分组到来」

第三步:

客户端收到服务端的响应报文,因而分配客户端 TCP 链接所必须的缓存等资源,因而链接已经创建。

实际上从第三步开始,客户端就能够携带应用层数据向服务端交换报文了,之后的每份报文中,SYN 都为 0,由于它只是用于同步初始序号的,这一点须要明确。

总的来讲,整个『握手』过程大体以下图所示:

image

下面咱们看看拆除一条 TCP 链接的『四次挥手』是怎样的过程。

由于一条 TCP 链接会消耗大量的主机资源,不只仅服务端须要分配各类缓存资源,客户端也一样须要分配相应资源。由于 TCP 是『全双工通讯』,服务端和客户端两方实际上是同样的,谁是客户谁是服务器是相对的。

强调这一点是为了说明,一条 TCP 链接不是只有客户端才能断开,服务端也一样能够主动断开链接,这一点须要清楚。

咱们这里假设客户端主动发起断开链接的请求为例:

第一步:

客户端构建一份特殊的 TCP 报文,该报文首部字段 FIN 被置为 1,而后发送该报文。

第二步:

服务端收到该特殊的 FIN 报文,因而响应客户端一个 ACK 报文,告诉客户端,请求关闭的报文已经收到,我正在处理。

第三步:

服务端发送一个 FIN 报文,告诉客户端,我将要关闭链接了。

第四步:

客户端返回一个 ACK 响应报文,告诉服务端,我收到你刚才发的报文了,我已经确认,你能够关闭链接了。

当服务端收到客户端发送的 ACK 响应报文时,将释放服务端用于该 TCP 链接的全部资源,与此同时,客户端也会定时等待必定时间后彻底释放本身用于该链接的相关资源。

用一张图更直观的描述一下:

image

结合着图与相关序号信息,咱们再详细说说其中的一些细节。

首先,客户端发送一个特殊分组,该分组的序号为 u。发送完成以后,客户端进入 FIN-WAIT-1 这个状态,这个状态下,该 TCP 链接的客户端再也不能发送数据报,可是是能够接受数据报的,它等待着服务端的响应报文。

接着,服务端收到客户端发送的终止链接报文请求,服务端构建响应报文,告诉客户端「序号 u+1 之前的分组我都收到了」,而且进入 CLOSE-WAIT 状态,这个状态持续时间很短。

服务端会紧接着发送它的 FIN 数据报,通知客户端我服务端即将关闭链接,并随即进入 LAST_ACK 状态等待客户端响应报文。

一旦客户端收到这个 FIN 报文,将返回确认报文并进入 TIME-WAIT 状态,等待 2MSL 时间间隔后彻底释放客户端 TCP 链接所占用资源。

与此同时,当服务端收到客户端最后的确认报文,就将直接断开服务端链接并释放相关资源。

至于为何最后客户端须要等 2MSL 时间长度再彻底释放 TCP 相关资源呢?

那是由于 2MSL 是一份报文存在于网络中最长的时间,超过该时间到达的报文都将被丢弃,而若是客户端最后的确认报文于网络中丢失的话,服务端必将发起超时请求,从新发送第三次挥手动做,此时等待中的客户端就可随即从新发送一份确认请求。

这是为何客户端等待一个最长报文传输时间的缘由。有人可能好奇为何前面的各次请求都没有作超时等待而只最后一次数据发送作了超时等待?

其实缘由很简单,相信你也能想到,就是 TCP 自带计时能力,超过必定时间没有收到某个报文的确认报文,会自动从新发送,而这里若是不作等待而直接关闭链接,那么我如何知道服务端到底收到没个人确认报文呢。

经过等待一个最长周期,若是这个周期内没有收到服务端的报文请求,那么咱们的确认报文必然是到达了服务端了的,不然重复发送一次便可。

至此,TCP 的『三次握手』和『四次挥手』咱们已经简单描述完成了,下面咱们看看 TCP 的一些其余特性,好比:可靠传输,拥塞控制等

首先咱们来看 TCP 是如何实现可靠传输的,即如何解决网络传输中丢包的问题。

TCP 使用『回退 N 步』协议实现的可靠传输,准确来讲,TCP 是在它的基础上进行了一部分优化。

image

『回退 N 步』协议也被称做『滑动窗口』协议,即最多容许发送方有 N 个「已发送但未被确认」的数据报文,如图所示,p1 到 p3 长度即为 N,这里的窗口指的就是 p1 到 p3 这个区间。

只有当发送端收到 p1 的确认报文后,整个窗口才能向前滑动,而实际上在没有收到 p1 的确认报文前,即使它后面的报文已经被接收,服务端也仅仅会缓存这些『非预期的报文』

直到服务端收到最小预期的那个报文后,从缓存中取出已经到达的后续报文,合并并向上交付,而后向发送端返回一个确认报文。

当发送端窗口从左往右已经连续多个报文被确认后,整个窗口将向前滑动多个单位长度。

下面咱们看一个例子:

image

这是一个发送方的窗口,灰色表示已经被确认的报文,黄色表示已发送但未被确认的报文,绿色表示下一个待发送的报文,白色表示不可用的报文。

这是咱们假设服务端已经收到 六、7 两份报文,可是它上一次向上交付给应用层的是 4 号报文,也就是说它在等 5 号报文,因此它暂时会将 六、7 两个报文缓存起来,等到 5 号报文来了一并交付给应用层。

如今 5 号报文因为超时被重传了,终于到达目的地了,如愿以偿,服务端向上交付 五、六、7 三份报文,并返回一份确认报文,ACK = 8,表示序号 8 之前的全部报文都收到了

当发送端收到这份确认报文后,五、六、7 变成灰色,窗口向前移动三个单位长度。

此外,我还想强调一个细节,TCP 是没有否认确认的,因此若是服务端连续响应的多份报文是对同一序号的确认,那颇有可能该序号之后的某个报文丢失。

例如:若是服务端发送多个对分组 5 的 ACK 确认,那说明什么?说明目前我服务端完整的向上交付的序号是 5 号,后续的报文我没收到,你最好从新发一下别等待超时了。

这也是『快速重传』的核心原理。

那么 TCP 的可靠传输咱们也基本介绍完了,下面咱们看看若是网络拥塞的时候,TCP 是如何控制发送流量的呢?

TCP 认为:丢包即拥塞,须要下降发送效率,而每一次收到确认数据报即认为网络通畅,会增长发送效率。

TCP 的拥塞控制算法包含三个部分,慢启动、拥塞避免和快速恢复

慢启动的思想是,刚开始缓慢的发送,好比某个时间段内只发送一次数据报,当收到确认报文后,下一次一样的时间间隔内,将发送两倍速率的两份数据报,并以此类推。

因此,短期内,一个 TCP 链接的发送方将以指数级增加,但一旦出现丢包,即收到冗余的 ACK 确认,或者对于一个包的确认 ACK 始终没收到而不得不启动一次超时重传,那么发送方认为「网络是拥塞的」。

因而将速率直接调成一,即一个往返时间段,只发送一个分组,而且设置一个变量 ssthresh 表述一个阈值的概念,这个值是上次丢包时发送方发送速率的一半。

以后的发送方的发送效率同样会以指数级增加,可是不一样于第一次,此次一旦达到这个阈值,TCP 将进入『拥塞避免』模式,该模式下的发送效率将再也不指数级增加,会谨慎的增加。

拥塞避免的思想是,每一个往返时间段发送的全部数据报所有获得确认后,下一次就增长一个分组的发送,这样缓慢的增加效率是谨慎的。

那么一旦出现发送端超时丢包,注意这里是超时,将发送速率置为一并从新进入慢启动状态,阈值就是当前发送效率的一半。

而若是是服务端返回多个冗余 ACK 以明确你丢包,TCP 认为这不是严重的,对于这种状况,TCP 减半当前发送效率并进入快速恢复阶段。

快速恢复的基本思想是,收到几个冗余的 ACK 就增长几个分组的发送效率,就是说,你服务端不是没收到个人几个报文吗,这两次发送我提高速率迅速发给你。

当这期间出现了由发送端超时致使的丢包,一样的处理方式,初始化发送速率为一并减半当前发送效率做为阈值,进入慢启动阶段。

固然,若是这期间收到了对丢失报文的确认,那么将适当下降发送效率并进入拥塞避免状态。

这样,整个 TCP 最核心的几个思想都已经介绍完了,整个运输层基本上也算明了了。关于运输层,你应当有了必定的理解,我再总结一下。

运输层的任务就是从应用层的各个进程的套接字那取回来全部须要发送的数据,而后选择 TCP 或者 UDP 将数据封装并推给下面的网络层待发送。

未完,待续。。。


文章中的全部代码、图片、文件都云存储在个人 GitHub 上:

(https://github.com/SingleYam/overview_java)

欢迎关注微信公众号:扑在代码上的高尔基,全部文章都将同步在公众号上。

image
相关文章
相关标签/搜索