乐趣区

从输入URL到渲染页面-网络协议

本文是从输入 URL 到渲染页面专栏的第二篇文章 – 网络协议
我们知道 TCP/IP 协议将网络协议分了四层

我们重点说下应用层,网络层和传输层

数据在网络上是如何传输的?

网络层 IP

数据想在互联网上进行传输,就要符合网际协议(Internet Protocol,简称 IP)标准。互联网上不同的在线设备都有唯一的地址标识,用一个数字来表示。
类比我们平常网购,用我们的收件地址类比设备的唯一标识,我们知道了这个收件地址,就可以往这个地址发送包裹。计算机的地址就称为 IP 地址,访问任何网站实际上只是你的计算机向另外一台计算机请求信息。
如果要想把一个数据包从主机 A 发送给主机 B,那么在传输之前,数据包上会被附加上主机 B 的 IP 地址信息,这样在传输过程中才能正确寻址。另外,数据包上还会附加上主机 A 的 IP 地址,有了这些信息主机 B 才可以回复信息给主机 A。这些附加的信息会被装进一个叫 IP 头的数据结构里。

下面我们一起来看下一个易于理解的数据包从主机 A 到主机 B 的简化传输过程(不是 4 层网络协议):

  • 上层将数据包交给网络层;
  • 网络层再将 IP 头附加到数据包上,组成新的 IP 数据包,并交给底层;
  • 底层通过物理网络将数据包传输给主机 B;
  • 数据包被传输到主机 B 的网络层,在这里主机 B 拆开数据包的 IP 头信息,并将拆开来的数据部分交给上层;
  • 最终,含有信息的数据包就到达了主机 B 的上层了。

传输层 UDP/TCP

上面我们讨论的基于 IP 传输是非常底层的协议,只负责把数据包传送到对方电脑,但是对方电脑并不知道把数据包交给谁。因此,需要基于 IP 之上开发能和应用打交道的协议,也就是传输层,最常见的就是 UDP 和 TCP 协议。

增加了传输层,我们就可以把前面的三层结构扩充为四层结构,如下图所示


下面我们再一起来看增加了传输层的数据传输路线:

  • 上层将数据包交给传输层;
  • 传输层会在数据包前面附加上 UDP/TCP 头,组成新的数据包,再将新的数据包交给网络层;
  • 网络层再将 IP 头附加到数据包上,组成新的 IP 数据包,并交给底层;
  • 数据包被传输到主机 B 的网络层,在这里主机 B 拆开 IP 头信息,并将拆开来的数据部分交给传输层;
  • 在传输层,数据包中的 UDP/TCP 头会被拆开,并根据 UDP/TCP 中所提供的端口号,把数据部分交给上层的应用程序;
  • 最终,数据包就到了主机 B 上层应用程序这里。

那么在传输层使用 UDP 和 TCP 传输有什么区别呢,他们分别适合什么场景呢,下面我们来看一下

UDP 和 TCP 的对比

在使用 UDP 发送数据时,有各种因素会导致数据包出错,虽然 UDP 可以校验数据是否正确,但是对于错误的数据包,UDP 并不提供重发机制,只是丢弃当前的包,而且 UDP 在发送之后也无法知道是否能达到目的地。虽说 UDP 不能保证数据可靠性,但是传输速度却非常快,所以 UDP 会应用在一些关注速度、但不那么严格要求数据完整性的领域,如在线视频、互动游戏等。

UDP 缺点:

  • 数据包在传输过程中容易丢失,且没有重发机制;
  • 大文件会被拆分成很多小的数据包来传输,这些小的数据包会经过不同的路由,并在不同的时间到达接收端,而 UDP 协议并不知道如何组装这些数据包,从而不能把这些数据包还原成完整的文件。

针对 UDP 的缺点,TCP 头除了包含了目标端口和本机端口号外,还提供了用于排序的序列号,以便接收端通过序号来重排数据包。
另外,传输可以保证数据的可靠性,并且提供了重发机制。
那么 TCP 是怎么做到的呢,这就不得不提大名鼎鼎的“三次握手”和“四次挥手”了。

下面我们看看一次完成的 TCP 传输过程是怎样的:

  • 首先,建立连接阶段(三次握手)。TCP 提供面向连接的通信传输。面向连接是指在数据通信开始之前先做好两端之间的准备工作。所谓三次握手,是指在建立一个 TCP 连接时,客户端和服务器总共要发送三个数据包以确认连接的建立。
  • 其次,传输数据阶段。在该阶段,接收端需要对每个数据包进行确认操作,也就是接收端在接收到数据包之后,需要发送确认数据包给发送端。所以当发送端发送了一个数据包之后,在规定时间内没有接收到接收端反馈的确认消息,则判断为数据包丢失,并触发发送端的重发机制。同样,一个大的文件在传输过程中会被拆分成很多小的数据包,这些数据包到达接收端后,接收端会按照 TCP 头中的序号为其排序,从而保证组成完整的数据。
  • 最后,断开连接阶段(四次挥手)。数据传输完毕之后,就要终止连接了。

下面是网上很经典的三次握手和四次挥手的示意图

  • client 发起连接,发送一个 SYN 包表示建立连接,还有一个 SEQ = X(随机数)
  • server 收到后,对 client 应答,并发送连接请求。发送一个既包含 syn 又包含 ack 的包,此时 ack=X+1,SEQ = Y(随机数)。
  • client 接到 server 的应答后,看到 ack = X+ 1 就知道 server 端已经接受了我之前的请求。对 server 的连接请求做应答,此时 ack=Y+1

  • 第一次 client 发出一个 FIN 包和一个 seq=x, 之后 client 进入 FIN_WAIT_1 阶段
  • server 接收到之后回复一个 ack=x+1(原理同上),和 seq=y,表示收到了 client 的关闭请求, 之后进入 CLOSE_WAIT 状态,client 进入 FIN_WAIT_2 状态
  • 之后 server 处理完自己其他的 package 之后发送一个 ack=x+1,和 seq=y,此时 server 进入 LAST_ACK 状态,不再回复消息
  • client 接收到 server 的 FIN 包后回复一个 ack=y+ 1 之后进入 TIME_WAIT 状态,server 接收到这个包之后直接进入 CLOSED 状态,client 等待了两个 msl(Maximum Segment Lifetime 最大报文生存时间)之后没有收到应答,代表 server 正常关闭,便也进入 CLOSED 状态, 关闭连接

到这里你应该就明白了,TCP 为了保证数据传输的可靠性,牺牲了数据包的传输速度

HTTP 协议

接下来我们看下应用层 HTTP 协议的发展历史

HTTP1 时代

HTTP/0.9

首先我们来看看诞生最早的 HTTP/0.9。他的出现主要用于学术交流,需求很简单——用来在网络之间传递 HTML 超文本的内容,所以被称为超文本传输协议。整体来看,它的实现也很简单,采用了基于请求响应的模式,从客户端发出请求,服务器返回数据。

  • 因为 HTTP 都是基于 TCP 协议的,所以客户端先要根据 IP 地址、端口和服务器建立 TCP 连接,而建立连接的过程就是 TCP 协议三次握手的过程。
  • 建立好连接之后,会发送一个 GET 请求行的信息,如 GET /index.html 用来获取 index.html。
  • 服务器接收请求信息之后,读取对应的 HTML 文件,并将数据以 ASCII 字符流返回给客户端。
  • HTML 文档传输完成后,断开连接。

总的来说,当时的需求很简单,就是用来传输体积很小的 HTML 文件,所以 HTTP/0.9 的实现有以下三个特点。

  • 第一个是只有一个请求行,并没有 HTTP 请求头和请求体,因为只需要一个请求行就可以完整表达客户端的需求了。
  • 第二个是服务器也没有返回头信息,这是因为服务器端并不需要告诉客户端太多信息,只需要返回数据就可以了。
  • 第三个是返回的文件内容是以 ASCII 字符流来传输的,因为都是 HTML 格式的文件,所以使用 ASCII 字节码来传输是最合适的。

HTTP/1.0

随着互联网的发展,只能传输 html 已经不能满足需求了。还包括了 JavaScript、CSS、图片、音频、视频等不同类型的文件。因此支持多种类型的文件下载是 HTTP/1.0 的一个核心诉求,而且文件格式不仅仅局限于 ASCII 编码,还有很多其他类型编码的文件。

为了让客户端和服务器可以更灵活的交流,HTTP/1.0 引入了请求头和响应头,它们都是以为 Key-Value 形式保存的,在 HTTP 发送请求时,会带上请求头信息,服务器返回数据时,会先返回响应头信息。例如下面的代码就是请求头和响应头信息的部分信息:

accept: text/html  // 期望服务器返回 html 类型的文件
accept-encoding: gzip, deflate, br // 期望服务器可以采用 gzip、deflate 或者 br 其中的一种压缩方式
accept-Charset: ISO-8859-1,utf-8 // 表示期望返回的文件编码是 UTF-8 或者 ISO-8859-1
accept-language: zh-CN,zh // 期望页面的优先语言是中文
content-encoding: br // 表示服务器采用了 br 的压缩方法
content-type: text/html; charset=UTF-8 // 表示服务器返回的是 html 文件,并且该文件的编码类型是 UTF-8

这就是浏览器和服务器在 1.0 时代的一个交流方式,就好像两个人在对“暗号”一样。

HTTP/1.0 除了对多文件提供良好的支持外,还引入了很多其他的特性,这些特性都是通过请求头和响应头来实现的。
下面我们来看看新增的几个典型的特性:

  • 有的请求服务器可能无法处理,或者处理出错,这时候就需要告诉浏览器服务器最终处理该请求的情况,这就引入了状态码。状态码是通过响应行的方式来通知浏览器的。
  • 为了减轻服务器的压力,提供了 Cache 机制,用来缓存已经下载过的数据。
  • 服务器需要统计客户端的基础信息,比如 Windows 和 macOS 的用户数量分别是多少,所以请求头中还加入了用户代理的字段。

HTTP/1.1

虽然 1.0 已经可以应付绝大部分的场景,但是他还是有以下几个缺陷:

  • 每进行一次 HTTP 通信,都需要经历建立 TCP 连接、传输 HTTP 数据和断开 TCP 连接三个阶段 —— 增加了持久连接的方法(Connection: keep-alive),它的特点是在一个 TCP 连接上可以传输多个 HTTP 请求,只要浏览器或者服务器没有明确断开连接,那么该 TCP 连接会一直保持。(目前浏览器中对于同一个域名,默认允许同时建立 6 个 TCP 持久连接)
  • 队头阻塞问题 —— 没有解决
  • 每个域名绑定了一个唯一的 IP 地址,因此一个服务器只能支持一个域名。但是随着虚拟主机技术的发展,需要实现在一台物理主机上绑定多个虚拟主机,每个虚拟主机都有自己的单独的域名,这些单独的域名都公用同一个 IP 地址 —— 请求头中增加了 Host 字段,用来表示当前的域名地址,这样服务器就可以根据不同的 Host 值做不同的处理。
  • 需要在响应头中设置完整的数据大小,如 Content-Length: 901,这样浏览器就可以根据设置的数据大小来接收数据。不过随着服务器端的技术发展,很多页面的内容都是动态生成的,因此在传输数据之前并不知道最终的数据大小,这就导致了浏览器不知道何时会接收完所有的文件数据 —— 通过引入 Chunk transfer 机制来解决这个问题,服务器会将数据分割成若干个任意大小的数据块,每个数据块发送时会附上上个数据块的长度,最后使用一个零长度的块作为发送数据完成的标志。这样就提供了对动态内容的支持。

HTTP2

虽然 HTTP/1.1 采取了很多优化资源加载速度的策略,也取得了一定的效果,但是 HTTP/1.1 对带宽的利用率却并不理想,这也是 HTTP/1.1 的一个核心问题。(带宽是指每秒最大能发送或者接收的字节数。我们把每秒能发送的最大字节数称为上行带宽,每秒能够接收的最大字节数称为下行带宽。)之所以会出现这个问题,主要是由以下三个原因导致的。

  • TCP 的慢启动。一旦一个 TCP 连接建立之后,就进入了发送数据状态,刚开始 TCP 协议会采用一个非常慢的速度去发送数据,然后慢慢加快发送数据的速度,直到发送数据的速度达到一个理想状态,我们把这个过程称为慢启动(类似汽车发动过程)。慢启动是 TCP 为了减少网络拥塞的一种策略,我们是没有办法改变的。而之所以说慢启动会带来性能问题,是因为页面中常用的一些关键资源文件本来就不大,如 HTML 文件、CSS 文件和 JavaScript 文件,通常这些文件在 TCP 连接建立好之后就要发起请求的,但这个过程是慢启动,所以耗费的时间比正常的时间要多很多,这样就推迟了宝贵的首次渲染页面的时长了。
  • 同时开启了多个 TCP 连接,这些连接会竞争固定的带宽。系统同时建立了多条 TCP 连接,当带宽充足时,每条连接发送或者接收速度会慢慢向上增加;而一旦带宽不足时,这些 TCP 连接又会减慢发送或者接收的速度。比如一个页面有 200 个文件,使用了 3 个 CDN,那么加载该网页的时候就需要建立 6 * 3,也就是 18 个 TCP 连接来下载资源;在下载过程中,当发现带宽不足的时候,各个 TCP 连接就需要动态减慢接收数据的速度。这样就会出现一个问题,因为有的 TCP 连接下载的是一些关键资源,如 CSS 文件、JavaScript 文件等,而有的 TCP 连接下载的是图片、视频等普通的资源文件,但是多条 TCP 连接之间又不能协商让哪些关键资源优先下载,这样就有可能影响那些关键资源的下载速度了。
  • 队头阻塞。我们知道在 HTTP/1.1 中使用持久连接时,虽然能公用一个 TCP 管道,但是在一个管道中同一时刻只能处理一个请求,在当前的请求没有结束之前,其他的请求只能处于阻塞状态。

HTTP2 推出了著名的多路复用技术来解决上面的三个问题。

  • 一个域名只使用一个 TCP 长连接和消除队头阻塞问题。
  • 将请求分成一帧一帧的数据去传输,这样请求可以并行。

加入了多路复用技术后请求是如何进行的呢?

  • 首先,浏览器准备好请求数据,包括了请求行、请求头等信息,如果是 POST 方法,那么还要有请求体。
  • 这些数据经过二进制分帧层处理之后,会被转换为一个个带有请求 ID 编号的帧,通过协议栈将这些帧发送给服务器。
  • 服务器接收到所有帧之后,会将所有相同 ID 的帧合并为一条完整的请求信息。
  • 然后服务器处理该条请求,并将处理的响应行、响应头和响应体分别发送至二进制分帧层。
  • 同样,二进制分帧层会将这些响应数据转换为一个个带有请求 ID 编号的帧,经过协议栈发送给浏览器。
  • 浏览器接收到响应帧之后,会根据 ID 编号将帧的数据提交给对应的请求。

通过上面的分析,我们知道了多路复用是 HTTP/2 的最核心功能,它能实现资源的并行传输。多路复用技术是建立在二进制分帧层的基础之上。基于二进制分帧层,HTTP/2 还实现了很多其他功能,下面我们就来简要了解下。

  1. 可以设置请求的优先级。我们知道浏览器中有些数据是非常重要的,但是在发送请求时,重要的请求可能会晚于那些不怎么重要的请求,如果服务器按照请求的顺序来回复数据,那么这个重要的数据就有可能推迟很久才能送达浏览器,这对于用户体验来说是非常不友好的。为了解决这个问题,HTTP/2 提供了请求优先级,可以在发送请求时,标上该请求的优先级,这样服务器接收到请求之后,会优先处理优先级高的请求。
  2. 服务器推送。HTTP/2 还可以直接将数据提前推送到浏览器。当用户请求一个 HTML 页面之后,服务器知道该 HTML 页面会引用的 JavaScript 文件和 CSS 文件,那么在接收到 HTML 请求之后,附带将要使用的 CSS 文件和 JavaScript 文件一并发送给浏览器,这样当浏览器解析完 HTML 文件之后,就能直接拿到需要的 CSS 文件和 JavaScript 文件,这对首次打开页面的速度起到了至关重要的作用。
  3. 头部压缩。HTTP/2 对请求头和响应头进行了压缩。一方面,头信息使用 gzip 或 compress 压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了。
退出移动版