关闭

转自张孝祥:web开发中的缓存问题的研究

标签: web开发浏览器服务器internettomcatservlet
1077人阅读 评论(0) 收藏 举报
分类:

一般情况下,浏览器都会缓存已经访问过的页面内容,关于如何禁止浏览器缓存的介绍,在网上到处都有相关的文章,但是,关于浏览器如何利用缓存,如何处理缓存的讲解,却鲜有人谈及.我一直为这个问题所困惑,这个问题也是绝大多数有经验的WEB开发人员所共同面临的问题,我有些朋友已做过几十个大大小小的WEB项目,当与他们交流这些问题时,他们虽然也在项目中遇到和解决过这些问题,但由于没有足够的时间和精力来仔细思考这些问题的原因和细节,他们对这些问题始终也是一知半解、含糊不清,而目前又很少关于这些问题的专门和详细讲解,我最近用了两天的时间,把浏览器缓存的问题透彻地研究了一翻,主要包括一下方面的细节.

1.如何禁止浏览器缓存,这是最简单的问题,本来羞于在此讲解,但是为了完整性,不妨将其列为一个知识点.

2.浏览器在访问已缓存过的资源时,它在什么情况下会向服务器发送请求?在什么情况下根本就不向服务器发送请求.这与浏览器的缓存设置有关!但是,由于几乎所有人的浏览器都是采用的默认设置,所以,重点应该放在分析浏览器的默认缓存设置的研究上.

3.当通过其他网页文档中的超链接来访问某一个已经缓存过的资源时,浏览器是否要向服务器发出访问请求?如果不发,则会出现一个问题:当销售一件商品后再回到商品库存的显示页面时,看到的还将是先前看到的内容,而不是更新的库存数据。但是,在访问一个普通的HTML文件时,如果浏览器每次都向服务器发送访问请求,效率就会相对低下,这就失去了缓存的意义和价值.所以,结论应是浏览器访问动态页面时不能使用缓存,而访问静态页面时应该使用缓存,但是,仅仅根据被访问页面的资源名称,浏览器是无法知道商品库存的显示页面是属于动态内容,还是属于静态内容。浏览器是根据什么方式来判断它缓存的资源是动态的,还是静态的呢?在什么请求下,它会对缓存的资源总是发出新的请求呢?

4.对于缓存的内容,即使浏览器向服务器发送了请求,但服务器在接收到请求后,可能不会返回内容,而是让浏览器继续使用缓存的内容,这在实际应用中有什么好处呢?如何处理其具体细节呢?

5.服务器端也有缓存,当服务器接收到浏览器的请求后,假设它返回响应内容,但返回的响应内容可能不是最新的内容,而很可能是一个旧的缓存版本,这又是怎么回事呢?

所有这些问题,在笔者的《深入体验java web开发内幕》一书中都有深刻的分析和详细的实验步骤.

以下是该书的节选(不包括服务器端缓存技术的讲解,服务器端缓存技术在其他章节有案例分析)

4.5.8 浏览器缓存内幕与getLastModified方法
  在HttpServlet类中定义了一个getLastModified方法,其完整语法定义如下:
      protected long getLastModified(HttpServletRequest req)
  其中的返回值表示自1970年1月1日的0点0分0秒开始计算的一个毫秒数,HttpServlet类中定义的getLastModified方法总是返回一个负数,在HttpServlet子类中可以对这个方法进行覆盖,以便返回一个代表当前输出的响应内容的修改时间,HttpServlet类的service方法可以根据这个返回值在响应消息中自动生成Last-Modified头字段。
  一般情况下,浏览器都会缓存已经访问过的页面内容,getLastModified方法的返回值可以影响浏览器如何处理和利用缓存内容。在详细了解getLastModified方法的应用之前,应该先对浏览器的缓存机制有所了解。单击IE浏览器的“工具”“Internet选项”菜单,打开“Internet选项”对话框,接着再单击“常规”选项卡中的“Internet临时文件”栏中的“设置”按钮,打开如图4.16所示的“设置”对话框。

图4.16

在图4.16所示的“设置”对话框的“Internet临时文件夹”栏中,可以看到浏览器保存所有缓存页面内容的文件夹的完整目录名称,对于Administrator用户来说,其默认的设置为“C:/Documents and Settings/Administrator/Local Settings/Temporary Internet Files”。单击“Internet临时文件夹”栏中的“查看文件”按钮,可以打开这个文件夹,如图4.17所示。.

4.17

从图4.16中还可以看到,其“检查所存网页的较新版本”功能项的设置值有4个选项,只要先单击“设置”对话框标题栏中的问号按钮,然后再单击相应的选项,就可以看到每个选项的作用和意义:

  • “每次访问此页时检查”选项表示浏览器每次访问一个页面时,不管浏览器是否缓存过此页面,都要向服务器发出访问请求。这种设置的优点是实时性很强,肯定能够访问到网页的最新内容,但是如果网页内容很少更新,这种设置的访问效率就比较低了。
  • “每次启动Internet Explorer时检查”选项表示在浏览器的每次启动运行期间,在第一次访问一个页面时,不管浏览器是否缓存过此页面,都要向服务器发出访问请求,但是在浏览器的本次启动运行期间对该页面的后续访问,浏览器将不再向服务器发出访问请求,而是直接使用缓存中的内容。这种设置具有较高的访问效率,同时也兼顾了较好的实时性,它可以保证每次启动浏览器后看到的都是最新的网页内容。
  • “自动”选项与“每次启动Internet Explorer时检查”选项的功能相似,只是对图像的访问有所不同,如果随着时间的推移,浏览器发现网页上的图像更新并不频繁,这样,即使浏览器在对某个已缓存的图像执行本次启动运行以来的第一次访问时,它也不一定会向服务器发出访问请求,而是干脆直接使用缓存中的内容。“自动”选项是浏览器的默认设置,所以,几乎所有人的浏览器都是按照这种方式工作的,这个选项的作用和意义应该成为读者熟悉的重点。
  • “不检查”选项表示浏览器不管在什么情况下访问一个页面时,只要能够在本地找到此页面的缓存信息,浏览器就不会向服务器发出访问请求,而是直接使用缓存的内容。这种设置的优点是访问效率很高,但是如果服务器端的网页内容更新后,浏览器看到的内容很可能是过期的内容。

    在浏览器的“检查所存网页的较新版本”的功能项采用默认的“自动”设置项的情况下,如果浏览器刚刚访问过一个网页,服务器端就更新了这个网页的内容,当浏览在关闭前又重新访问这个页面时,用户看到的将不是更新的网页内容,而是过期的网页内容。为了提高浏览效率,在访问静态的网页内容时,这么一点小概率的过期信息还是应该允许的,并且这些过期信息也不会造成什么不好的后果,就像你偶尔有一次看到了前一天发生的新闻,而不是当天的新闻,这又有什么问题呢?可是,如果浏览器访问的是一个动态网页,这本来就要求浏览器在其整个运行期间的每次访问都能看到最新的内容,例如,销售一件商品后再回到商品库存的显示页面时,看到的就应该是更新的库存数据,而不应该是先前看到的内容。仅仅根据被访问页面的资源名称,浏览器是无法知道商品库存的显示页面是属于动态内容,还是属于静态内容。对于这种情况,浏览器将根据响应消息中是否包含Last-Modified头字段来进行处理,如果响应消息中没有包含Last-Modified头字段,它将在每次访问此页面时都向服务器发出访问请求,否则,它仅在每次启动运行后的第一次访问此页面时才向服务器发出访问请求,而在启动运行期间对此页面的后续访问都不再向服务器发出访问请求。
    在第2章中曾经讲解过,响应消息中的Last-Modified头字段可用于指定响应内容的最后更新时间,当客户机缓存此文档内容后,它在以后的请求消息中将根据Last-Modified头字段指定的时间来生成If-Modified-Since请求头字段,以指出缓存文档的最后更新时间。只有文档的修改时间比If-Modified-Since请求头指定的时间新时,服务器才会返回文档内容。如果自从If-Modified-Since指定的时间以来,网页内容没有发生修改,服务器将返回一个304(Not Modified)状态码来表示浏览器缓存的版本是最新的,而不会向浏览器返回文档内容,浏览器则继续使用以前缓存的内容。通过这种方式,可以在一定程度上减少浏览器与服务器之间的通信数据量,从而提高了通信效率。
    HttpServlet类为If-Modified-Since请求头和Last-Modified头字段的这种应用提供了处理机制,当继承了HttpServlet类的Servlet程序接收到一个GET方式的访问请求时,HttpServlet中重载的service方法在调用doGet方法之前,它还将调用getLastModified方法,并根据getLastModified方法的返回值来决定是否调用doGet方法和在响应消息中是否生成Last-Modified头字段,具体规则如下:

  • 当getLastModified方法返回一个负数时,不管请求消息中的情况怎样,service方法都将直接调用doGet方法来生成响应内容,这正是HTTPServlet类中定义的getLastModified方法的行为;
  • 当getLastModified方法返回一个正数,且请求消息中没有包含If-Modified-Since请求头时(这往往出现在第对某个资源的第一次访问时),或者请求消息中包含的If-Modified-Since请求头中的时间值比getLastModified方法返回的时间值旧时,service方法将根据getLastModified方法的返回值生成一个Last-Modified头字段,然后调用doGet方法生成响应内容;
  • 当getLastModified方法返回一个正数时,且请求消息中包含的If-Modified-Since请求头中的时间值比getLastModified方法返回的时间值新或者与之相同时,service方法将不调用doGet方法,而是向浏览器返回一个304(Not Modified)状态码表示浏览器可以使用其以前缓存的内容。

动手体验:揭开浏览器缓存的奥秘

(1)编写一个名为CacheServlet的Servlet程序,在其doGet方法中向浏览器和Tomcat的命令行窗口中都打印出当前的时间值,getLastModified方法也是向Tomcat的命令行窗口中打印出当前的时间值和返回当前时间值,这里先将getLastModified方法注释掉,如例程4-9所示。

例程4-9  CacheServlet.java

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class CacheServlet extends HttpServlet
{
   public void doGet(HttpServletRequest request,
     HttpServletResponse response) throws ServletException, IOException
   {
      PrintWriter out = response.getWriter();
      long now = System.currentTimeMillis();
      out.println("doGet:" + now);
      System.out.println("doGet:" + now);
   }
 
    /*protected long getLastModified(HttpServletRequest req)
     {
        long now = System.currentTimeMillis();
        System.out.println("getLastModified:" + now);
        return now;
       }*/
}

编译CacheServlet.java文件,确保编译后生成的class文件放置进了d:/myweb/WEB-INF/classes目录中。

(2)在d:/myweb目录中编写一个名称为CacheTest.html的网页文件,如例程4-10所示。

例程4-10  CacheTest.html

<a href="servlet/CacheServlet">缓存测试</a>

(3)为了便于查看浏览器如何生成缓存内容,最好是先删除掉Internet临时文件夹中保存的所有缓存内容。单击IE浏览器的“工具”“Internet选项”菜单,打开如图4.16所示的“Internet选项”对话框,接着再单击“常规”选项卡中的“Internet临时文件”栏中的“删除文件”按钮,这就删除了Internet临时文件夹中保存的所有缓存内容。单击“常规”选项卡中的“Internet临时文件”栏中的“设置”按钮,在打开的“设置”对话框框中单击“Internet临时文件夹”栏中的“查看文件”按钮,打开如图4.17所示的Internet临时文件夹,可以看到其中已经不再有任何缓存的文件。另外,在“设置”对话框框中还需要将“检查所存网页的较新版本”的功能项恢复为默认的“自动”。
    在浏览器地址栏中输入如下地址:
         http://localhost:8080/it315/CacheTest.html
    在浏览器窗口中显示的结果页面中,单击“缓存测试”超链接访问CacheServlet。再次打开Internet临时文件夹,这时可以看到其中生成了刚才访问过的CacheTest.html和CacheServlet这两个页面的缓存文件,如图4.18所示。选中其中的CacheServlet缓存文件,从Windows资源管理器窗口中显示出的摘要信息中可以看到,CacheServlet缓存文件中记录的上次修改时间为“无”。

图4.18

(4)在命令行窗口中执行telnet 127.0.0.1 8080命令,连接上Tomcat WEB服务器后,接着在telnet程序命令窗口中,输入如下内容:
                    GET /it315/servlet/CacheServlet HTTP/1.1<回车>
                     Host:<空格><回车>
                     <回车>
    telnet窗口中显示出的结果如图4.19所示。

 图4.19

    从图4.19中可以看到,CacheServlet返回的响应消息中没有Last-Modified头字段,这正是图4.18中显示的CacheServlet缓存文件的上次修改时间为“无”的原因。

(5)回到浏览器窗口中,简要记住一下浏览器窗口和Tomcat的命令行窗口中显示出的时间值,然后反复单击浏览器工具栏中的“后退”和“前进”按钮,可以看到每次显示出的CacheServlet页面内容都没有变化,Tomcat的命令行窗口中也没有打印出新的信息。直接在浏览器地址栏中输入CacheServlet的访问地址,结果也是如此。这说明用浏览器的“后退”或“前进”方式访问已缓存的页面时,或者直接在浏览器地址栏中访问已缓存的页面时,浏览器将直接调用缓存的内容,而不向服务器发出新的访问请求。
    在图4.18所示的Windows资源管理器窗口中删除CacheServlet页面的缓存文件,再次使用“后退”和“前进”按钮访问CacheServlet,或者直接在浏览器地址栏中输入CacheServlet的访问地址,这时候浏览器窗口和Tomcat的命令行窗口中都将显示出新的时间值。这说明,在缓存文件不再存在的情况下,使用“后退”和“前进”按钮访问CacheServlet时,或者直接在浏览器地址栏中输入CacheServlet的访问地址,浏览器将向服务器发出新的访问请求。
    通过浏览器工具栏中的“后退”或“前进”按钮回到CacheTest.html页面,然后单击其中的超链接来访问CacheServlet,这时可以看到浏览器窗口和Tomcat的命令行窗口中显示出了新的时间值。重复这个过程,可以看到CacheServlet页面每次都能显示出新的时间值,这说明通过其他网页文档中的超链接来调用CacheServlet时,浏览器将向服务器发出新的访问请求,而不是调用已缓存的内容。
    (6)修改CacheServlet.java源文件,取消对getLastModified方法的注释,重新编译CacheServlet.java源文件,等待Tomcat重新装载CacheServlet后,在浏览器窗口中刷新对CacheServlet的访问。打开Internet临时文件夹(如果该文件夹原来已经打开,则按F5键刷新一下),选中其中的CacheServlet缓存文件,从Windows资源管理器窗口中显示出的摘要信息中可以看到,这次的CacheServlet缓存文件中有了一个上次修改时间的记录信息,如图4.20所示。

图4.20

(7)重复第(4)步的操作,这时候的telnet窗口中显示出的结果如图4.21所示。

图4.21

    从图4.21中可以看到,CacheServlet这次返回的响应消息中多了一个Last-Modified头字段,这正是图4.20中显示的CacheServlet缓存文件有了一个上次修改时间的记录信息的原因。
    (8)回到浏览器窗口中,简要记住一下浏览器窗口和Tomcat的命令行窗口中显示出的时间值,然后反复单击浏览器工具栏中的“后退”和“前进”按钮,可以看到每次显示出的CacheServlet页面内容都没有变化,Tomcat的命令行窗口中也没有打印出新的信息。直接在浏览器地址栏中输入CacheServlet的访问地址,结果也是如此。这种情况与在步骤(5)中看到的实验效果完全一样。
    通过浏览器工具栏中的“后退”或“前进”按钮回到CacheTest.html页面,单击其中的超链接来访问CacheServlet,这时看到浏览器窗口和Tomcat的命令行窗口中显示出的内容仍然没有变化,多次重复这个过程,看到的结果还是没有变化。这种情况与步骤(5)中的看到的实验效果截然不同,通过两者的实验效果的对比,我们可以得出如下结论:

  • 如果某个页面的响应消息中包含有Last-Modified头字段,当通过其他网页文档中的超链接来调用这个页面时,浏览器只在每次启动后的第一次访问这个页面时才向服务器发出访问请求,对于在这次启动运行期间对该页面的后续访问,浏览器将不再向服务器发出访问请求,而是直接调用缓存的内容。在访问一个普通的HTML文件时,Tomcat的缺省Servlet都会产生一个Last-Modified头字段来说明这个HTML文件的最新修改时间,因此,在浏览器的一次启动运行期间,它只在对某个HTML文件进行第一次访问时才向服务器发出真正的访问请求。对HTML文件的页面来说,它的内容正好在某个浏览器的某次运行期间的对该页面的第一次和后续访问之间发生改变的概率很小,并且即使发生这种情况,也不会出现什么大的问题,所以,Tomcat的缺省Servlet的这种处理方式完全是合理的和应该的。
  • 如果某个页面的响应消息中没有包含Last-Modified头字段,当通过其他网页文档中的超链接来调用这个页面时,浏览器在整个启动运行期间对该页面的每次访问,都将向服务器发出访问请求,而不会调用已缓存的内容。因此,只要动态网页程序中没有产生Last-Modified头字段,就不用担心通过超链接再次访问该动态网页程序时会出现浏览器不向服务器发生访问请求的问题。

    在浏览器窗口中刷新对CacheServlet的访问,刷新的作用就是让浏览器务必向服务器发出访问请求,这时候浏览器窗口中显示出了新的内容。查看启动Tomcat的命令行窗口,可以看到其中打印出类似如下的两行新信息:
        getLastModified:1147408274473
        doGet:1147408274473
    这说明CacheServlet调用了getLastModified方法后,接着又调用了doGet方法。因为getLastModified方法返回的是当前时间,它肯定比浏览器发出的请求消息中的If-Modified-Since请求头字段指定的时间值(即getLastModified方法上次被调用时返回的“当前时间”)要新,所以,CacheServlet调用完getLastModified方法后,接着又调用了doGet方法,于是在浏览器中就看到了新的内容。
    (9)修改CacheServlet.java源文件,让getLastModified方法中的最后那条return语句返回一个固定的时间值,如下所示:
         protected long getLastModified(HttpServletRequest req)
                 {
                  long now = System.currentTimeMillis();
                  System.out.println("getLastModified:" + now);
                  return /*now*/1234;
                 }
    重新编译CacheServlet.java源文件,等待Tomcat重新装载CacheServlet后,新启动一个浏览器程序访问CacheTest.html页面,然后单击CacheTest.html页面中的“缓存测试”超链接访问CacheServlet,这时显示出来的CacheServlet页面的内容还是在步骤(8)中的最后看到的内容,没有发生改变!查看启动Tomcat的命令行窗口,可以看到其中仅打印出了一行类似如下的新信息:
        getLastModified:1147414361586
    这个现象说明,浏览器刚才确实向服务器发出了访问请求。由于浏览器发出的请求消息中带有一个If-Modified-Since请求头字段,其设置值为上次的Last-Modified头字段指定的时间,而CacheServlet中的getLastModified方法这次返回的时间值比Last-Modified头字段指定的时间值更旧(在一般情况下应该是相等,这里为了简化程序的编写,仅仅是返回1234这个比较陈旧的时间值,但不影响实验效果),所以,CacheServlet没有调用doGet方法(Tomcat的命令行窗口中没有打印出doGet方法被调用的提示信息),而是向浏览器返回一个304(Not Modified)状态码来表示浏览器缓存的版本是最新的,从而导致浏览器仍旧显示出了上次缓存的内容。
    在浏览器窗口中刷新对CacheServlet的访问,这时浏览器中显示的内容依然如故,查看启动Tomcat的命令行窗口,可以看到其中又只是打印出了一行getLastModified方法被调用的提示信息。可见,如果浏览器缓存的某个页面的响应消息中包含有Last-Modified头字段,当浏览器再次向服务器发出针对该页面的访问请求时,只要处理这个页面的Servlet程序的getLastModified方法返回的时间值比上次的Last-Modified头字段的时间值旧或者与之相同时,服务器就不会返回新的内容。
    (10)重复第(7)步的telnet访问操作,等待服务器返回响应结果后,接着输入如下内容:
         GET /it315/servlet/CacheServlet HTTP/1.1<回车>
         Host:<空格><回车>
         If-Modified-Since:<空格>Thu, 01 Jan 1970 00:00:01 GMT
         <回车>
    其中的If-Modified-Since头字段中的值是从刚才返回的Last-Modified头字段中进行复制得到的,这时候可以看到服务器只返回了一个304(Not Modified)状态码,而没有返回任何实体内容。接着输入如下内容:
         GET /it315/servlet/CacheServlet HTTP/1.1<回车>
         Host:<空格><回车>
         If-Modified-Since:<空格>Thu, 01 Jan 1970 00:00:00 GMT
         <回车>
    注意,其中的If-Modified-Since头字段中的值比上一次的值小了1秒,这时候可以看到服务器返回了200状态码和新的实体内容,如图4.22所示。


0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:29233次
    • 积分:384
    • 等级:
    • 排名:千里之外
    • 原创:9篇
    • 转载:7篇
    • 译文:0篇
    • 评论:2条
    最新评论