本文是从输入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 还实现了很多其他功能,下面我们就来简要了解下。
- 可以设置请求的优先级。我们知道浏览器中有些数据是非常重要的,但是在发送请求时,重要的请求可能会晚于那些不怎么重要的请求,如果服务器按照请求的顺序来回复数据,那么这个重要的数据就有可能推迟很久才能送达浏览器,这对于用户体验来说是非常不友好的。为了解决这个问题,HTTP/2 提供了请求优先级,可以在发送请求时,标上该请求的优先级,这样服务器接收到请求之后,会优先处理优先级高的请求。
- 服务器推送。HTTP/2 还可以直接将数据提前推送到浏览器。当用户请求一个 HTML 页面之后,服务器知道该 HTML 页面会引用的JavaScript 文件和 CSS 文件,那么在接收到 HTML 请求之后,附带将要使用的 CSS 文件和 JavaScript 文件一并发送给浏览器,这样当浏览器解析完 HTML 文件之后,就能直接拿到需要的 CSS 文件和 JavaScript 文件,这对首次打开页面的速度起到了至关重要的作用。
- 头部压缩。HTTP/2 对请求头和响应头进行了压缩。一方面,头信息使用gzip或compress压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。