HTTP1.1长链接
HTTP协议的初始版本中,每进行一次HTTP通信就要断开一次TCP链接,也就是短连接。
以早期的通信情况来说,因为都是些容量很小的文本传输,所以即使这样也没有多大问题,但是随着HTTP的大量普及,文旦中包含大量富文本的情况多了起来。每次的请求都会造成无谓的TCP链接建立和断开,增加通信录的开销。
为了解决这个问题,有些浏览器在请求时,用了一个非标准的Connection字段。这个字段要求服务器不要关闭TCP链接,以便其他请求复用,服务器同样回应这个字段。
Connection: keep-alive
复制代码
一个可以复用的TCP链接就建立了,直到客户端或服务器主动关闭链接,但是这并非标准字段,不同实现的行为可能不一致,还可能造成混乱。
- 长链接
HTTP1.1版本在1997年1月发布,最大的变化就是引入了持久链接,即TCP链接默认不关闭,可以被多个请求复用,不需要再声明Connection: keep-alive。
持久连接减少了TCP链接的重复建立和断开所造成的的额外开销,减轻了服务器端的负载。减少开销的时间让HTTP请求和响应能够更早的结束,这样Web页面的速度也就响应变快了。
客户端和服务器发现对方一段时间没有活动,就可以主动关闭链接,不过规范的做法是客户端在最后一个请求时发送Connection: close,明确要求服务器关闭链接。目前对于同一个域名,大多数浏览器允许同时建立6个持久链接。
- 管道机制
同一个TCP链接里面客户端可以同时发送多个请求,这样就进一步改变了HTTP协议的效率。
从前发送请求后需等待及接收响应,才能发送下一个请求,管道化技术出现后不用等待响应即可直接发送下一个请求,这样就能够做到同时并行发送多个请求,而不需要一个接一个的等待响应了。
管道化技术比持久化链接还要快,请求数越多时间差越明显。
一个TCP链接可以传送多个回应,势必就要有一种机制,区分数据包是属于哪一个回应的,这就是Content-length字段的作用,声明本次回应的数据长度。
Content-Length: 3000
复制代码
上面代码告诉浏览器,本次回应的长度是3000个字节,后面的字节就属于下一个回应了。在1.0版本中,Content-Length字段不是必须的,因为浏览器发现服务器关闭了TCP链接,就表明收到的数据包已经完成了。
- 分块传输
使用Content-Length字段的前提条件是,服务器发送回应之前,必须知道回应的数据长度。对于一些耗时的动态操作来说,意味着,服务器要等到所有操作完成,才能发送数据,显然这样的效率不高,更好的方法是产生一块数据就发送一块,采用流模式取代缓存模式。
因此1,1规定可以不使用content-length字段,而是用分块传输编码,只要请求或响应头信息有Transfer-Encoding字段,就表明响应将又数量未定的数据块组成。
Transfer-Encoding: chunked
复制代码
每个非空数据块之前会有一个16进制的数值,表示这个块的的长度,最后是一个大小为0的块,表示本次回应的数据发送完了。
HTTP/1.1 200 OK
...
25
This is the data in the first chunk
...
2
...
4
...
0
...
复制代码
虽然HTTP1.1允许复用TCP链接,但是同一个TCP链接里面,所有的数据通信是按次序进行的,服务器只有处理完一个回应才会进行下一个回应。如果前面的请求慢,后面就会有需要请求排队,称为对头阻塞。为了避免这种问题,可以减少请求数或者同事多开持续请求。这就出现了很多的优化技巧,比如说。合并脚本和样式表,将图片嵌入css代码,域名分片等等。其实如果HTTP协议设计的更好一些,这些额外的工作都是可以避免的。
HTTP2协议
为了解决响应阻塞问题2015年推出了HTTP2。
HTTP2主要用于解决HTTP1.1效率不高的问题,他不叫HTTP2.0是因为不打算发布子版本了,下一个版本直接就叫HTTP3。
- 二进制协议
HTTP1.1头信息肯定是文本,数据体可以是文本也可以是二进制,HTTP2则是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为帧,头信息帧和数据帧。
二进制协议的一个好处是可以定义额外的帧,HTTP2定了一近十种帧,为将来的高级应用打好基础,如果使用文本实现这种功能,解析数据将会变得非常麻烦,二进制解析则方便很多。
- 多工
HTTP2复用TCP链接,在一个链接里,客户端和浏览器都可以同时发送多个请求或回应,而且不用按照顺序一一对应,这样就避免了堵塞。
在一个TCP链接里面,服务器同时收到了A请求和B请求,先回应了A请求结果发现处理过程非常耗时,先发送A请求已经处理好的部分,再回应B请求,完成后再发送A请求剩余的部分。这种双向的,实时通信就叫做多工。
效果地址: https:http2.akamai.com/demo
- 数据流
因为HTTP2的数据包是不按顺序发送的,同一个链接里面连续的数据包,可能属于不同的回应,因此必须要对数据包做标记,指出他属于哪个回应。
HTTP2将每个请求或回应的所有数据包,称为一个数据流,每个数据流都有一个独一无二的编号,数据包发送的时候,都必须标记数据流ID,用来区分它属于哪个数据流,另外还规定,客户端发出的数据流,ID一律为奇数,服务器发布的,ID为偶数。
数据流发送到一半的时候,客户端和服务器都可以发送信号取消这个数据流。1.1版本取消数据的唯一方法就是关闭TCP链接,HTTP2可以取消某一次请求,同时保证TCP链接还开着,可以被其他请求使用。
客户端还可以指定数据流的优先级,优先级越高,服务器就会越早回应。
- 压缩头信息
HTTP协议不带有状态,每次请求都必须附上所有信息,所以请求的很多字段都是重复的,比如Cookie和User Agent,一模一样的内容每次请求都必须附带,这会浪费很多带宽也影响速度。
HTTP2对这一点做了优化,引入了头信息压缩机制,一方面头信息使用gzip或compress压缩后再发送,另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送这个字段只发送索引号这样就提高速度了。
- 服务器推送
HTTP2允许服务器未经过请求主动向客户端发送资源,这就叫服务器推送。
常见场景是客户端请求一个网页,这个网页包含很多静态资源,正常情况下,客户端必须收到网页后解析HTML编码,发现有静态资源再发出静态资源请求,其实服务器可以预期到客户端请求网页后很可能会再请求静态资源,所有就主动把这些静态资源随着网页一起发给客户端了。
这个功能还是建议考虑自身的需要,会增加一部分成本开销。
压缩传输数据资源
通过压缩传输数据资源提升性能体验。默认HTTP进行数据传输数据是没有进行压缩的,原始数据多大传输的数据就多大。
我们都知道文件压缩之后数据体积减少是很客观的。
- 响应数据压缩
HTTP响应数据一般会根据数据的类型进行压缩方案的处理,比如文本最常用的方案就是Gzip的压缩方案,目前大部分的网站都采用这种压缩方式。
- gzip
浏览器再请求服务器的时候会在请求头中通过Accept-Encoding字段标识可以接收gzip压缩方案,服务器在收到请求后可以获取到这种压缩方案,将资源压缩后返回给浏览器,并且在响应头中加入Content-Encoding字段,值为gzip。
如果客户端不添加Accept-Encoding头,服务器返回了Content-Encoding,客户端如果支持的话也会正常解析。Accept-Encoding基本是浏览器自动添加的。
const zlib = require('zlib');
const fs = require('fs');
const rs = fs.cerateReadStream('jquery.js');
const ws = fs.cerateWriteStream('jquery.js.gz');
const gz = zlib.createGzip();
rs.pipe(gz).pipe(ws);
ws.on('error', (err) => {
console.log('失败');
})
ws.on('finish', () => {
console.log('完成')
})
复制代码
正常工作中gzip一般可以在nginx服务器中开启,不需要自己编写。还是比较简单的。
gzip一般是针对文本文件,比如js,css,对于图片来说一般是在开发阶段压缩。
- 请求数据压缩
HTTP2以前请求头是不可以压缩的,HTTP2引入了头信息压缩机制,一方面头信息使用gzip或express压缩后再发送,另一方面,客户端和服务器同时维护一张头信息表,通过索引字段来传输,减少厅信息数据体积。
实际工作中会存在请求正文非常大的场景,比如发表长篇博客,上报用于调试网络数据等等,这些数据如果能在本地压缩后再提交就可以节省网络流量,减少传输时间。
DFLATE是一种使用Lempel-Ziv压缩算法的哈夫曼编码压缩格式。
ZLIB是一种使用DEFLATE的压缩格式。
GZIP是一种使用DEFLATE的压缩格式。
Content-Encoding中的deflate实际上是ZLIB。
前端发送的时候可以进行压缩:
const rawBody = 'content=test';
const rawLen = rawBody.length;
const bufBody = new Unit8Array(rawLen);
for (let i = 0; i < rawLen; i++) {
bufBody[i] = rawBody.charCodeAt(i);
}
const format = 'gzip';
let buf;
switch (format) {
case gzip': buf = window.pako.gzip(bufBody); break;
}
const xhr = new XMLHttpRequest();
xhr.open('POST', '/service/');
xhr.setRequestHeader('Content-Encoding', format);
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
xhr.send(buf);
复制代码
服务器端进行解压
const http = require('http');
const zlib = require('zlib');
http.createServer((req, res) => {
let zlibStream;
const encoding = req.headers['content-encoding']
switch (encoding) {
case 'gzip' : zlibStream = zlib.createGunzip(); break;
}
res.writeHead(200, { 'Content-Type': 'text/plain' });
req.pipe(zlibStream).pipe(res);
}).listen(3000)
复制代码
这种压缩一半也只适用于文本,如果数据量太大压缩过程也是比较耗时的。