客户端和服务器之间的信息通信有多重方式:
- XMLHttpRequest/ajax/axios/$.ajax/fetch数据交互
- 跨域处理方案:ajax、fetch、jsonp、postMessage...
- 资源获取:(html/css/js/image/音视频...)
- webscoket
- ...
一次HTTP过程包括:
- 客户端把信息传递给服务器或者向服务器发送请求(请求 Request)
- 服务器端接收客户端信息并且返回给客户端相关的内容(响应 Response)
- 请求+响应=一次HTTP事务
客户端和服务器端之间传输的所有内容,统称为HTTP报文。一次HTTP报文包括以下信息:
- 起始行:基本信息(包含HTTP的版本等)。
- 请求起始行
'GET{请求方式} /res-min/themes/marxico.css{请求地址} HTTP/1.1{HTTP版本号}'。 - 响应起始行
'HTTP/1.1{HTTP版本号} 200{HTTP响应状态码} OK{状态码描述}'
- 请求起始行
- 首部(头):请求头(客户端->服务器)、响应头(服务器->客户端)
- 主体:请求主体(客户端->服务器)、响应主体(服务器->客户端)
客户端和服务器之间的数据传输,依托于网络(通信模式 TCP/UDP... & 传输协议 HTTP/HTTPS/FTP...)。那么,这个过程详细是怎么样的呢?
从输入URL地址到看到页面,中间都经历了什么?如何优化这一过程?
老生常谈的问题
- URL解析(识别URL)
- 检查缓存(强缓存、协商缓存{针对于资源文件请求} & 本地存储{针对于数据请求})
- DNS服务器解析(域名解析:根据域名解析出服务器的外网IP)
- TCP三次握手(建立客户端和服务器之间的网络连接通道)
- 基于HTTP/HTTPS等传输协议,实现客户端和服务器之间的信息通信
- TCP四次挥手(把建立好的网络通道释放掉)
- 客户端渲染(呈现出页面和效果)
下面详细说说每个过程
URL解析(识别URL)
URI统一资源标识符,包括
- URL:统一资源定位符
- URN:统一资源名称
一段URL 'http://user:pass@www.baidu.cn:80/st/index.html?xxx=xxx&xxx=xxx#video' ,结构分析如下:
传输协议
传输协议:http / https / ftp ...
- HTTP超文本传输协议。即除了传输文本(例如字符串等)还可以传输其余的信息(例如:文件流、二进制或者Buffer格式再或者BASE64格式的数据)
- HTTPS=HTTP+SSL 更加安全的HTTP。传输的内容经过加密处理,一般涉及支付类的产品都采用这种协议
- FTP文件传输协议,一般用于直接基于一些FTP工具(例如filezilla),把开发的文件部署到服务器上
- ...
登录认证信息
用户名密码:user:pass,一般是不用的
域名
域名:www.baidu.cn
- 顶级域名 baidu.cn
- 一级域名 www.baidu.cn
- 二级域名 video.baidu.cn
- 三级域名 student.video.baidu.cn
- ...
购买的是顶级域名,自己后期可以分配二级/三级域名。
域名的目的就是给对应的服务器外网IP起一个别名,方便用户记忆
域名和服务器都购买完成后,需要在DNS服务器上生成一条解析记录,用于以后的DNS解析
.com / .cn / .net / .org / .gov /不同的后缀也有一些不同的意义
协议、域名、端口号只要有一个不同,则为跨域。跨域的问题后面写文章单独讲。以下皆为跨域:
'www.baidu.com'VS'www.qq.com':跨域'www.baidu.com'VS'video.baidu.com':跨域(主域相同,但是子域不同)' www.baidu.com:80'VS'www.baidu.com:443':跨域'http://www.baidu.com'VS'https://www.baidu.com':跨域
下面为同源:
'http://www.baidu.com:80/st.html' VS 'http://www.baidu.com:80/index.html' :同源
端口号
端口号:80。端口就是用来区分一台服务器上的多个项目的(每一个项目其实都是一个服务)。 取值范围 0~65535之间
默认端口号:在浏览器地址栏中输入地址,我们不写端口号,浏览器会帮助我们加上,传递给服务器的时候是带着端口号的。http->80 , https->443,ftp->21。
请求资源的路径名称
请求资源的路径名称:/st/index.html,可以基于路径找到客户端需要的资源文件。
用户看到的URL地址可能是重写后的(看到的地址在文件目录中不存在),例如ajax数据请求的接口地址为/api/list,后台可以根据这个不存在的地址返回其他相应的东西
问号参数信息(查询字符串)
问号参数信息:?xxx=xxx&xxx=xxx
- 把信息传递给服务器。GET系列请求一般都是这样传递参数 。 xxx=xxx&xxx=xxx这种格式叫做x-www-form-urlencoded格式
- 如果是页面跳转,查询字符串可以把信息传递给另外一个页面
片段标识符(HASH值)
HASH(哈希)值:#video 一般用作:
- 锚点定位
- HASH路由
其他问题(URL编译问题)
如果一段url如下
let url = ` http://www.xxx.com/index.html?lx=1&from=http://www.qq.com/'.
其中查询字符串from包含一个完整的域名,浏览其解析的时候就会出问题, http://www.xxx.com/index.html?lx=1&from= 会被解析为一个url, http://www.qq.com/ 会被解析为另一个url。
所以我们要对整个url或者后面的查询字符串进行编码,让浏览器只识别成为一个url
编码分类:
encodeURI&decodeURI:编译空格和中文,一般编译整个URL中的信息let url = ` http://www.xxx.com/s t/index.html?x=1&name=你好&from=http://www.qq.com ` console.log(encodeURI(url))
空格和中文被编码
encodeURIComponent&decodeURIComponent:编译空格和中文以及一些特殊符号,所以一般只是用来编译传递的信息值的,而不是整个URL,以解决URL解析不了或者传递信息的乱码等问题。let url = ` http://www.xxx.com/st/index.html?x=1&name=${encodeURIComponent('你 好')}&from=${encodeURIComponent('http://www.qq.com')} ` console.log(url)
escape&unescape:用于客户端页面信息传递或者一些信息的编译的「例如:cookie中的中文内容编译」console.log(escape('你好')); console.log(encodeURIComponent('你好'));
检查缓存
缓存处理是基于HTTP网络层进行优化的一个非常重要的手段「针对的资源文件请求」
检查缓存的两种方式
- 强缓存、协商缓存(针对于资源文件请求)
- 本地存储(针对于数据请求)
缓存位置
- Memory Cache : 内存缓存
- Disk Cache:硬盘缓存
区别:
- 打开网页时:浏览器会首先查找 Disk Cache中是否有匹配的缓存,如有则使用,如没有则发送网络请求。
- 普通刷新 (F5)时:因TAB没关闭,因此Memory Cache是可用的,所以刷新时,如果内存中有缓存,会被优先使用,其次才去查找Disk Cache
- 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control: no-cache,服务器直接返回 200 和最新内容
强缓存
强缓存:Expires / Cache-Control
强缓存的作用过程是这样的:
- 第一次请求资源时返回请求结果和缓存标识(响应头Expires/ Cache-Control),把请求结果和缓存表示存储在浏览器缓存当中
- 缓存标识的作用机理:
- Expires:缓存过期时间,用来指定资源到期的时间(HTTP/1.0)
- Cache-Control:会返回如下样子的响应头
'Cache-Control: max-age=2592000',意思为第一次拿到资源后的2592000秒内(30天),再次发送请求,读取缓存中的信息(HTTP/1.1)
- 如果再次发送请求,会先检测缓存信息和缓存标识Expires/ Cache-Control,如果有,且未过期,那么客户端直接读取缓存的信息,不再发送请求给服务器
- 注意如果两者同时存在的话,Cache-Control优先级高于Expires
问题:本地缓存了文件,但是服务对应的资源文件更新了,我们如何保证获取的是最新的内容?
-
请求资源文件的时候设置时间戳
例如:第一次
<link href='index.css?20210224215800'>,第二次<link href='index.css?20210227000000'>。如果服务器资源有更新,再次发请求,保证时间戳不一样,这样就不会走本地的强缓存了,而是从新拉取最新的资源 -
文件HASH名
例如:第一次
<link href='dasdasd43546.css'>,如果服务器资源更新,文件名字重新HASH(webpack)<link href='75675675fsdff6.css'>,这样就不会走本地的强缓存了
所以HTML文件永远不会去做强缓存
第一次
<html>
<link href='dasdasd43546.css'>
</html>
第二次
<html>
<link href='75675675fsdff6.css'>
</html>
协商缓存
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程
协商缓存的作用过程是这样的(以Etag举例):
- 首先第一次发送请求,获得的响应包含响应头(假设为Etag:s35b56f)和响应主体,然后把内容缓存下来
- 第二次发送请求给后台,会携带缓存标识(If-Modfined-Since/If-None-Match)发送HTTP请求,例如请求头:If-Modfined-Since:s35b56f,这个标识就是第一次返回的Etag的一个标识
- 服务器根据Etag判断文件是否更新
- 没更新:返回304,不返回内容,通知客户端读取本地的缓存信息
- 更新了:返回200,以及最新的资源信息,以及Last-Modfined/Etag的新的值
- 浏览器接收到返回的信息
- 如果是200:说明是最新的信息那就直接渲染,并且把最新的结果和表示缓存到本地
- 如果是304:就从本地缓存中获取内容进行渲染
Last-Modified / ETag的意义:
- Last-Modified:记录服务器资源文件最后一次更新的时间
- ETag:只要服务器资源文件改变,会生成一个不同的标识
协商缓存的意义:
- 当强缓存失效(或者不存在)(例如:html)那就可以做协商缓存,然后校验协商缓存即可
- 每一次都会向服务器校验资源是否更新
可以看一下百度某个css文件的缓存设置
注意:
强缓存还是协商缓存都是服务器端设置的,客户端浏览器自己会根据返回的一些信息,进行相关处理,无需前端单独设置
注意:强缓存没有发送请求,在发送请求之前发现有缓存就用了本地的缓存内容,而协商缓存发送了请求(因为要询问后台是否使用缓存,与后台协商),如果返回了304,就用本地的缓存。
本地存储缓存
从这个角度来缓存,就需要使用js进行编码,逻辑处理
本地存储缓存的两种方式:
- 页面不关闭,针对于不经常更新的数据,我们读取缓存数据,这种数据一般存在内存中,页面刷新,存储的数据就没有了
- 页面关闭,重新打开,我们也可以读取缓存中的数据,这种数据就是持久化存储,我们可以自己设置过期时间
客户端存储数据的几种方案:
- (全局)变量存储「vuex/redux」:页面刷新或者关闭后重新打开,之前存储的数据都没有了(内存释放问题导致的)
- cookie
- webStorage:localStorage & sessionStorage
- IndexedDB
- Cache
- Manifest 离线存储
localStorage VS sessionStorage
HTML5新增的API「不兼容IE8及以下浏览器」
- localStorage:持久化本地存储「没有过期时间」,页面关闭存储的内容也是在的,只有手动清除(或者卸载浏览器)才会清除
- sessionStorage会话存储,页面关闭后,存储的信息会消失「但是页面刷新是不消失的」
localStorage VS cookie
地存储的数据是有同源访问限制的,只允许读取本源下存储的内容
-
cookie只允许一个源下最多存储4KB内容,所以不能存储太多的数据。localStorage可以同源下存储5MB内容!
-
cookie是需要设置过期时间的,超过时间就失效了,并且有路径等限制。localStorage是持久化存储,没有过期时间,除非自己设定一些过期的处理机制。
-
cookie不稳定「一些浏览器自带的清除操作,有可能会把cookie清除掉;开启无痕浏览或者隐私模式,则不能存储cookie信息。但是localStorage不受这些操作的影响。
-
cookie兼容低版本浏览器
-
cookie不算严格的本地存储,和服务器之间有很多的联系。客户端向服务器发送请求的时候,会默认把本地的cookie信息,基于请求头发送给服务器;并且如果服务器返回的响应头中有Set-Cookie字段,浏览器也会默认把这些信息在客户端本地存在cookie中。localStorage是严格本地存储,默认情况下和服务器没有关系。
用代码来演示本地缓存的使用原理
将缓存数据存在全局变量中(刷新缓存消失):
let submit = document.querySelector('#submit'),
runing = false;
let serverData;
submit.onclick = function () {
if (runing) return;//简单的防抖处理
runing = true;
if (serverData) {
// 如果有数据,直接使用缓存数据,无需从服务器获取
console.log('请求回来的数据是:', serverData);
runing = false;
return;
}
// 从服务器拉去数据,并其存储到全局变量中
axios.get('http://127.0.0.1:8888/home_banner').then(response => {
console.log('请求回来的数据是:', response.data);
serverData = response.data;
runing = false;
});
};
sessionStorage:
submit.onclick = function () {
if (runing) return;
runing = true;
let data = sessionStorage.getItem('@A');
if (data) {
// 如果有数据,直接使用缓存数据,无需从服务器获取
console.log('请求回来的数据是:', JSON.parse(data));
runing = false;
return;
}
// 从服务器拉去数据
axios.get('http://127.0.0.1:8888/home_banner').then(response => {
console.log('请求回来的数据是:', response.data);
sessionStorage.setItem('@A', JSON.stringify(response.data));
runing = false;
});
localStorage持久化存储要加上过期时间。
submit.onclick = function () {
if (runing) return;
runing = true;
let data = localStorage.getItem('@A');
if (data) {
data = JSON.parse(data);
// 自己可以设定过期的标准 1小时
if (new Date() - data.time <= 60 * 60 * 1000) {
console.log('请求回来的数据是:', data.data);
runing = false;
return;
}
}
// 从服务器拉去数据,并其存储到全局变量中
axios.get('http://127.0.0.1:8888/home_banner').then(response => {
console.log('请求回来的数据是:', response.data);
localStorage.setItem('@A', JSON.stringify({
time: +new Date(),
data: response.data
}));
runing = false;
});
}
DNS服务器解析
两种解析方法
-
递归查询
-
迭代查询
如果访问资源的域名的域名比较多,说明资源是部署到多台服务器上的,此时需要更多的DNS解析,导致消耗的时间会更多
多台服务器也有自己的好处:
-
资源的合理利用,性能高的服务器就用来存储数据,性能不好的就用来存储静态资源,减少资金
-
抗压能力加强,多台服务器减少压力
-
提高HTTP并发,多台服务器可以提高总共服务器的并发请求 例如百度:
将不用的资源放到不同的服务器上
DN解析优化:
每一次DNS解析时间预计在20~120毫秒,可以使用DNS预获取(DNS Prefetch)来减少DNS请求次数。
例如:
<meta http-equiv="x-dns-prefetch-control" content="on">
<link rel="dns-prefetch" href="//static。360buyimg。com"/>
<link rel="dns-prefetch" href="//misc。360buyimg。com"/>
<link rel="dns-prefetch" href="//img10.360buyimg。com"/>
<link rel="dns-prefetch" href="//d。3.cn"/>
<link rel="dns-prefetch" href="//d。jd。com"/>
原理:link会单独开辟一个线程去加载资源,同时html继续向下渲染。所以可以单独开辟一个县城预先解析所有DNS并且缓存,如果下面再遇到这个域名,例如img的src中包含这个域名,那么DNS解析就不用重新再次解析了。
TCP三次握手
TCP三次握手的目的:让客户端和服务器端建立稳定可靠的传输通道,确定双方都可以收发信息。
UDP不需要三次握手,但是快且不稳定。
数据传输
HTTP请求与响应。
这部分可以看一下我以前写的这篇文章
HTTP系列:AJAX基础梳理、axios基本使用梳理 (juejin.cn)
TCP四次挥手
释放建立的链接通道
- 服务器端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文
- 但关闭连接时,当服务器端收到FIN报文时,很可能并不会立即关闭链接(正在传输数据),所以只能先回复一个ACK报文,告诉客户端:”你发的FIN报文我收到了”,只有等到服务器端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送
- 等所有数据正在传输的数据全部传输完成,服务器才给再次发送一个FIN报文
故需要四步握手
TCP连接优化方法:
保持长链接,不关闭http通道,设置Connection: keep-alive请求头
客户端渲染
客户端渲染优化可以看我以前的两篇文章
浏览器渲染过程和CRP优化一:渲染过程 (juejin.cn)
浏览器渲染过程和CRP优化二:CRP优化 (juejin.cn)
网络层性能优化汇总
网络优化是前端性能优化的中的重点内容,因为大部分的消耗都发生在网络层,尤其是第一次页面加载,如何减少等待时间很重要。http层面的性能优化可以减少白屏的效果和时间
刚说了http的一次流程,中间顺便穿插一些优化的方法,这里做一个汇总,顺便补充一些内容
-
利用缓存
-
对于静态资源文件实现强缓存和协商缓存(扩展:文件有更新,如何保证及时刷新)
-
对于不经常更新的接口数据采用本地存储做数据缓存(扩展:cookie / localStorage / vuex|redux 区别)
-
-
DNS优化
-
分服务器部署,增加HTTP并发性(导致DNS解析变慢)
-
DNS Prefetch
-
-
TCP的三次握手和四次挥手
- Connection:keep-alive
-
数据传输
-
减少数据传输的大小
-
内容或者数据压缩(webpack等)
-
服务器端一定要开启GZIP压缩(一般能压缩60%左右)
-
大批量数据分批次请求(例如:下拉刷新或者分页,保证首次加载请求数据少)
-
-
减少HTTP请求的次数
-
资源文件合并处理
-
字体图标,一些就不要发请求了,尽量使用字体去做,阿里图标
-
雪碧图 CSS-Sprit
-
图片的BASE64
-
-
-
CDN服务器“地域分布式”
在多个地方部署服务器,让各个地域的访问速度保持一致
-
采用HTTP2.0
减少白屏:
-
loading人性化体验
-
骨架屏:客户端骨屏(其实也是一个loading) + 服务器骨架屏
-
图片延迟加载
减少白屏也是一个值得探索的问题,以后写文章再详细说
HTTP几个版本的区别
HTTP1.0和HTTP1.1的一些区别
- 缓存处理,HTTP1.0中主要使用 Last-Modified,Expires 来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略:ETag,Cache-Control
- 带宽优化及网络连接的使用,HTTP1.1支持断点续传,即返回码是206(Partial Content)
- 错误通知的管理,在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除…
- Host头处理,在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)
- 长连接,HTTP1.1中默认开启Connection: keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点
HTTP2.0和HTTP1.X相比的新特性
-
新的二进制格式(Binary Format),HTTP1.x的解析是基于文本,基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合,基于这种考虑HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮
-
header压缩,HTTP1.x的header带有大量信息,而且每次都要重复发送,HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小
-
服务端推送(server push),例如我的网页有一个sytle.css的请求,在客户端收到sytle.css数据的同时,服务端会将sytle.js的文件推送给客户端,当客户端再次尝试获取sytle.js时就可以直接从缓存中获取到,不用再发请求了
// 通过在应用生成HTTP响应头信息中设置Link命令 Link: </styles.css>; rel=preload; as=style, </example.png>; rel=preload; as=image -
多路复用(MultiPlexing)
并发请求指的是建立多个链接通道,而不是在一个通道上进行多个HTTP请求
-
HTTP/1.0 每次请求响应,建立一个TCP连接,用完关闭
-
HTTP/1.1 「长连接」 ,但是,若干个请求排队串行化单线程处理,后面的请求等待前面请求的返回才能获得执行机会,一旦有某请求超时等,后续请求只能被阻塞,毫无办法,也就是人们常说的线头阻塞;
-
HTTP/2.0 「多路复用」多个请求可同时在一个连接上并行执行,某个请求任务耗时严重,不会影响到其它连接的正常执行;
-