HTTP网络通信常用细节梳理

·  阅读 610
HTTP网络通信常用细节梳理

1. DNS解析

当浏览器从第三方服务器请求资源时,必须先将该跨域域名解析为IP地址,然后浏览器才能发出请求,此过程称为DNS解析。

DNS作为互联网的基础协议,其解析的速度似乎很容易被网站优化人员忽略,现在大多数新流量乃全已经针对DNS解析进行了优化,比如DNS缓存。

典型的一次DNS解析需要耗费20-120毫秒,所花费的时间几乎可以忽略不计,但是当网站中使用的资源依赖多个不同域的时候,时间就会成倍增加,从而增加了网站的加载时间。

比如某些图片较多的页面中,在发起图片加载请求之前预先把域名解析好将会有至少5%的图片加载速度提成。

一般来说前端与华中与DNS有关的有两点,一是减少DNS的请求次数(缓存DNS地址),二是进行DNS的预获取,DNS Prefetch

dns缓存可以在服务器设置DNS缓存的时间,不经常变更的ip建议设置的时间长一些。尽可能使用A或者AAAA代替CNAME,使用CND加速域名。还可以自己搭建DNS服务。

DNS与解析可以在页面中通过link标签来实现。

<link rel="dns-prefetch" href="https://fonts.googleapis.com" />
复制代码

DNS与解析只能解析不同域,同域是不能解析的,因为已经解析完了。dns-prefetch要慎用,不要每个页面都添加,会造成资源浪费。默认情况下浏览器会对当前页面中所有出现的域名进行预解析,及时没有写link标签,这是隐式解析。

2. HTTP1.1长链接

经过DNS解析获取到IP之后就要进行TCP的链接进行数据传输。

HTTP协议的初始版本中,每进行一次HTTP通信就要断开一次TCP链接,也就是短连接。

以早期的通信情况来说,因为都是些容量很小的文本传输,所以即使这样也没有多大问题,但是随着HTTP的大量普及,文旦中包含大量富文本的情况多了起来。每次的请求都会造成无谓的TCP链接建立和断开,增加通信录的开销。

为了解决这个问题,有些浏览器在请求时,用了一个非标准的Connection字段。这个字段要求服务器不要关闭TCP链接,以便其他请求复用,服务器同样回应这个字段。

Connection: keep-alive
复制代码

一个可以复用的TCP链接就建立了,直到客户端或服务器主动关闭链接,但是这并非标准字段,不同实现的行为可能不一致,还可能造成混乱。

1. 长链接

HTTP1.1版本在19971月发布,最大的变化就是引入了持久链接,即TCP链接默认不关闭,可以被多个请求复用,不需要再声明Connection: keep-alive

持久连接减少了TCP链接的重复建立和断开所造成的的额外开销,减轻了服务器端的负载。减少开销的时间让HTTP请求和响应能够更早的结束,这样Web页面的速度也就响应变快了。

客户端和服务器发现对方一段时间没有活动,就可以主动关闭链接,不过规范的做法是客户端在最后一个请求时发送Connection: close,明确要求服务器关闭链接。目前对于同一个域名,大多数浏览器允许同时建立6个持久链接。

2. 管道机制

同一个TCP链接里面客户端可以同时发送多个请求,这样就进一步改变了HTTP协议的效率。

从前发送请求后需等待及接收响应,才能发送下一个请求,管道化技术出现后不用等待响应即可直接发送下一个请求,这样就能够做到同时并行发送多个请求,而不需要一个接一个的等待响应了。

管道化技术比持久化链接还要快,请求数越多时间差越明显。

一个TCP链接可以传送多个回应,势必就要有一种机制,区分数据包是属于哪一个回应的,这就是Content-length字段的作用,声明本次回应的数据长度。

Content-Length: 3000
复制代码

上面代码告诉浏览器,本次回应的长度是3000个字节,后面的字节就属于下一个回应了。在1.0版本中,Content-Length字段不是必须的,因为浏览器发现服务器关闭了TCP链接,就表明收到的数据包已经完成了。

3. 分块传输

使用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协议设计的更好一些,这些额外的工作都是可以避免的。

3. HTTP2协议

为了解决响应阻塞问题2015年推出了HTTP2

HTTP2主要用于解决HTTP1.1效率不高的问题,他不叫HTTP2.0是因为不打算发布子版本了,下一个版本直接就叫HTTP3

1. 二进制协议

HTTP1.1头信息肯定是文本,数据体可以是文本也可以是二进制,HTTP2则是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为帧,头信息帧和数据帧。

二进制协议的一个好处是可以定义额外的帧,HTTP2定了一近十种帧,为将来的高级应用打好基础,如果使用文本实现这种功能,解析数据将会变得非常麻烦,二进制解析则方便很多。

2. 多工

HTTP2复用TCP链接,在一个链接里,客户端和浏览器都可以同时发送多个请求或回应,而且不用按照顺序一一对应,这样就避免了堵塞。

在一个TCP链接里面,服务器同时收到了A请求和B请求,先回应了A请求结果发现处理过程非常耗时,先发送A请求已经处理好的部分,再回应B请求,完成后再发送A请求剩余的部分。这种双向的,实时通信就叫做多工。

效果地址: https:http2.akamai.com/demo

3. 数据流

因为HTTP2的数据包是不按顺序发送的,同一个链接里面连续的数据包,可能属于不同的回应,因此必须要对数据包做标记,指出他属于哪个回应。

HTTP2将每个请求或回应的所有数据包,称为一个数据流,每个数据流都有一个独一无二的编号,数据包发送的时候,都必须标记数据流ID,用来区分它属于哪个数据流,另外还规定,客户端发出的数据流,ID一律为奇数,服务器发布的,ID为偶数。

数据流发送到一半的时候,客户端和服务器都可以发送信号取消这个数据流。1.1版本取消数据的唯一方法就是关闭TCP链接,HTTP2可以取消某一次请求,同时保证TCP链接还开着,可以被其他请求使用。

客户端还可以指定数据流的优先级,优先级越高,服务器就会越早回应。

4. 压缩头信息

HTTP协议不带有状态,每次请求都必须附上所有信息,所以请求的很多字段都是重复的,比如CookieUser Agent,一模一样的内容每次请求都必须附带,这会浪费很多带宽也影响速度。

HTTP2对这一点做了优化,引入了头信息压缩机制,一方面头信息使用gzipcompress压缩后再发送,另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送这个字段只发送索引号这样就提高速度了。

5. 服务器推送

HTTP2允许服务器未经过请求主动向客户端发送资源,这就叫服务器推送。

常见场景是客户端请求一个网页,这个网页包含很多静态资源,正常情况下,客户端必须收到网页后解析HTML编码,发现有静态资源再发出静态资源请求,其实服务器可以预期到客户端请求网页后很可能会再请求静态资源,所有就主动把这些静态资源随着网页一起发给客户端了。

这个功能还是建议考虑自身的需要,会增加一部分成本开销。

4. 压缩传输数据资源

通过压缩传输数据资源提升性能体验。默认HTTP进行数据传输数据是没有进行压缩的,原始数据多大传输的数据就多大。

文件压缩之后数据体积减少是很客观的。

HTTP响应数据一般会根据数据的类型进行压缩方案的处理,比如文本最常用的方案就是Gzip的压缩方案,目前大部分的网站都采用这种压缩方式。

1. 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一般是针对文本文件,比如jscss,对于图片来说一般是在开发阶段压缩。

2. 请求数据压缩

HTTP2以前请求头是不可以压缩的,HTTP2引入了头信息压缩机制,一方面头信息使用gzipexpress压缩后再发送,另一方面,客户端和服务器同时维护一张头信息表,通过索引字段来传输,减少厅信息数据体积。

实际工作中会存在请求正文非常大的场景,比如发表长篇博客,上报用于调试网络数据等等,这些数据如果能在本地压缩后再提交就可以节省网络流量,减少传输时间。

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)
复制代码

这种压缩一般也只适用于文本,如果数据量太大压缩过程也是比较耗时的。

分类:
前端
标签: