PHP中通过Web执行C/C++应用程序

  一、简介
  
  如果你对Unix/Linux有所了解的话,你应该知道他们大都自带了C和C++的编译器,分别是GCC和G++。Unix在程序安装及Make等许多地方使用到了这些编译器。利用一些控制台命令,C++和PHP, 我将向你介绍怎样生成一个完整的C++程序例子,他可以在用PHP程序来执行,并能获得相应的输出结果。我将先生成C++程序代码,并编译它,谈后讨论我们将如果通过使用PHP的函数passthru来执行这个程序。从某种意义上来说,这边文章给我们提供一种通过Web页面来访问一般程序的方法。
  
  为了能更好的理解这篇文章,你应该有一台运行着apache和最新版本php的unix/Linux服务器。同时也应该掌握C++, unix控制台命令,当然一些PHP的编程经验也是必需的。
  
  二、编写一个C++程序
  
  例如,我们可以写一个能够通过命令行还接收参数的C++ 简单程序,并命名为Sampleapp.然后我们能够按照下面的方式给他传递三个不同的参数 :
  
  Sampleapp ?参数一 ?参数二 ?参数三
  
  这个程序的功能是能输出传递给他的参数的个数和每个参数的值,然后我们可以用PHP脚本程序来执行编译好的C++程序。
  
  利用你习惯的文本编辑器,新建一个名为Sampleapp.cpp 的文件,再此文件中输入如下的代码:
  
  #include <iostream.h>
  
  int main(int argc, char* argv[])
  {
  cout << endl << “You passed ” << argc-1 << ” arguement”
  << (argc-1 == 1 ? “” : “s”) << “.” << endl;
  
  cout << (argc-1 == 1 ? “This” : “These”)
  << ” arguement” << (argc-1 == 1 ? “” : “s”) << ” “
  << (argc-1 == 1 ? “is” : “are”) << “: ” << endl << endl;
  
  for(int i = 1; i < argc; i++)
  cout << “[” << i << “] ” << argv << endl;
  
  return 0;
  }




  这个C++程序包含的程序的入口点:main(),main()函数带了两个参数:argc(命令行传入参数的个数)和argv(一个包含了所传参数实际值的字符型指针数组)。这个两个参数能被C++编译器自动捕获。
  
  cout << endl << “You passed ” << argc-1 << ” arguement”
  << (argc-1 == 1 ? “” : “s”) << “.” << endl;;
  
  这句话的意思是获得从执行命令行传入的参数的个数。Argv这个字符型指针数组是从0开始检索的,它至少包含一个实际的值(即本程序的路径和名称),这个值由C++编译器自动地附加上去。条件操作符”?”是用来判断命令行传入地参数是否多于一个。例如,如果命令行过传入两个参数,我们地程序将输出如下信息:
  
  You passed 2 arguments.
  
  cout << (argc-1 == 1 ? “This” : “These”)
  << ” arguement” << (argc-1 == 1 ? “” : “s”) << ” “
  << (argc-1 == 1 ? “is” : “are”) << “: ” << endl << endl;
  
  接下来,我们同样用条件操作符来输出另一句话。不过要记住,即使我们不从程序执行命令行传入任何参数,main函数地argv[]参数也包含一个值。同样地,如果我们从命令行传入两个参数给程序,程序将输出如下地信息:
  
  These arguments are:
  
  for(int i = 1; i < argc; i++)
  cout << “[” << i << “] ” << argv << endl;
  
  最后,main函数逐一的输出命令行传入的每个参数,它用到了一个简单的for(;;)循环语句,这个函数能根据参数的个数将参数值一个一个的输出。假如我们传给程序两个参数”first”和second”, for循环输出的结果如下:
  
  [1] ?first
  [2] ?second
  
  以上是关于这个C++程序的简单说明,它的功能十分简单,就是将命令行传入的参数用cout函数显示在输出屏幕上。
  
  接下来,我们将编译这个.cpp文件,如果你在windows平台下,需要telnet到所使用的server上。在这里,我们使用大多Unix机器上都提供的G++编译器来编译这个源文件。不过为了确信你的机器安装了G++,你可以输入如下命令:which g++。如果G++已经安装了,Unix shell将显示出G++所在的全路径。如果没有安装,它将提示你说”command couldn’t be found”. 你可以在这里下载到G++.
  
  在源文件所在的目录输入如下G++命令:
  
  g++ -c sampleapp.cpp.
  通过这个命令,我们就将.cpp文件编译成了包含机器代码的目标文件。通过 ls ?a命令,你可以发现在本目录下出现了一个新文件sampleapp.o,这就是.cpp源文件被编译成机器码的结果。不过我们最终想要的是一个可执行文件,因为我们还要输入如下的G++命令:
  
  g++ sampleapp.cpp ?o sampleapp
  
  这样我们就获得了一个名为sampleapp的可执行文件。不过注意的是,Unix下的可执行文件跟Windows不一样,它没有任何后缀。
  
  下面我们可以来检验一下程序执行的结果,如果如下命令:
  
  sampleapp one -two /three
  我们可以看到如下的执行结果:
  
  You passed 3 arguments.
  These arguments are:
  
  [1] one
  [2] ?two
  [3] /three
  
  现在,可执行的C++程序成生完毕,下面我们将生成一个能够通过 web浏览器来访问这个程序的PHP教本程序。
  三、生成PHP脚本程序
  
  为了能通过Internet来调用我们的C++程序,我们需要生成一个PHP脚本程序。这个PHP脚本程序将有一个Form表单,以便用户能输入可以传给程序Sampleapp的参数。PHP脚本的代码太长就不在这里全部列出了,需要的话可以通过下面的地址来下载它。(Php code)
  
  if(@$submit)
  {
  
  }
  else
  {
  }
 
  首先,脚本程序检查看变量$submit是否有值,这个变量$submit的值是程序后面的Form表单提交后传递过来的,它缺省为空值。符号@的作用是当变量$submit的值不存在的时忽略相关的错误信息。
  
  由于变量$submit缺省为空,所以一开始执行else{}中的代码,它在浏览器上简单的显示一个Form表单。Form的action属性设为变量$PHP_SELF,即表单提交后返回本页。同时Form表单包含了一个文本输入条,这是用来让用户输入要传递给C++程序的命令行参数。Form如下图所示:
  
  一旦我们输入执行命令并提交表单,变量$submit(即按钮Go的名字)就获得一个值,这样PHP教本将执行if{}之间的代码。
  
  if($args == “”)
  echo “<h1>You didn’t enter any arguments.</h1>”;
  else
  {
  echo “<h1>SampleApp Result</h1>”;
  $command = “/htdocs/sampleapp ” . escapeshellcmd($args);
  
  passthru($command);
  }
  
  变量$args是自动产生的,它的值是Form表单中文本输入条传过来的值。如果没有输入任何信息,程序将简单的告诉用户没有输入任何值。
  
  如果用户输入任何非空的信息,程序将把text域的值,即变量$args传给C++程序。下面这段代码就是执行C++的程序的执行命令:
  
  $command = “/htdocs/sampleapp ” . escapeshellcmd($args);
  函数eacapeshellcmd是用来当做安全检查工具,以过滤调一些如”,”,””和”\”等的特殊字符。这可以防止一些用户企图输入某些字符来调用系统内部命令。
  
  例如,如果你在Form表单的text域中输入”1 ?two /three”,那么变量$command的值就为: /htdocs/sampleapp 1 ?two /three
  
  你能发现我们定义了程序sampleapp的全路径,在这个例子中,程序文件位于/htdocs目录下。你可以根据的自己程序所在的目录做相应的修改。
  
  passthru($command);
  
  最后,我们使用PHP的函数passthru来执行变量$command所包含的命令并且将原始的执行结果输出到浏览器上。在我的服务器上,返回结果的HTML页面如下:
  
  w在本文即将结束之前,几个可能碰到的问题我想说一下。首先,当你执行sampleapp.php教本程序的时候,如果你没有看到程序的任何输出信息,或许是开了安全模式。如果这样,系统将不会允许PHP脚本来执行系统内部程序。关于如何关闭安全模式,请访问网页http://www.php.net/manual/en/features.safe-mode.php,上面有详细的介绍。其次,在一些Unix系统上,PHP函数passthru不能将内部程序的输出传递给浏览页面,如果发生这种情况,可以用system函数来代替passthru函数。
  
  四、结论
  
  从本的例子可以看出,Unix操作系统非常强大,并且PHP允许开发者通过脚本以独立的线程来执行系统内部程序。本文的所给的例子非常的简单,但是只要再多花一点功夫,你可以写一个能更新Mysql数据库的c++程序,运行其他系统命令的程序或者是操作系统文件/目录结构的程序。但是,不管怎样,你都应该确保你的系统安全,绝对不能让任何其他的脚本程序随意访问系统内部程序。

关于表单输入域长度的问题

今天发现在LINUX使用FIREFOX浏览网站时,出现了表单的input和textarea比较长的情况,网页结构被整了乱七八糟的。

然而在WINDOWS下使用IE或者FIREFOX浏览却没有那种情况,而且好好的,挺喜欢的

后来经过反复测试发现原来在LINUX下和WINDOWS下对网页的表单输入域的长度是不一致的(恩,应该是这样,我是这么对自己解释的).

比如说input的size=”50″,在WINDOWS下只占估计250像素的长度,然而在LINUX下几乎占了整个页面的长度

这是一种比较夸张和超出逻辑的情况,我不知道这个差别是怎么出现的,总之我是找到了解决的办法,也就是在控制表单的输入域长度的时候尽量使用CSS,而不要使用SIZE属性,使用CSS使网页布局更加一致。

此外,文件域在这两个操作系统上的差别也很夸张,在LINUX上它是比较长的,而且控制起来也很麻烦,我基本上不给它设置什么CSS或者属性。

算了,不截图发上来了,记住以后控制表单输入域长度不要使用SIZE属性,要使用CSS(style=”width:300px;”)。

哎….这些差别是乍搞的呢?!嘿,还真验证了那么一句话:林子大了什么鸟都有。

不要使用opendir,readdir,closedir

上次写代码的时候发现了一个问题。

关于目录的访问我一般使用的结构是


但是当文件/目录名比较特殊时,比如说是0,这样$file的值为0,虽然它表示文件名,并不表示其他含义,但是程序可不认,它把它作为false离开了这次循环,所以当文件/目录名在boolean语法中表示false的意思时,这个访问结构是不成立的。(虽然我只对文件/目录名为0的情况进行了测试不成功,天知道还有什么其他的情况)


所以,应该使用scandir函数来访问目录,该结构可为:


这个结构本身就比上面那个执行效率高,而且绝对不会出问题。

escape,encodeURI,encodeURIComponent函数比较

js对文字进行编码涉及3个函数:escape,encodeURI,encodeURIComponent,相应3个解码函数:unescape,decodeURI,decodeURIComponent


1、  传递参数时需要使用encodeURIComponent,这样组合的url才不会被#等特殊字符截断。                           

例如:

<script language=”javascript”>document.write(‘<a href=”http://passport.baidu.com/?logout&aid=7&u=’+encodeURIComponent(“http://cang.baidu.com/bruce42″)+'”>退出</a>’);</script>


2、  进行url跳转时可以整体使用encodeURI

例如: Location.href=encodeURI(http://cang.baidu.com/do/s?word=中国asp之家&ct=21);


3、  js使用数据时可以使用escape

例如:搜藏中history纪录。


4、  escape对0-255以外的unicode值进行编码时输出%u****格式,其它情况下escape,encodeURI,encodeURIComponent编码结果相同。


最多使用的应为encodeURIComponent,它是将中文、韩文等特殊字符转换成utf-8格式的url编码,所以如果给后台传递参数需要使用encodeURIComponent时需要后台解码对utf-8支持(form中的编码方式和当前页面编码方式相同)


(中国asp之家注:就是说如果你的页面编码是gb2312的话,服务器端接收的将是乱码)


escape不编码字符有69个:*,+,-,.,/,@,_,0-9,a-z,A-Z

encodeURI不编码字符有82个:!,#,$,&,’,(,),*,+,,,-,.,/,:,;,=,?,@,_,~,0-9,a-z,A-Z

encodeURIComponent不编码字符有71个:!, ‘,(,),*,-,.,_,~,0-9,a-z,A-Z


附上这个三个函数的介绍:


escape 方法

对 String 对象编码以便它们能在所有计算机上可读,

escape(charString)
必选项 charstring 参数是要编码的任意 String 对象或文字。

说明
escape 方法返回一个包含了 charstring 内容的字符串值( Unicode 格式)。所有空格、标点、重音符号以及其他非 ASCII 字符都用 %xx 编码代替,其中 xx 等于表示该字符的十六进制数。例如,空格返回的是 “%20” 。

字符值大于 255 的以 %uxxxx 格式存储。

注意   escape 方法不能够用来对统一资源标示码 (URI) 进行编码。对其编码应使用 encodeURI 和encodeURIComponent 方法。



encodeURI 方法

将文本字符串编码为一个有效的统一资源标识符 (URI)。

encodeURI(URIString)
必选的 URIString 参数代表一个已编码的 URI。

说明
encodeURI 方法返回一个编码的 URI。如果您将编码结果传递给 decodeURI,那么将返回初始的字符串。encodeURI 方法不会对下列字符进行编码:”:”、”/”、”;” 和 “?”。请使用 encodeURIComponent 方法对这些字符进行编码。



encodeURIComponent 方法

将文本字符串编码为一个统一资源标识符 (URI) 的一个有效组件。

encodeURIComponent(encodedURIString)
必选的 encodedURIString 参数代表一个已编码的 URI 组件。

说明

encodeURIComponent 方法返回一个已编码的 URI。如果您将编码结果传递给 decodeURIComponent,那么将返回初始的字符串。因为 encodeURIComponent 方法对所有的字符编码,请注意,如果该字符串代表一个路径,例如 /folder1/folder2/default.html,其中的斜杠也将被编码。这样一来,当该编码结果被作为请求发送到 web 服务器时将是无效的。如果字符串中包含不止一个 URI 组件,请使用 encodeURI 方法进行

Apache2 使用 mod_gzip 增进传输效能

要加快网页传输的速度,一个方法是增加网络的频宽,另一个方法是降低数据量,这是大家都知道的道理. HTTP 通讯协议基本上就是 server 接受 client 的 request (GET) 然后送出被要求的资料,如果 server 端可以送出一份压缩过的数据而可以被 client 端接受,那么虽然是传送相同的资料,但是经过压缩就可以有效的降低数据量.
本站使用 Win32 的 Apache 2.0.54,如果想要达成上面讲的机制,要另加一个 gzip 的模块 mod_gzip,这里可以下载 mod_gzip 预先编译好的版本 mod_gzip-2.0.53-w32.zip,用在 Apache 2.0.54 没问题. 用法如下:
在 httpd.conf 加上

LoadModule gzip_module modules/mod_gzip.so
<ifmodule mod_gzip.c>
mod_gzip_on Yes
mod_gzip_compression_level 9
mod_gzip_item_include file \.css$
mod_gzip_item_include file \.js$
mod_gzip_item_include mime text/.*
</ifmodule>

其中 mod_gzip_compression_level 9 是最高压缩,mod_gzip_item_include 是设定什么样的数据文件要压缩,上例中 *.css 及 *.js 会被压缩传送. 图档如 .jpg, .png,或音乐 .mp3 等,则不宜再压缩,因为档案本身就已经是压缩过的.
修改完后要 restart Apache. Apache 如果接到 browser 送来的 Accept-Encoding 含有 gzip 就会以 gzip 压缩再传送(Accept-Encoding: gzip,deflate ),现在新的 browser 都已经支持. 如果 browser 没有送来支持的讯息,server 端就会以不压缩的原数据传送.

实验结果,本来 style.css 这个档是 12911 bytes,压缩传送是 3342 bytes,省了近 10k 的传输量,意谓者使用者等待的时间缩短了.

Apache2 官方包装内含有 mod_deflate,是一样的功能,用法不一样.

Apache2 使用 mod_deflate 增进传输效能

在前篇提过使用 mod_gzip 增进传输效能,但似乎有些地方有问题,如果加上mod_gzip_item_include mime application/x-httpd-php,照理说应该会把 php 的输出压缩传出,但是在 browser 看到的是空白一片. 只好试试 Apache2 官方包装内的 mod_deflate,结果没有出现同样的问题,官方版还是比较有保障.
用法如下:
在 httpd.conf 加上

LoadModule deflate_module modules/mod_deflate.so
<ifmodule mod_deflate.c>
DeflateCompressionLevel 9
AddOutputFilterByType DEFLATE text/html text/plain text/xml application/x-httpd-php
AddOutputFilter DEFLATE css js
</ifmodule>

其中 DeflateCompressionLevel 9 是最高压缩,经实验证明,压缩能力跟 mod_gzip 是一样的,style.css 12911 bytes,压缩传送同样是 3342 bytes.
Apache2 还是推荐使用 mod_deflate 较好.

使用Apache2的mod_deflate模块

今天看了精神奕奕的两篇文章:《Apache2 使用 mod_gzip 增进传输效能》《Apache2 使用 mod_deflate 增进传输效能》。看起来这是位WordPress前辈啊,05年初就开始接触了WordPress。

文章介绍了使用mod_gzip模块和mod_deflate模块压缩传输数据,提供传输性能的方法。WordPress本身支持gzip压缩输入,不过仅限于WordPress输出的页面,比如Blog页面以及WordPress后台页面等等。而其他地方,比如模版的js文件和CSS文件,因为这些文件不用被PHP引擎执行,没经过WordPress的处理,所以没有压缩。不过,K2模版的几个js是特例,因为他们都是做成PHP文件,包含了wp-blog-header.php文件,并在文件启动了gzip压缩。

我的服务器安装了mod_deflate模块,到Apache 2.2 文档里看了一下,找到mod_deflate模块的文档。精神奕奕的文章里说的DeflateCompressionLevel等命令都不能在.htaccess文件里使用,只能用AddOutputFilter命令[via]。于是在.htaccess文件里加上:

 

<ifmodule mod_deflate.c>

AddOutputFilter DEFLATE css js txt

</ifmodule>


这样,当传输以css,js,txt为扩展名的文件时,Apache将使用mod_deflate模块对其进行压缩后再传输。这三种文件都是文本格式的,压缩起来效果明显。还有html和xml文件也可以压缩,不过我的网站上基本没有这两种文件。Apache应该是按照最终要读取的文件的扩展名来判断的,刚才关了WordPress的gzip压缩,然后加上了在.htaccess文件里加上了html,发现我的Blog文章页面并没有被压缩。而这种压缩是在PHP等脚本解析程序执行完了之后才进行的,如果WordPress没有gzip压缩功能,倒是可以利用mod_deflate来对页面进行压缩,hoho

测试了一下,K2自带的style.css文件,未启用前Content-Length是15914字节,跟文件大小一致,启用后Content-Length只有4415。WordPress 2.0.5的/wp-admin/wp-admin.css文件,原来大小是15047字节,启用DEFLATE后Content-Length只有3990。

这个效果还是很明显的,对于网络传输速度不太好的服务器还是有点用的。而默认压缩等级没法调,因为我用的是虚拟主机。现在也不知道该怎么看服务器的默认压缩等级,不过看精神奕奕的效果:启用前12911字节,启用后3342字节,大概效果也差不多。

Update:今天找DreamHost的资料,偶然间看到这篇《PHP中HTTP方式下的Gzip压缩传输方法举偶》说,在.htaccess文件里可以打开PHP的gzip压缩输入,方法是在.htaccess文件里加入下面两行:

 

php_flag zlib.output_compression on

php_value zlib.output_compression_level 2


这样不错,等于是给每个PHP程序开头加上了ob_start(“ob_gzhandler”);这条语句。查了一下,WordPress也是利用ob_start()函数来启动gzip压缩的。

翻了一下PHP文档-ob_gzhandler,zlib.output_compression和ob_gzhandler还是有区别的。zlib.output_compression是和PHP脚本解析程序并行的一个线程,当PHP输入时,这边读入,压缩,而已经压缩好的文档达到一定数量(默认是4k),它就向浏览器发送数据。而ob_gzhandler则是在PHP脚本执行完所有代码才把缓存好的输出文件进行压缩并传输给浏览器,所以相对慢一点,不过使用它可以在PHP程序里控制一些参数,比如压缩等级等等。.htaccess里用php_value zlib.output_compression 2048语句可以设置输出数据达到2k就传输。

压缩等级的调整有两种说法,不过懒得研究了,默认等级是6,890k的文件压缩出来的数据和最高的9级只差1k,而且9级需要更多的CPU时间,用默认的6级就可以了。

另外,有个Real-Time Compression Check工具,可以检测某个URL是否是压缩传输的。好像这家公司做IIS下的zip压缩程序,所以弄了这么个在线工具。只要输入你的地址,回车,就可以看到服务器类型、是否是压缩传输以及压缩前和压缩后的大小比较。大多数情况下html代码都能压缩到1/4~1/3,gzip压缩传输还是很不错的。

服务器网页缓存的深入分析

Expires、Cache-Control、Last-Modified、ETag是RFC 2616(HTTP/1.1)协议中和网页缓存相关的几个字段。前两个用来控制缓存的失效日期,后两个用来验证网页的有效性。要注意的是, HTTP/1.0有一个功能比较弱的缓存控制机制:Pragma,使用HTTP/1.0的缓存将忽略Expires和Cache-Control头。我们 这里以Apache2.0服务器为例,只讨论HTTP/1.1协议。

Expires
    Expires字段声明了一个网页或URL地址不再被浏览器缓存的时间,一旦超过了这个时间,浏览器都应该联系原始服务器。RFC告诉我们:“由于推断的失效时间也许会降低语义透明度,应该被谨慎使用,同时我们鼓励原始服务器尽可能提供确切的失效时间。”

    对于一般的纯静态页面,如html、gif、jpg、css、js,默认安装的Apache服务器,不会在响应头添加这个字段。Firefox浏览器接受 到相应后,如果发现没有Expires字段,浏览器根据文件的类型和“Last-Modified”字段来推断出一个合适的失效时间,并存储在客户端。推 测出的时间一般是接受到响应时间后的三天左右。

    Apache的expires_module模块可以在Http响应头部自动加上Expires字段。在Apache的httpd.conf文件中进行如下配置:

#启用expires_module模块
LoadModule expires_module modules/mod_expires.so
# 启用有效期控制
ExpiresActive On
# GIF有效期为1个月
ExpiresByType image/gif A2592000
# HTML文档的有效期是最后修改时刻后的一星期
ExpiresByType text/html M604800
#以下的含义类似
ExpiresByType text/css “now plus 2 month”
ExpiresByType text/js “now plus 2 day”
ExpiresByType image/jpeg “access plus 2 month”
ExpiresByType image/bmp “access plus 2 month”
ExpiresByType image/x-icon “access plus 2 month”
ExpiresByType image/png “access plus 2 month”



对于动态页面,如果在页面内部没有通过函数强制加上Expires,例如header(”Expires: ” . gmdate(”D, d M Y H:i:s”) . ” GMT”),Apache服务器会把Wed, 11 Jan 1984 05:00:00 GMT作为Expires字段内容,返回给浏览器。即认为动态页面总是失效的。而浏览器仍然会保存已经失效的动态页面。
可以发现Firefox浏览器总是缓存所有页面,不管失效、不失效还是没有声明失效时间。即使缓存中声明了一个网页的实效日期是 1970-01- 01 08:00:00,浏览器仍然会发送该文件在缓存中的Last-Modified和ETag字段。如果在服务器端验证通过,返回304状态,浏览器就还会 使用此缓存。

Cache-Control
    Cache-Control字段中可以声明多些元素,例如no-cache, must-revalidate, max-age=0等。这些元素用来指明页面被缓存最大时限,如何被缓存的,如何被转换到另一个不同的媒介,以及如何被存放在持久媒介中的。但是任何一个 Cache-Control指令都不能保证隐私性或者数据的安全性。“private”和“no-store”指令可以为隐私性和安全性方面提供一些帮 助,但是他们并不能用于替代身份验证和加密。
     Apache的mod_cern_meta模块允许文件级Http响应头部的控制,同时它也可以配置Cache-Control头(或任何其他头)。响应 头文件是放在原始目录的子目录中,根据原始文件名所命名的一个文件。具体用法请参阅Apache的官方网站。其中Cache-Control : max-age表示失效日期。如果没有启动mod_cern_meta模块,Apache服务器会把Expires字段中的日期换算成以秒为单位的一个 delta值,赋值给max-age。如果启动mod_cern_meta模块,并且配置了max-age值,Apache会将这个覆盖Expires字 段。同时,max-age隐含了Canche-Control: public。这样浏览器接受到的Cache-Control : max-age和Expires值就是一致的。
    如果失效日期Cache-Control : max-ag=0或者是负值,浏览器会在对应的缓存中把Expires设置为1970-01-01 08:00:00。

Last-Modified
    Last-Modified和ETag是条件请求(Conditional Request)相关的两个字段。如果一个缓存收到了针对一个页面的请求,它发送一个验证请求询问服务器页面是否已经更改,在HTTP头里面带上” ETag”和”If Modify Since”头。服务器根据这些信息判断是否有更新信息,如果没有,就返回HTTP 304(NotModify);如果有更新,返回HTTP 200和更新的页面内容,并且携带新的”ETag”和”LastModified”。
    使用这个机制,能够避免重复发送文件给浏览器,不过仍然会产生一个HTTP请求。
一般纯静态页面本身都会有Last-Modified信息,Apache服务器会读取页面文件中的Last-Modified信息,并添加到http响应头部。
    对于动态页面,如果在页面内部没有通过函数强制加上Last-Modified,例如header(”Last-Modified: ” . gmdate(”D, d M Y H:i:s”) . ” GMT”),Apache服务器会把当前时间作为Last-Modified,返回给浏览器。
   无论是纯静态页面还是动态页面,Firefox浏览器巧妙地按照接受到服务器响应的时间设置缓存页面的Last-Modified,而不是按照http响应头部中的Last-Modified字段。

ETag
   既然有了Last-Modified,为什么还要用ETag字段呢?因为如果在一秒钟之内对一个文件进行两次更改,Last-Modified就会不正确。因此,HTTP/1.1利用Entity Tag头提供了更加严格的验证。
    Apache服务器默认情况下,会对所有的静态、动态文件的响应头添加ETag字段。在Apache的httpd.conf文件中可以通过FileETag指令配置该选项。
   FileETag指令配置了当文档是基于一个文件时用以创建 Etag(entity tag)响应头的文件的属性。在Apache 1.3.22及以前,ETag的值是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的。如果一个目录的 配置包含了‘FileETag INode MTime Size’而其一个子目录包含了‘FileETag -INode’那么这个子目录的设置(并会被其下任何没有进行覆盖的子目录继承)将等价于‘FileETag MTime Size’。
    在多台负载平衡的服务器环境下,同一个文件会有不同的etag或者文件修改日期,浏览器每次都会重新下载。设置‘FileETag None’可以使响应头不再包含ETag字段。






三种典型web服务器Header设置内容过期方法

一、Internet 信息服务 (IIS)的内容过期设置
如果IIS 网站中有时间敏感信息,可以配置设置来保证过期信息不被代理服务器或 Web 浏览器缓存。可以配置网站内容,使之在任
何的时间自动过期。当启用内容过期时,Web 浏览器将比较当前日期和截止日期,以便决定是显示缓存页还是从服务器请求更新的页
。Microsoft ASP.NET 这样的服务器端技术可用于动态更改提供的内容。通常,时间敏感信息只限于单个文件、目录或网站;不过,您也可以为某台计算机上的所有网站设置内容过期。
必须是本地计算机上 Administrators 组的成员或者必须被委派了相应的权限,才能执行下列步骤。作为安全性的最佳操作,请使用不属于 Administrators 组的帐户登录计算机,然后使用运行方式命令以管理员身份运行 IIS管理器
在命令提示符下,键入 runas /user:administrative_accountname “mmc %systemroot%\system32\inetsrv\iis.msc”。
设置网站内容的过期时间
1. 在 IIS 管理器中,展开本地计算机;右键单击要设置内容过期的网站、虚拟目录或文件,然后单击“属性”。
2. 单击“HTTP 头”选项卡。
3. 选中“启用内容过期”复选框。
4. 单击“立即过期”、“此时间段后过期”或“过期时间”,然后在对应的框中输入所需的过期信息。
5. 单击“确定”。


二、APACHE服务的内容过期设置
Apache配置摘录及解释
i. 过期相关设置

LoadModule headers_module modules/mod_headers.so
#Load 修改header的模块。
LoadModule expires_module modules/mod_expires.so
#Load 设定过期header的模块。
Header append Via: CCN-BJ-4-502
#增加一个Via header,值配置成设备的hostname。
KeepAliveTimeout 60
#设置连接的保持时间为60秒。
ExpiresActive On
#启用过期header功能。
ExpiresDefault A604800
#缺省过期时间为“访问后的604800秒”
<Directory /data/download>
Options FollowSymLinks
AllowOverride None
Order allow,deny
Allow from all
ExpiresByType text/html A300
#text/html类型文件的过期设置为“访问后的300秒”
ExpiresByType text/css A259200
#text/css类型文件的过期设置为“访问后的259200秒”
ExpiresByType application/x-javascript A300
# application/x-javascript类型文件的过期设置为“访问后的300秒”
ExpiresByType image/gif A2592000
#image/gif类型文件的过期设置为“访问后的2592000秒”
ExpiresByType application/x-shockwave-flash A2592000
# application/x-shockwave-flash类型文件的过期设置为“访问后的2592000秒”
</Directory>





上述配置文件中load的两个模块:mod_headers.so 和mod_expires.so 可以让Apache具有对header的一些定制功能。

ExpiresByType:     表示按照文件类型-MIME-TYPE设定过期策略;
A300: 表示在Access后300秒后过期;
ExpiresByType text/css A2592000:    表示Mime type是text/css的文件,在Access后2592000秒过期。
ExpiresDefault A604800: 表示除了单独制定的文件类型等过期策略外的其他内容,按照这个缺省的策略设定:访问后604800秒过期。

上面的方法可以实现根据web发布的不同文件类型,针对不同的发布目录进行过期策略设置。在按照如上方法设置后,Apache会自动的产生两个相关的http header,举例如下:



HTTP/1.1 200 OK
Date: Tue, 27 Mar 2007 17:44:21 GMT
Server: Apache/2.0.54 (Unix)
Last-Modified: Thu, 25 Jan 2007 07:45:45 GMT
ETag: “72df3a-93-99499c40”
Accept-Ranges: bytes
Content-Length: 147
Cache-Control: max-age=2592000
Expires: Thu, 26 Apr 2007 17:44:21 GMT
Via: CCN-BJ-4-575
Keep-Alive: timeout=60, max=100
Connection: Keep-Alive
Content-Type: image/gif
Length: 147 [image/gif]

其中:Date + Max-age = Expires. Max-age是个时间长度,对应web server上面设置的过期时间;Expires是根据max-age算出来的过期时间点,两者是一致的,不同cache在判断内容是否过期时会严格比较 系统时间和上述过期时间,或者比较age(在cache中存住的时间长度)和max-age的值。




三、lighttpd
lighttpd默认是没有开启expire模块的,需要我们在使用是手工开启这个模块支持。把mod_expire前面的“#”号去掉。
# vi /usr/local/lighttpd/etc/lighttpd.conf
                                “mod_expire”,
设定过期时间
    * 设定指定url的过期时间:

expire.url = (
       “/images/” => “access 3 hours”,
       “/admin/” => “access 3 hours”,
       “/area/” => “access 3 hours”,
       “/calendar/” => “access 3 hours”,
       “/common/” => “access 3 hours”,
       “/front/” => “access 3 hours”,
       “/inc/” => “access 3 hours”,
       “/jeditor/” => “access 3 hours”,
       “/js/” => “access 3 hours”,
       “/script/” => “access 3 hours”,
       “/theme/” => “access 3 hours”,
       “/upload/” => “access 3 hours”,
       “/view/” => “access 3 hours”,
       “/help/” => “access 3 hours”,
       “/htm/” => “access 5 minutes”
       )

设置Etag
在配置文件中增加,etag.use-inode=”disable”(i节点不参与etag的运算),保证多台服务器生成的Etag值一致。

如何提高Web页面的性能

1. 尽可能的减少 HTTP 的请求数 [content]
2. 使用 CDN(Content Delivery Network) [server]
3. 添加 Expires 头(或者 Cache-control ) [server]
4. Gzip 组件 [server]
5. 将 CSS 样式放在页面的上方 [css]
6. 将脚本移动到底部(包括内联的) [javascript]
7. 避免使用 CSS 中的 Expressions [css]
8. 将 JavaScript 和 CSS 独立成外部文件 [javascript] [css]
9. 减少 DNS 查询 [content]
10. 压缩 JavaScript 和 CSS (包括内联的) [javascript] [css]
11. 避免重定向 [server]
12. 移除重复的脚本 [javascript]
13. 配置实体标签(ETags) [css]
14. 使 AJAX 缓存 [content]
15. 尽早清除缓冲区 [server]
16. AJAX 请求使用“GET”方法 [server]
17. 延迟加载组件 [content]
18. 预加载组件 [content]
19. 减少 DOM 元素的数量 [content]
20. 跨域分离组件 [content]
21. 减少 iframes 的数量 [content]
22. 不出现 404 [content]
23. 减小 cookie 的体积 [cookie]
24. 为组件使用 cookie-free 的域名 [cookie]
25.减少访问 DOM 的次数 [javascript]
26. 开发巧妙的事件处理程序 [javascript]
27. 优先选择使用 <link> 而非 @import [css]
28. 避免使用 filters [css]
29. 优化图片 [images]
30. 优化 CSS sprites [images] DL@bitsCN_com网管软件下载
32. 不要在 HTML 中缩放图片 [images]
33. 减小 favicon.ico 的体积并缓存 [images]
34. 保持组件在 25K 以下 [mobile]
35. 将组件分拆到多个文档中 [mobile]

面向站长和网站管理员的Web缓存加速指南


这是一篇知识性的文档,主要目的是为了让Web缓存相关概念更容易被开发者理解并应用于实际的应用环境中。为了简要起见,某些实现方面的细节被简化或省略了。如果你更关心细节实现则完全不必耐心看完本文,后面参考文档和更多深入阅读部分可能是你更需要的内容。


  1. 什么是Web缓存,为什么要使用它?
  2. 缓存的类型:

    1. 浏览器缓存;
    2. 代理服务器缓存;

  3. Web缓存无害吗?为什么要鼓励缓存?
  4. Web缓存如何工作:
  5. 如何控制(控制不)缓存:

    1. HTML Meta标签 vs. HTTP头信息;
    2. Pragma HTTP头信息(为什么不起作用);
    3. 使用Expires(过期时间)HTTP头信息控制保鲜期;
    4. Cache-Control(缓存控制) HTTP头信息;
    5. 校验参数和校验;

  6. 创建利于缓存网站的窍门;
  7. 编写利于缓存的脚本;
  8. 常见问题解答;
  9. 缓存机制的实现:Web服务器端配置;
  10. 缓存机制的实现:服务器端脚本;
  11. 参考文档和深入阅读;
  12. 关于本文档;


什么是Web缓存,为什么要使用它?

Web 缓存位于Web服务器之间(1个或多个,内容源服务器)和客户端之间(1个或多个):缓存会根据进来的请求保存输出内容的副本,例如html页面, 图片,文件(统称为副本),然后,当下一个请求来到的时候:如果是相同的URL,缓存直接使用副本响应访问请求,而不是向源服务器再次发送请求。

使用缓存主要有2大理由:


  • 减少相应延迟:因为请求从缓存服务器(离客户端更近)而不是源服务器被相应,这个过程耗时更少,让web服务器看上去相应更快;
  • 减少网络带宽消耗:当副本被重用时会减低客户端的带宽消耗;客户可以节省带宽费用,控制带宽的需求的增长并更易于管理。

缓存的类型


浏览器缓存


对 于新一代的Web浏览器来说(例如:IE,Firefox):一般都能在设置对话框中发现关于缓存的设置,通过在你的电脑上僻处一块硬盘空间用于存储你已 经看过的网站的副本。浏览器缓存根据非常简单的规则进行工作:在同一个会话过程中(在当前浏览器没有被关闭之前)会检查一次并确定缓存的副本足够新。这个 缓存对于用户点击“后退”或者点击刚访问过的链接特别有用,如果你浏览过程中访问到同一个图片,这些图片可以从浏览器缓存中调出而即时显现。

代理服务器缓存


Web代理服务器使用同样的缓存原理,只是规模更大。代理服务器群为成百上千用户服务使用同样的机制;大公司和ISP经常在他们的防火墙上假设代理缓存或者单独的缓存设备;

由 于带路服务器缓存并非客户端或者源服务器的一部分,而是位于原网络之外,请求必须路由到他们才能起作用。一个方法是手工设置你的浏览器:告诉浏览器使用 那个代理,另外一个是通过中间服务器:这个中间服务器处理所有的web请求,并将请求转发到后台网络,而用户不必配置代理,甚至不必知道代理的存在;

代理服务器缓存:是一个共享缓存,不只为一个用户服务,经常为大量用户使用,因此在减少相应时间和带宽使用方面很有效:因为同一个副本会被重用多次。

网关缓存


也被称为反向代理缓存或间接代理缓存,网关缓存也是一个中间服务器,和内网管理员部署缓存用于节省带宽不同:网关缓存一般是网站管理员自己部署:让他们的网站更容易扩展并获得更好的性能;
请求有几种方法被路由到网关缓存服务器上:其中典型的是让用一台或多台负载均衡服务器从客户端看上去是源服务器;

网络内容发布商 (Content delivery networks CDNs)分布网关缓存到整个(或部分)互联网上,并出售缓存服务给需要的网站,Speedera和Akamai就是典型的网络内容发布商(下文简称CDN)。

本问主要关注于浏览器和代理缓存,当然,有些信息对于网关缓存也同样有效;

Web缓存无害吗?为什么要鼓励缓存?


Web缓存在互联网上最容易被误解的技术之一:网站管理员经常怕对网站失去控制,由于代理缓存会“隐藏”他们的用户,让他们感觉难以监控谁在使用他们的网站。
不幸的是:就算不考虑Web缓存,互联网上也有很多网站使用非常多的参数以便管理员精确地跟踪用户如何使用他们的网站;如果这类问题也是你关心的,本文将告诉你如何获得精确的统计而不必将网站设计的非常缓存不友好。
另外一个抱怨是缓存会给用户过期或失效的数据;无论如何:本文可以告诉你怎样配置你的服务器来控制你的内容将被如何缓存。

CDN是另外一个有趣的方向,和其他代理缓存不同:CDN的网关缓存为希望被缓存的网站服务,没有以上顾虑。即使你使用了CDN,你也要考虑后续的代理服务器缓存和浏览器缓存问题。

另外一方面:如果良好地规划了你的网站,缓存会有助于网站服务更快,并节省服务器负载和互联网的链接请求。这个改善是显著的:一个难以缓存的网站可能需要几秒去载入页面,而对比有缓存的网站页面几乎是即时显现:用户更喜欢速度快的网站并更经常的访问;

这样想:很多大型互联网公司为全世界服务器群投入上百万资金,为的就是让用户访问尽可能快,客户端缓存也是这个目的,只不过更靠近用户一端,而且最好的一点是你甚至根本不用为此付费。

事实上,无论你是否喜欢,代理服务器和浏览器都回启用缓存。如果你没有配置网站正确的缓存,他们会按照缺省或者缓存管理员的策略进行缓存。

缓存如何工作


所有的缓存都用一套规则来帮助他们决定什么时候使用缓存中的副本提供服务(假设有副本可用的情况下);一些规则在协议中有定义(HTTP协议1.0和1.1),一些规则由缓存的管理员设置(浏览器的用户或者代理服务器的管理员);
一般说来:遵循以下基本的规则(不必担心,你不必知道所有的细节,细节将随后说明)


  1. 如果响应头信息:告诉缓存器不要保留缓存,缓存器就不会缓存相应内容;
  2. 如果请求信息是需要认证或者安全加密的,相应内容也不会被缓存;
  3. 如果在回应中不存在校验器(ETag或者Last-Modified头信息),缓存服务器会认为缺乏直接的更新度信息,内容将会被认为不可缓存。
  4. 一个缓存的副本如果含有以下信息:内容将会被认为是足够新的

    • 含有完整的过期时间和寿命控制头信息,并且内容仍在保鲜期内;
    • 浏览器已经使用过缓存副本,并且在一个会话中已经检查过内容的新鲜度;
    • 缓存代理服务器近期内已经使用过缓存副本,并且内容的最后更新时间在上次使用期之前;
    • 够新的副本将直接从缓存中送出,而不会向源服务器发送请求;

  5. 如果缓存的副本已经太旧了,缓存服务器将向源服务器发出请求校验请求,用于确定是否可以继续使用当前拷贝继续服务;
总之:新鲜度和校验是确定内容是否可用的最重要途径:
 

如果副本足够新,从缓存中提取就立刻能用了;
而经缓存器校验后发现副本的原件没有变化,系统也会避免将副本内容从源服务器整个重新传输一遍。

如何控制(控制不)缓存


有很多工具可以帮助设计师和网站管理员调整缓存服务器对待网站的方式,这也许需要你亲自下手对服务器的配置进行一些调整,但绝对值得;了解如何使用这些工具请参考后面的实现章节;

HTML meta标签和HTTP 头信息


HTML的编写者会在文档的<HEAD>区域中加入描述文档的各种属性,这些META标签常常被用于标记文档不可以被缓存或者标记多长时间后过期;
META 标签使用很简单:但是效率并不高,因为只有几种浏览器会遵循这个标记(那些真正会“读懂”HTML的浏览器),没有一种缓存代理服务器能遵循这个 规则(因为它们几乎完全不解析文档中HTML内容);有事会在Web页面中增加:Pragma: no-cache这个META标记,如果要让页面保持刷新,这个标签其实完全没有必要。
如果你的网站托管在ISP机房中,并且机房可能不给你权限去控制HTTP的头信息(如:Expires和Cache-Control),大声控诉:这些机制对于你的工作来说是必须的;
另 外一方面: HTTP头信息可以让你对浏览器和代理服务器如何处理你的副本进行更多的控制。他们在HTML代码中是看不见的,一般由Web服务器自动生成。但是,根据 你使用的服务,你可以在某种程度上进行控制。在下文中:你将看到一些有趣的HTTP头信息,和如何在你的站点上应用部署这些特性。

HTTP头信息发送在HTML代码之前,只有被浏览器和一些中间缓存能看到,一个典型的HTTP 1.1协议返回的头信息看上去像这样:

HTTP/1.1 200 OK
Date: Fri, 30 Oct 1998 13:19:41 GMT
Server: Apache/1.3.3 (Unix)
Cache-Control: max-age=3600, must-revalidate
Expires: Fri, 30 Oct 1998 14:19:41 GMT
Last-Modified: Mon, 29 Jun 1998 02:28:12 GMT
ETag: “3e86-410-3596fbbc”
Content-Length: 1040
Content-Type: text/html


在头信息空一行后是HTML代码的输出,关于如何设置HTTP头信息请参考实现章节;

Pragma HTTP头信息 (为什么它不起作用)


很 多人认为在HTTP头信息中设置了Pragma: no-cache后会让内容无法被缓存。但事实并非如此:HTTP的规范中,响应型头信息没有任何关于Pragma属性的说明,而讨论了的是请求型头信息 Pragma属性(头信息也由浏览器发送给服务器),虽然少数集中缓存服务器会遵循这个头信息,但大部分不会。用了Pragma也不起什么作用,要用就使 用下列头信息:

使用Expires(过期时间)HTTP头信息来控制保鲜期


Expires(过期时间) 属性是HTTP控制缓存的基本手段,这个属性告诉缓存器:相关副本在多长时间内是新鲜的。过了这个时间,缓存器就会向源服务器发送请求,检查文档是否被修改。几乎所有的缓存服务器都支持Expires(过期时间)属性;

大部分Web服务器支持你用几种方式设置Expires属性;一般的:可以设计一个绝对时间间隔:基于客户最后查看副本的时间(最后访问时间)或者根据服务器上文档最后被修改的时间;

Expires 头信息:对于设置静态图片文件(例如导航栏和图片按钮)可缓存特别有用;因为这些图片修改很少,你可以给它们设置一个特别长的过期时间,这会使你的网站对 用户变得相应非常快;他们对于控制有规律改变的网页也很有用,例如:你每天早上6点更新新闻页,你可以设置副本的过期时间也是这个时间,这样缓存 服务器就知道什么时候去取一个更新版本,而不必让用户去按浏览器的“刷新”按钮。

过期时间头信息属性值只能是HTTP格式的日期时间,其他的都会被解析成当前时间“之前”,副本会过期,记住:HTTP的日期时间必须是格林威治时间(GMT),而不是本地时间。举例:

Expires: Fri, 30 Oct 1998 14:19:41 GMT

所以使用过期时间属性一定要确认你的Web服务器时间设置正确,一个途径是通过网络时间同步协议(Network Time Protocol NTP),和你的系统管理员那里你可以了解更多细节。
虽然过期时间属性非常有用,但是它还是有些局限,首先:是牵扯到了日期,这样Web服务器的时间和缓存服务器的时间必须是同步的,如果有些不同步,要么是应该缓存的内容提前过期了,要么是过期结果没及时更新。
还有一个过期时间设置的问题也不容忽视:如果你设置的过期时间是一个固定的时间,如果你返回内容的时候又没有连带更新下次过期的时间,那么之后所有访问请求都会被发送给源Web服务器,反而增加了负载和响应时间;

Cache-Control(缓存控制) HTTP头信息


HTTP 1.1介绍了另外一组头信息属性:Cache-Control响应头信息,让网站的发布者可以更全面的控制他们的内容,并定位过期时间的限制。
有用的 Cache-Control响应头信息包括:


  • max-age=[秒] — 执行缓存被认为是最新的最长时间。类似于过期时间,这个参数是基于请求时间的相对时间间隔,而不是绝对过期时间,[秒]是一个数字,单位是秒:从请求时间开始到过期时间之间的秒数。
  • s-maxage=[秒] — 类似于max-age属性,除了他应用于共享(如:代理服务器)缓存
  • public — 标记认证内容也可以被缓存,一般来说: 经过HTTP认证才能访问的内容,输出是自动不可以缓存的;
  • no-cache — 强制每次请求直接发送给源服务器,而不经过本地缓存版本的校验。这对于需要确认认证应用很有用(可以和public结合使用),或者严格要求使用最新数据的应用(不惜牺牲使用缓存的所有好处);
  • no-store — 强制缓存在任何情况下都不要保留任何副本
  • must-revalidate — 告诉缓存必须遵循所有你给予副本的新鲜度的,HTTP允许缓存在某些特定情况下返回过期数据,指定了这个属性,你高速缓存,你希望严格的遵循你的规则。
  • proxy-revalidate — 和 must-revalidate类似,除了他只对缓存代理服务器起作用

举例:

Cache-Control: max-age=3600, must-revalidate

如果你计划试用Cache-Control属性,你应该看一下这篇HTTP文档,详见参考和深入阅读;

校验参数和校验


在Web缓存如何工作: 我们说过:校验是当副本已经修改后,服务器和缓存之间的通讯机制;使用这个机制:缓存服务器可以避免副本实际上仍然足够新的情况下重复下载整个原件。
校验参数非常重要,如果1个不存在,并且没有任何信息说明保鲜期(Expires或Cache-Control)的情况下,缓存将不会存储任何副本;
最常见的校验参数是文档的最后修改时间,通过最后Last-Modified头信息可以,当一份缓存包含Last-Modified信息,他基于此信息,通过添加一个If-Modified-Since请求参数,向服务器查询:这个副本从上次查看后是否被修改了。
HTTP 1.1介绍了另外一个校验参数: ETag,服务器是服务器生成的唯一标识符ETag,每次副本的标签都会变化。由于服务器控制了ETag如何生成,缓存服务器可以通过If-None-Match请求的返回没变则当前副本和原件完全一致。
所有的缓存服务器都使用Last-Modified时间来确定副本是否够新,而ETag校验正变得越来越流行;
所有新一代的Web服务器都对静态内容(如:文件)自动生成ETag和Last-Modified头信息,而你不必做任何设置。但是,服务器对于动态内容(例如:CGI,ASP或数据库生成的网站)并不知道如何生成这些信息,参考一下编写利于缓存的脚本章节;

创建利于缓存网站的窍门


除了使用新鲜度信息和校验,你还有很多方法使你的网站缓存友好。


  • 保持URL稳定: 这是缓存的金科玉律,如果你给在不同的页面上,给不同用户或者从不同的站点上提供相同的内容,应该使用相同的URL,这是使你的网站缓存友好最简单,也是 最高效的方法。例如:如果你在页面上使用 “/index.html” 做为引用,那么就一直用这个地址;
  • 使用一个共用的库存放每页都引用的图片和其他页面元素;
  • 对于不经常改变的图片/页面启用缓存,并使用Cache-Control: max-age属性设置一个较长的过期时间;
  • 对于定期更新的内容设置一个缓存服务器可识别的max-age属性或过期时间;
  • 如果数据源(特别是下载文件)变更,修改名称,这样:你可以让其很长时间不过期,并且保证服务的是正确的版本;而链接到下载文件的页面是一个需要设置较短过期时间的页面。
  • 万不得已不要改变文件,否则你会提供一个非常新的Last-Modified日期;例如:当你更新了网站,不要复制整个网站的所有文件,只上传你修改的文件。
  • 只在必要的时候使用Cookie,cookie是非常难被缓存的,而且在大多数情况下是不必要的,如果使用cookie,控制在动态网页上;
  • 减少试用SSL,加密的页面不会被任何共享缓存服务器缓存,只在必要的时候使用,并且在SSL页面上减少图片的使用;
  • 使用可缓存性评估引擎,这对于你实践本文的很多概念都很有帮助;

编写利于缓存的脚本


脚本缺省不会返回校验参数(返回Last-Modified或ETag头信息)或其他新鲜度信息(Expires或Cache-Control),有些动态脚本的确是动态内容(每次相应内容都不一样),但是更多(搜索引擎,数据库引擎网站)网站还是能从缓存友好中获益的。
一般说来,如果脚本生成的输出在未来一段时间(几分钟或者几天)都是可重复复制的,那么就是可缓存的。如果脚本输出内容只随URL变化而变化,也是可缓存的;但如果输出会根据cookie,认证信息或者其他外部条件变化,则还是不可缓存的。


  • 最利于缓存的脚本就是将内容改变时导出成静态文件,Web服务器可以将其当作另外一个网页并生成和试用校验参数,让一些都变得更简单,只需要写入文件即可,这样最后修改时间也有了;
  • 另外一个让脚本可缓存的方法是对一段时间内能保持较新的内容设置一个相对寿命的头信息,虽然通过Expires头信息也可以实现,但更容易的是用Cache-Control: max-age属性,它会让首次请求后一段时间内缓存保持新鲜;
  • 如 果以上做法你都做不到,你可以让脚本生成一个校验属性,并对 If-Modified-Since 和/或If-None-Match请求作出反应,这些属性可以从解析HTTP头信息得到,并对符合条件的内容返回304 Not Modified(内容未改变),可惜的是,这种做法比不上前2种高效;

其他窍门:


  • 尽量避免使用POST,除非万不得已,POST模式的返回内容不会被大部分缓存服务器保存,如果你发送内容通过URL和查询(通过GET模式)的内容可以缓存下来供以后使用;
  • 不要在URL中加入针对每个用户的识别信息:除非内容是针对每个用户不同的;
  • 不要统计一个用户来自一个地址的所有请求,因为缓存常常是一起工作的;
  • 生成并返回Content-Length头信息,如果方便的话,这个属性让你的脚本在可持续链接模式时:客户端可以通过一个TCP/IP链接同时请求多个副本,而不是为每次请求单独建立链接,这样你的网站相应会快很多;
具体定义请参考实现章节。

常见问题解答


让网站变得可缓存的要点是什么?


好的策略是确定那些内容最热门,大量的复制(特别是图片)并针对这些内容先部署缓存。

如何让页面通过缓存达到最快相应?


缓存最好的副本是那些可以长时间保持新鲜的内容;基于校验虽然有助于加快相应,但是它不得不和源服务器联系一次去检查内容是否够新,如果缓存服务器上就知道内容是新的,内容就可以直接相应返回了。

我理解缓存是好的,但是我不得不统计多少人访问了我的网站!


如果你必须知道每次页面访问的,选择【一】个页面上的小元素,或者页面本身,通过适当的头信息让其不可缓存,例如: 可以在每个页面上部署一个1×1像素的透明图片。Referer头信息会有包含这个图片的每个页面信息;
明确一点:这个并不会给你一个关于你用户精确度很高的统计,而且这对互联网和你的用户这都不太好,消耗了额外的带宽,强迫用户去访问无法缓存的内容。了解更多信息,参考访问统计资料。

我如何能看到HTTP头信息的内容?


很多浏览器在页面属性或类似界面中可以让你看到Expires 和Last-Modified信息;如果有的话:你会找到页面信息的菜单和页面相关的文件(如图片),并且包含他们的详细信息;
看到完整的头信息,你可以用telnet手工连接到Web服务器;
为此:你可能需要用一个字段指定端口(缺省是80),或者链接到www.example.com:80 或者 www.example.com 80(注意是空格),更多设置请参考一下telnet客户端的文档;
打开网站链接:请求一个查看链接,如果你想看到http://www.example.com/foo.html 连接到www.example.com的80端口后,键入:

GET /foo.html HTTP/1.1 [回车]
GET /foo.html HTTP/1.1 [return]
Host: www.example.com [回车][回车]
Host: www.example.com [return][return]

在[回车]处按键盘的回车键;在最后,要按2次回车,然后,就会输出头信息及完整页面,如果只想看头信息,将GET换成HEAD。

我的页面是密码保护的,代理缓存服务器如何处理他们?


缺省的,网页被HTTP认证保护的都是私密内容,它们不会被任何共享缓存保留。但是,你可以通过设置Cache-Control: public让认证页面可缓存,HTTP 1.1标准兼容的缓存服务器会认出它们可缓存。
如果你认为这些可缓存的页面,但是需要每个用户认证后才能看,可以组合使用Cache-Control: public和no-cache头信息,高速缓存必须在提供副本之前,将将新客户的认证信息提交给源服务器。设置就是这样:

Cache-Control: public, no-cache

无论如何:这是减少认证请求的最好方法,例如: 你的图片是不机密的,将它们部署在另外一个目录,并对此配置服务器不强制认证。这样,那些图片会缺省都缓存。

我们是否要担心用户通过cache访问我的站点?


代理服务器上SSL页面不会被缓存(不推荐被缓存),所以你不必为此担心。但是,由于缓存保存了非SSL请求和从他们抓取的URL,你要意识到没有安全保护的网站,可能被不道德的管理员可能搜集用户隐私,特别是通过URL。
实际上,位于服务器和客户端之间的管理员可以搜集这类信息。特别是通过CGI脚本在通过URL传递用户名和密码的时候会有很大问题;这对泄露用户名和密码是一个很大的漏洞;
如果你初步懂得互联网的安全机制,你不会对缓存服务器有任何。

我在寻找一个包含在Web发布系统解决方案,那些是比较有缓存意识的系统?


这很难说,一般说来系统越复杂越难缓存。最差就是全动态发布并不提供校验参数;你无发缓存任何内容。可以向系统提供商的技术人员了解一下,并参考后面的实现说明。

 

 

 

 

我的图片设置了1个月后过期,但是我现在需要现在更新。


过期时间是绕不过去的,除非缓存(浏览器或者代理服务器)空间不足才会删除副本,缓存副本在过期之间会被一直使用。
最好的办法是改变它们的链接,这样,新的副本将会从源服务器上重新下载。记住:引用它们的页面本身也会被缓存。因此,使用静态图片和类似内容是很容易缓存的,而引用他们的HTML页面则要保持非常更新;
如果你希望对指定的缓存服务器重新载入一个副本,你可以强制使用“刷新”(在FireFox中在reload的时候按住shift键:就会有前面提到恶Pragma: no-cache头信息发出)。或者你可以让缓存的管理员从他们的界面中删除相应内容;

我运行一个Web托管服务,如何让我的用户发布缓存友好的网页?


如果你使用apahe,可以考虑允许他们使用.htaccess文件并提供相应的文档;
另外一方面: 你也可以考虑在各种虚拟主机上建立各种缓存策略。例如: 你可以设置一个目录 /cache-1m 专门用于存放访问1个月的访问,另外一个 /no-cache目录则被用提供不可存储副本的服务。
无论如何:对于大量用户访问还是应该用缓存。对于大网站,这方面的节约很明显(带宽和服务器负载);

我标记了一些网页是可缓存的,但是浏览器仍然每次发送请求给服务。如何强制他们保存副本?


缓存服务器并不会总保存副本并重用副本;他们只是在特定情况下会不保存并使用副本。所有的缓存服务器都回基于文件的大小,类型(例如:图片 页面),或者服务器空间的剩余来确定如何缓存。你的页面相比更热门或者更大的文件相比,并不值得缓存。
所以有些缓存服务器允许管理员根据文件类型确定缓存副本的优先级,允许某些副本被永久缓存并长期有效;

缓存机制的实现 – Web服务器端配置


一般说来,应该选择最新版本的Web服务器程序来部署。不仅因为它们包含更多利于缓存的功能,新版本往往在性能和安全性方面都有很多的改善。

Apache HTTP服务器


Apache有些可选的模块来包含这些头信息: 包括Expires和Cache-Control。 这些模块在1.2版本以上都支持;
这些模块需要和apache一起编译;虽然他们已经包含在发布版本中,但缺省并没有启用。为了确定相应模块已经被启用:找到httpd程序并运行httpd -l 它会列出可用的模块,我们需要用的模块是mod_expires和mod_headers


  • 如 果这些模块不可用,你需要联系管理员,重新编译并包含这些模块。这些模块有时候通过配置文件中把注释掉的配置启用,或者在编译的时候增加-enable -module=expires和-enable-module=headers选项(在apache 1.3和以上版本)。 参考Apache发布版中的INSTALL文件;

Apache一旦启用了相应的模块,你就可以在.htaccess文件或者在服务器的access.conf文件中通过mod_expires设置副本什 么时候过期。你可设置过期从访问时间或文件修改时间开始计算,并且应用到某种文件类型上或缺省设置,参考模块的文档获得更多信息,或者遇到问题的时候向你身边的apache专家讨教。
应用Cache-Control头信息,你需要使用mod_headers,它将允许你设置任意的HTTP头信息,参考mod_headers的文档可以获得更多资料;
这里有个例子说明如何使用头信息:


  • .htaccess文件允许web发布者使用命令只在配置文件中用到的命令。他影响到所在目录及其子目录;问一下你的服务器管理员确认这个功能是否启用了。

### 启用 mod_expires
ExpiresActive On
### 设置 .gif 在被访问过后1个月过期。
ExpiresByType image/gif A2592000
### 其他文件设置为最后修改时间1天后过期
### (用了另外的语法)
ExpiresDefault “modification plus 1 day”
### 在index.html文件应用 Cache-Control头属性
<Files index.html>
Header append Cache-Control “public, must-revalidate”
</Files>       


  • 注意: 在适当情况下mod_expires会自动计算并插入Cache-Control:max-age 头信息

Apache 2.0的配置和1.3类似,更多信息可以参考2.0的mod_expires和mod_headers文档;

Microsoft IIS服务器


Microsoft的IIS可以非常容易的设置头信息,注意:这只针对IIS 4.0服务器,并且只能在NT服务器上运行。
为网站的一个区域设置头信息,先要到管理员工具界面中,然后设置属性。选择HTTP Header选单,你会看到2个有趣的区域:启用内容过期和定制HTTP头信息。头一个设置会自动配置,第二个可以用于设置Cache-Control头信息;
设置asp页面的头信息可以参考后面的ASP章节,也可以通过ISAPI模块设置头信息,细节请参考MSDN。

Netscape/iPlanet企业服务器


3.6版本以后,Netscape/iPlanet已经不能设置Expires头信息了,他从3.0版本开始支持HTTP 1.1的功能。这意味着HTTP 1.1的缓存(代理服务器/浏览器)优势都可以通过你对Cache-Control设置来获得。
使用Cache-Control头信息,在管理服务器上选择内容管理|缓存设置目录。然后:使用资源选择器,选择你希望设置头信息的目录。设置完头信息后,点击“OK”。更多信息请参考Netscape/iPlanet企业服务器的手册。

缓存机制的实现:服务器端脚本


需要注意的一点是:也许服务器设置HTTP头信息比脚本语言更容易,但是两者你都应该使用。
因 为服务器端的脚本主要是为了动态内容,他本身不产生可缓存的文件页面,即使内容实际是可以缓存的。如果你的内容经常改变,但是不是每次页面请求都改变, 考虑设置一个Cache-Control: max-age头信息;大部分用户会在短时间内多次访问同一页面。例如: 用户点击“后退”按钮,即使没有新内容,他们仍然要再次从服务器下载内容查看。

CGI程序


CGI脚本是生成内容最流行的方式之一,你可以很容易在发送内容之前的扩展HTTP头信息;大部分CGI实现都需要你写 Content-Type头信息,例如这个Perl脚本:

#!/usr/bin/perl
print “Content-type: text/html\n”;
print “Expires: Thu, 29 Oct 1998 17:04:19 GMT\n”;
print “\n”;
### 后面是内容体…

由于都是文本,你可以很容易通过内置函数生成Expires和其他日期相关的头信息。如果你使用Cache-Control: max-age;会更简单;

print “Cache-Control: max-age=600\n”;

这样脚本可以在被请求后缓存10分钟;这样用户如果按“后退”按钮,他们不会重新提交请求;
CGI的规范同时也允许客户端发送头信息,每个头信息都有一个‘HTTP_’的前缀;这样如果一个客户端发送一个If-Modified-Since请求,就是这样的:

HTTP_IF_MODIFIED_SINCE = Fri, 30 Oct 1998 14:19:41 GMT


参考一下cgi_buffer库,一个自动处理ETag的生成和校验的库,生成Content-Length属性和对内容进行gzip压缩。在Python脚本中也只需加入一行;

服务器端包含 Server Side Includes


SSI(经常使用.shtml扩展名)是网站发布者最早可以生成动态内容的方案。通过在页面中设置特别的标记,也成为一种嵌入HTML的脚本;
大部分SSI的实现无法设置校验器,于是无法缓存。但是Apache可以通过对特定文件的组执行权限设置实现允许用户设置那种SSI可以被缓存;结合XbitHack调整整个目录。更多文档请参考mod_include文档。

PHP


PHP是一个内建在web服务器中的服务器端脚本语言,当做为HTML嵌入式脚本,很像SSI,但是有更多的选项,PHP可以在各种Web服务器上设置为CGI模式运行,或者做为Apache的模块;
缺省PHP生成副本没有设置校验器,于是也无法缓存,但是开发者可以通过Header()函数来生成HTTP的头信息;
例如:以下代码会生成一个Cache-Control头信息,并设置为3天以后过期的Expires头信息;

<?php
Header(“Cache-Control: must-revalidate”);

$offset = 60 * 60 * 24 * 3;
$ExpStr = “Expires: ” . gmdate(“D, d M Y H:i:s”, time() + $offset) . ” GMT”;
Header($ExpStr);
?>

记住: Header()的输出必须先于所有其他HTML的输出;
正如你看到的:你可以手工创建HTTP日期;PHP没有为你提供专门的函数(新版本已经让这个越来越容易了,请参考PHP的日期相关函数文档),当然,最简单的还是设置Cache-Control: max-age头信息,而且对于大部分情况都比较适用;
更多信息,请参考header相关的文档;
也请参考一下cgi_buffer库,自动处理ETag的生成和校验,Content-Length生成和内容的gzip压缩,PHP脚本只需包含1行代码;

Cold Fusion


Cold Fusion是Macromedia的商业服务器端脚本引擎,并且支持多种Windows平台,Linux平台和多种Unix平台。Cold Fusion通过CFHEADER标记设置HTTP头信息相对容易。可惜的是:以下的Expires头信息的设置有些容易误导;

<CFHEADER NAME=”Expires” VALUE=”#Now()#”>

它并不像你想像的那样工作,因为时间(本例中为请求发起的时间)并不会被转换成一个符合HTTP时间,而且打印出副本的Cold fusion的日期/时间对象,大部分客户端会忽略或者将其转换成1970年1月1日。
但是:Cold Fusion另外提供了一套日期格式化函数, GetHttpTimeSTring. 结合DateAdd函数,就很容易设置过期时间了,这里我们设置一个Header声明副本在1个月以后过期;

<cfheader name=”Expires” value=”#GetHttpTimeString(DateAdd(‘m’, 1, Now()))#”>

你也可以使用CFHEADER标签来设置Cache-Control: max-age等其他头信息;
记住:Web服务器也会将头信息设置转给Cold Fusion(做为CGI运行的时候),检查你的服务器设置并确定你是否可以利用服务器设置代替Cold Fusion。

ASP和ASP.NET


在asp 中设置HTTP头信息是:确认Response方法先于HTML内容输出前被调用,或者使用 Response.Buffer暂存输出;同样的:注意某些版本的IIS缺省设置会输出Cache-Control: private 头信息,必须声明成public才能被共享缓存服务器缓存。
IIS的ASP和其他web服务器都允许你设置HTTP头信息,例如: 设置过期时间,你可以设置Response对象的属性;

<% Response.Expires=1440 %>

设置请求的副本在输出的指定分钟后过期,类似的:也可以设置绝对的过期时间(确认你的HTTP日期格式正确)

<% Response.ExpiresAbsolute=#May 31,1996 13:30:15 GMT# %>

Cache-Control头信息可以这样设置:

<% Response.CacheControl=”public” %>

在ASP.NET中,Response.Expires 已经不推荐使用了,正确的方法是通过Response.Cache设置Cache相关的头信息;

Response.Cache.SetExpires ( DateTime.Now.AddMinutes ( 60 ) ) ;
Response.Cache.SetCacheability ( HttpCacheability.Public ) ;

参考MSDN文档可以找到更多相关新年系;

参考文档和深入阅读


HTTP 1.1 规范定义


HTTP 1.1的规范有大量的扩展用于页面缓存,以及权威的接口实现指南,参考章节:13, 14.9, 14.21, 以及 14.25.

Web-Caching.com


非常精彩的介绍缓存相关概念,并介绍其他在线资源。

关于非连续性访问统计


Jeff Goldberg内容丰富的演说告诉你为什么不应该过度依赖访问统计和计数器;

可缓存性检测引擎


可缓存的引擎设计,检测网页并确定其如何与Web缓存服务器交互, 这个引擎配合这篇指南是一个很好的调试工具,

cgi_buffer库


包含库:用于CGI模式运行的Perl/Python/PHP脚本,自动处理ETag生成/校验,Content-Length生成和内容压缩。正确地。 Python版本也被用作其他大量的CGI脚本。

关于本文档


本文版权属于Mark Nottingham <mnot@pobox.com>,本作品遵循创作共用版权。
如果你镜像本文,请通过以上邮件告知,这样你可以在更新时被通知;
所有的商标属于其所有人。
虽然作者确信内容在发布时的正确性,但不保证其应用或引申应用的正确性,如有误传,错误或其他需要澄清的问题请尽快告知作者;
本文最新版本可以从 http://www.mnot.net/cache_docs/ 获得;
翻译版本包括: 捷克语版,法语版和中文版。
版本: 1.81 – 2007年3月16日
创作共用版权声明
翻译: 车东 2007年9月6日