当前位置: 首页 > 后端技术 > Node.js

HTTP缓存那些事

时间:2023-04-03 18:55:15 Node.js

阅读原序HTTP缓存机制是web应用性能优化的重要手段。准备技能。缓存的作用我们之所以使用缓存,是因为缓存可以为我们的web项目带来以下好处,提高性能和用户体验。它加快了浏览器的加载速度;减少冗余数据传输,节省网络流量和带宽;减轻了服务器的负担,大大提高了网站的性能。从本地缓存中读取静态资源可以加快浏览器网页的加载速度,确实可以减少数据传输。在提高网站性能方面,一两个用户的访问可能不会明显减轻服务器的负担。效果,但是如果网站在高并发下,使用缓存对于降低服务器压力和整个网站的性能会有质的改变。缓存规则介绍为了便于理解,我们认为浏览器有一个缓存数据库,用于存储缓存信息(实际上静态资源缓存在内存和磁盘中)。浏览器第一次请求数据时,缓存数据库中没有对应的缓存数据,需要向服务器请求,服务器返回缓存规则和数据,浏览器将缓存规则和数据存放在缓存数据库。在浏览器地址栏输入地址后请求的index.html不会被缓存,但index.html内部请求的其他资源会遵循缓存策略。HTTP缓存的规则有很多,主要是根据请求是否需要发送到服务器来划分的。有两类,强制缓存和协商缓存。强制缓存1.强制缓存流程强制缓存是指第一次访问服务器获取数据后,在有效时间内不再请求服务器,而是直接使用缓存的数据。强制缓存的过程如下。2.强制缓存判断过期时间那么如何判断缓存是否过期呢?实际上还是在第一次访问时根据服务器的响应头来实现的,这一点HTTP1.0和HTTP1.1是不一样的。在HTTP1.0版本中,服务器使用的响应头域是Expires,取值是一个未来的绝对时间(时间戳)。当浏览器请求的当前时间超过Expires设置的时间时,说明缓存失效,需要重新向服务器发送请求。否则,将直接从缓存数据库中获取数据。在HTTP1.1版本中,服务器使用的响应头域是Cache-Control,它有多个不同含义的值。private:客户端可以缓存;public:客户端和代理服务器都可以缓存(对于前端来说,可以认为和private一样);max-age=xxx:缓存的内容会在xxx秒后过期(相对时间,秒);no-cache:需要协商缓存(稍后介绍)来验证数据是否过期;no-store:不缓存所有内容,既不会触发强制缓存,也不会触发协商缓存。Cache-Control最常用的取值是max-age=xxx。缓存本身就是为了数据传输的优化和性能而存在的,所以几乎没有用到no-store。注意:在HTTP1.0版本中,Expires字段的绝对时间是从服务器获取的。由于请求需要时间,所以浏览器的请求时间和服务器收到请求时得到的时间有误差,也会导致缓存命中在HTTP1.1版本中,因为Cache-Control值中的xxxmax-age=xxx是以秒为单位的相对时间,浏览器收到资源后开始倒计时,避免了HTTP1.0中缓存命中错误的缺点。为了兼容低版本的HTTP协议,正常开发时会同时使用这两个响应头。HTTP1.1版本的实现优先级高于HTTP1.0。3.通过Network查看强制缓存我们的开发者通过Chrome浏览器工具,打开NetWork查看强制缓存的信息。以上是百度网站Logo图片的响应。我们可以清楚地看到它兼容HTTP1.0和HTTP1.1版本,并且使用强制缓存存储了10年。让我们看一下通过缓存检索到的数据与网络中其他资源的区别。实际上,缓存存储在内存和磁盘中。由当前浏览器自身的策略决定。它是相对随机的。从内存缓存中取出的数据会显示(frommemorycache),从磁盘缓存中取出的数据会显示。(来自磁盘缓存)。4.NodeJS服务器实现强制缓存//强制缓存consthttp=require("http");consturl=require("url");constpath=require("path");constmime=require("mime");constfs=require("fs");constserver=http.createServer((req,res)=>{let{pathname}=url.parse(req.url,true);pathname=pathname!=="/"?pathname:"/index.html";//获取读取文件的绝对路径letp=path.join(__dirname,pathname);//检查路径是否合法fs.access(p,err=>{//如果路径不合法,if(err)直接终止连接returnres.end("NotFound");//设置强制缓存res.setHeader("Expires",newDate(Date.now()+30000).toGMTString());res.setHeader("Cache-Control","max-age=30");//设置文件类型并响应浏览器res.setHeader("Content-Type",`${mime.getType(p)};charset=utf8`);fs.createReadStream(p).pipe(res);});});server.listen(3000,()=>{console.log(“服务器启动3000”);});上面mime模块的getType方法可以成功返回传入路径下文件对应的文件类型,比如text/html和application/javascript,都是第三方模块,使用前需要安装。npminstallmimenegotiationcache1.协商缓存过程协商缓存也叫比较缓存。设置好协商缓存后,在第一次访问服务器获取数据时,服务器会将数据和缓存ID返回给浏览器,客户端将数据和标识存入缓存数据库。下次请求时,会先去缓存中取出缓存标识,发送给服务器查询。当服务器数据发生变化时,标识会更新,因此服务器获取浏览器发送的标识进行比对。同样代表数据没有变化,响应浏览器通知数据没有变化,浏览器会去缓存中获取数据。如果identifier不一样,说明服务器改变了数据,所以会把新的数据和新的identifier返回给浏览器,浏览器会把新缓存的数据和identifier保存在缓存中,协商缓存的过程如下。协商缓存和强制缓存的区别在于,协商缓存每次请求都需要和服务器通信,命中缓存服务器返回的状态码不再是200,而是304。2.协商缓存判断强制缓存是通过过期时间来控制是否访问服务器,协商缓存每次都需要和服务器进行缓存标识比较。同样,协商缓存的实现在HTTP1.0和HTTP1.1之间是不同的。在HTTP1.0版本中,服务器通过Last-Modified响应头设置缓存标识,通常取请求数据的最后修改时间(绝对时间)为值,浏览器会接收返回的数据和标识并存储在缓存中,再次请求时会自动发送If-Modified-Since请求头,值为之前返回的最后一次修改时间(标识)。服务器获取If-Modified-Since的值,并将其与数据的最后修改时间进行比较。如果上次修改时间大于If-Modified-Since的值,说明已经被修改,则通过Last-Modified响应头返回新的上次修改时间和新数据,否则没有被修改,返回状态码304通知浏览器命中缓存。在HTTP1.1版本中,服务器通过Etag响应头设置缓存标识符(唯一标识符,类似指纹,生成规则由服务器决定),浏览器将数据和唯一标识符存储在缓存中接收数据。下次请求时,通过If-None-Match请求头将唯一标识带入服务器。服务器取出唯一标识符并与之前的标识符进行比较。如果不同,则表示已被修改,并返回新的标识符和数据。如果相同则返回状态码304,通知浏览器命中缓存。HTTP协商缓存策略的流程图如下:注意:HTTP1.0版本在使用协商缓存时仍然是不可靠的。假设一个文件在添加一个字符后被删除。作为修改,服务器应该命中缓存,但服务器重新发送了数据。因此,HTTP1.1中使用的Etag的唯一标识是根据文件的内容或摘要生成的,这保证了只要文件内容不变,就一定会命中缓存。兼容低版本HTTP协议,开发时会同时使用这两个响应头。同样,HTTP1.1的实现优先级高于HTTP1.0。3、通过Network查看协商缓存我们同样打开NetWork查看协商缓存的信息。再次请求服务器的请求头信息:打协商缓存的响应头信息:下面我们看一下Network中通过协商缓存取到的数据和第一次加载的区别。第一次请求:缓存后请求:对比两图可以发现,协商缓存生效时状态码为304,数据包大小和请求时间大大减少,因为服务器只返回了header部分,并通过状态码通知浏览器使用缓存,不再需要将消息正文部分返回给浏览器。4.NodeJS服务器实现协商缓存//协商缓存consthttp=require("http");consturl=require("url");constpath=require("path");constmime=require("mime");constfs=require("fs");0constcrytpo=require("crytpo");constserver=http.createServer((req,res)=>{let{pathname}=url.parse(req.url,true);pathname=pathname!=="/"?pathname:"/index.html";//获取读取文件的绝对路径letp=path.join(__dirname,pathname);//检查路径是否为legalfs.stat(p,(err,statObj)=>{//如果路径不合法,则直接终止连接if(err)returnres.end("NotFound");letmd5=crypto.createHash("md5");//创建加密转换流letrs=fs.createReadStream(p);//创建一个可读流//读取文件内容并加密rs.on("data",data=>md5.update(data));rs.on("end",()=>{letctime=statObj.ctime.toGMTString();//获取文件的最后修改时间letflag=md5.digest("hex");//获取加密后的唯一ID//获取协商缓存的请求头letifModifiedSince=req.headers["if-modified-since"];让ifNoneMatch=req.headers["if-none-match"];if(ifModifiedSince===ctime||ifNoneMatch===flag){res.statusCode=304;重发();}else{//设置协商缓存res.setHeader("Last-Modified",ctime);res.setHeader("Etag",flag);//设置文件类型并响应浏览器res.setHeader("Content-Type",`${mime.getType(p)};charset=utf8`);rs.管道(资源);}});});});server.listen(3000,()=>{console.log("serverstart3000");});上述代码中,通过可读流读取文件内容,并通过crypto模块进行md5加密后的结果作为唯一标识,从而保证只要文件内容不变,它将命中缓存,它与HTTP1.0和HTTP1.1版本兼容。只要满足一个,就直接返回304,通知浏览器命中缓存。注意:其实不建议对读取文件的内容进行加密。获取文件内容并进行md5加密的过程非常耗时,所以在开发中需要根据实际业务情况选择能够保证服务器性能的方式来生成唯一标识,比如基于摘要的文件。总结为了让缓存策略更加健壮和灵活,HTTP1.0和HTTP1.1的缓存策略会同时使用,甚至会同时使用强制缓存和协商缓存。对于强制缓存,服务器通知浏览器一个缓存时间。在下一个请求中,将直接使用缓存。如果超过有效时间,将执行协商的缓存策略。对于协商缓存,缓存信息中的Etag和Last-Modified会通过请求头If-None-Match和If-Modified-Since发送给服务端,同时服务端检查并设置新的强制缓存.当检查通过并返回304状态码时,浏览器直接使用缓存。如果协商缓存也失败,则服务器重置协商缓存的标识符。