HTTP是互联网的基础协议,也是数据传输的重要组成,本文主要介绍HTTP1.1 和 HTTP2.0, 以及HTTP的历史演进以及设计思路。在学习HTTP之前,我们先来了解HTTP的概念。
HTTP: 超文本传输协议(HyperText Transfer Protocol), 是一种用于分布式、协作式和超媒体信息系统的应用层协议, 是万维网的数据通信的基础,基于TCP/IP通信协议来传递数据,它不涉及数据包(packet)传输,主要规定了客户端和服务器之间的通信格式,默认使用80端口。
HTTP0.9
最早版本是1991年发布的0.9版。该版本极其简单,只有一个命令GET,不支持请求头。
HTTP/0.9具有典型的无状态性,每个事务独立进行处理,事务结束时就释放这个连接。
HTTP1.0
1996年5月,HTTP/1.0 版本发布,是HTTP协议的第二个版本,此协议版本是第一个在通讯中指定版本号的HTTP协议版本,至今仍被沿用,此版亦增加了很多新特性,如引入请求头和响应头。
一、响应对象不只限于超文本,还可以接受图、音视频、二进制文件等文件,为互联网的大发展奠定了基础
二、新增请求方法,如head/post,丰富了浏览器与服务器的交互手段
三、增加缓存功能,已请求过的内容再次请求时就可直接使用缓存
四、增加请求头、响应头,让请求和相应都更清晰
accept 解决文件格式问题,是json还是html,浏览器根据不同文件格式来解析文件
accept-charset 解决文件编码问题,告知浏览器如何将字符流解析成字节流
accept-encoding 解决大文件压缩问题,浏览器采用指定的解压方式来解压
-
accept-language 解决国际化问题,不同国家请求不同语言的文件
其他的新增功能还包括状态码(status code)、多字符集支持、多部分发送(multi-part type)、权限(authorization)、内容编码(content encoding)等
缺点:
1、每个TCP连接只能发送一个请求。发送数据完毕,连接就关闭,如果还要请求其他资源,就必须再新建一个连接。默认使用短链接(除非手动设置),会增加大量无谓的开销,比如TCP建立连接需要三次握手等(后面再详解TCP连接建立)
2、只有前一个请求完,才可以发送下一个请求,如果某个请求没有及时返回,容易造成队头阻塞
3、不支持断点续传等等
为了解决这些问题,推出了HTTP1.1
HTTP1.1
1997年1月,HTTP/1.1 版本发布,只比 1.0 版本晚了半年。它进一步完善了 HTTP 协议,一直用到了20年后的今天,,是目前使用最广泛的协议版本。HTTP/1.1是目前主流的HTTP协议版本,相对于HTTP/1.0新增了以下内容
**1. 默认为长连接 **
HTTP 1.1支持长连接(PersistentConnection)和请求的流水线(Pipelining)处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟,在HTTP1.1中默认开启Connection:keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点。
客户端和服务器发现对方一段时间没有活动,就可以主动关闭连接。不过,规范的做法是,客户端在最后一个请求时,发送Connection: close,明确要求服务器关闭TCP连接。
<pre style="margin: 0px; padding: 0px; color: rgb(51, 51, 51); font-size: 17px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">目前,对于同一个域名,大多数浏览器允许同时建立6个持久连接.</pre>
2. 管道机制
把先进先出(FIFO)队列从客户端(请求队列)移到服务端(响应队列),这样在同一个TCP连接里面,客户端可以同时发送多个请求。这样就进一步改进了HTTP协议的效率。
3、提供了范围请求功能(宽带优化)****
HTTP1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1则在请求头引入了range头域,它允许只请求资源的某个部分,即返回码是206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。这是支持文件断点续传的基础。
4.提供了虚拟主机的功能(HOST域)
在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)。
5.多了一些缓存处理字段
HTTP/1.1在1.0的基础上加入了一些cache的新特性,引入了实体标签,一般被称为e-tags,新增更为强大的Cache-Control头。
6.Content-Length 字段
一个TCP连接现在可以传送多个回应,势必就要有一种机制,区分数据包是属于哪一个回应的。这就是Content-length字段的作用,声明本次回应的数据长度,如
<pre style="margin: 0px; padding: 0px; color: rgb(51, 51, 51); font-size: 17px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Content-Length: 3495</pre>
这行代码告诉浏览器,本次回应的长度是3495个字节,后面的字节就属于下一个回应了。
7.分块传输编码
响应体可以分块传输,无需一次传输全部内容,使用Content-Length字段的前提条件是,服务器发送回应之前,必须知道回应的数据长度。对于一些很耗时的动态操作来说,这意味着,服务器要等到所有操作完成,才能发送数据,显然这样的效率不高。更好的处理方法是,产生一块数据,就发送一块,采用"流模式"(stream)取代"缓存模式"(buffer)。
因此,1.1版规定可以不使用Content-Length字段,而使用"分块传输编码"(chunked transfer encoding)。只要请求或回应的头信息有Transfer-Encoding字段,就表明回应将由数量未定的数据块组成。
1.1版还新增了许多数据交互方法:PUT、PATCH、HEAD、 OPTIONS、DELETE
缺点:
虽然1.1版允许复用TCP连接,但是同一个TCP连接里面,所有的数据通信是按次序进行的。服务器只有处理完一个回应,才会进行下一个回应。要是前面的回应特别慢,后面就会有许多请求排队等着。这称为"队头堵塞"(Head-of-line blocking)。
为了避免这个问题,只有两种方法:一是减少请求数,二是同时多开持久连接。这导致了很多的网页优化技巧,比如合并脚本和样式表、将图片嵌入CSS代码、域名分片(domain sharding)等等。如果HTTP协议设计得更好一些,这些额外的工作是可以避免的。
HTTP2.0
2015年,HTTP/2 发布。它不叫 HTTP/2.0,是因为标准委员会不打算再发布子版本了,下一个新版本将是 HTTP/3, HTTP2.0主要包括以下内容
1. 二进制分帧
HTTP 2.0 的所有帧都采用二进制编码
帧:客户端与服务器通过交换帧来通信,帧是基于这个新协议通信的最小单位。
消息:是指逻辑上的 HTTP 消息,比如请求、响应等,由一或多个帧组成。
流:流是连接中的一个虚拟信道,可以承载双向的消息;每个流都有一个唯一的整数标识符(1、2 … N);
HTTP/2 定义了近十种帧,为将来的高级应用打好了基础。如果使用文本实现这种功能,解析数据将会变得非常麻烦,二进制解析则方便得多
2.多路复用
有了新的分帧机制后,HTTP/2.0不再依赖多个TCP 连接去处理更多并发的请求。每个数据流都拆分成很多互不依赖的帧,而这些帧可以交错(乱序发送),还可以分优先级。最后再在另一端根据每个帧首部的流标识符把它们重新组合起来。HTTP 2.0 连接都是持久化的,而且客户端与服务器之间也只需要一个连接(每个域名一个连接)即可。在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,而且不用按照顺序一一对应,这样就避免了"队头堵塞".
3.头部压缩
HTTP 协议不带有状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如Cookie和User Agent,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。
HTTP/2 对这一点做了优化,引入了头信息压缩机制(header compression)。一方面,头信息使用gzip或compress压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了。
4.数据流
因为 HTTP/2 的数据包是不按顺序发送的,同一个连接里面连续的数据包,可能属于不同的回应。因此,必须要对数据包做标记,指出它属于哪个回应。
HTTP/2 将每个请求或回应的所有数据包,称为一个数据流(stream)。每个数据流都有一个独一无二的编号。数据包发送的时候,都必须标记数据流ID,用来区分它属于哪个数据流。另外还规定,客户端发出的数据流,ID一律为奇数,服务器发出的,ID为偶数。
数据流发送到一半的时候,客户端和服务器都可以发送信号(RST_STREAM帧),取消这个数据流。1.1版取消数据流的唯一方法,就是关闭TCP连接。这就是说,HTTP/2 可以取消某一次请求,同时保证TCP连接还打开着,可以被其他请求使用。
客户端还可以指定数据流的优先级。优先级越高,服务器就会越早回应。这样请求就不必排队了,既节省了时间,也最大限度地利用了每个连接
5.服务端推送
服务端推送能把客户端所需要的资源伴随着index.html一起发送到客户端,省去了客户端重复请求的步骤。正因为没有发起请求,建立连接等操作,所以静态资源通过服务端推送的方式可以极大地提升速度。
缺点:
HTTP2.0 TCP然是顺序发送,顺序接收的,依然有队头堵塞问题,TCP握手过程导致连接超时,也是影响数据传输效率的重要因素。于是,google带头对TCP和UDP进行改造整合,推出HTTP3.0
HTTP3.0
HTTP3.0又称为HTTP Over QUIC,其弃用TCP协议,改为使用基于UDP协议的QUIC协议来实现。
1、修复队头阻塞 Head-of-line blocking
队头阻塞 是计算机网络中是一种性能受限的现象,通俗来说就是:一个数据包影响了一堆数据包,它不来大家都走不了。
2、0RTT 建链
衡量网络建链的常用指标是RTT Round-Trip Time,也就是数据包一来一回的时间消耗。RTT包括三部分:往返传播时延、网络设备内排队时延、应用程序数据处理时延。
简单来说,基于TCP协议和TLS协议的HTTP2.0在真正发送数据包之前需要花费一些时间来完成握手和加密协商,完成之后才可以真正传输业务数据。
但是QUIC则第一个数据包就可以发业务数据,从而在连接延时有很大优势,可以节约数百毫秒的时间。
了解完HTTP的发展历史以及每个版本协议的特点后,下一篇文章,将对https进行一些简单梳理,在梳理之前,我们一起来解答一下,上一篇文章留下的思考题。(仅供参考)
如何编写V8友好的高性能javascript代码?
1、隐藏类(hidden class)的教训
在构造函数里初始化所有对象的成员(所以这些实例之后不会改变其隐藏类)
总是以相同的次序初始化对象成员
永远不要delete对象的某个属性
在以上例子中p2.z破坏了上述原则, 将导致p1与p2使用了不同的隐藏类。在我们为p2添加“z”这个成员之前,p1和p2一直共享相同的内部隐藏类——所以V8可以生成一段单独版本的优化汇编码,这段代码可以同时封装p1和p2的JavaScript代码。派生出这个新的隐藏类还将使编译器无法在optimized模式执行。我们越避免隐藏类的派生,就会获得越高的性能。
以上例子由于调用了delete,将导致hidden class产生变化,导致p1.y不能用inline cache直接获取。以上程序在使用了delete之后耗时0.339s,在注释掉delete后只需0.05s
2、deoptimized的教训
单态操作优于多态操作
谨慎使用try catch与for in
如果一个操作的输入总是相同类型的,则其为单态操作。否则,操作调用时的某个参数可以跨越不同的类型,那就是多态操作。比如add()的第二个调用就触发了多态操作:
3、内存管理与GC的教训
闭包会使程序逻辑变复杂,有时会看不清楚是否对象内存被释放,因此要注意释放闭包中的大对象, 否则会引起内存泄露。
一些异步操作如timer(timeout/setInterval)等也会造成内存泄漏
4、使用数字的教训
当类型可以改变时,V8使用标记来高效的标识其值。V8通过其值来推断你会以什么类型的数字来对待它。因为这些类型可以动态改变,所以一旦V8完成了推断,就会通过标记高效完成值的标识。不过有的时候改变类型标记还是比较消耗性能的,我们最好保持数字的类型始终不变。通常标识为有符号的31位整数是最优的。
优化手段都在我们日常代码的编写当中,以上也只是总结了一部分优化手段,也欢迎小伙伴积极投稿,一起共建前端代码规范。