您的位置:首页 > 编程语言 > PHP开发

CI框架源码解析十之输出类文件Output.php

2016-10-26 11:00 615 查看
        输出类是个核心类,它的功能只有一个:发送 Web 页面内容到请求的浏览器。如果你开启缓存,它也负责缓存你的 Web 页面。

        Output类参考说明(摘抄CI框架手册):在一般情况下,你可能根本就不会注意到输出类,因为它无需你的干涉, 对你来说完全是透明的。例如,当你使用 加载器 加载一个视图文件时,它会自动传入到输出类,并在系统执行的最后由 CodeIgniter 自动调用。尽管如此,在你需要时,你还是可以对输出进行手工处理。

         在说Output类前先说几个知识点和编程技巧:

$_SERVER['HTTP_ACCEPT_ENCODING']; // :对应请求头是Accept-Encoding:"gzip, deflate"
$_SERVER['HTTP_IF_MODIFIED_SINCE'];// :对应请求头是If-Modified-Since:"Sat, 16 Feb 2013 08:10:03 GMT"
$_SERVER['REQUEST_TIME']; // :请求发起时间
        在代码中想要实现这种连续调用类方法的方式,只需要在每个方法末尾加上return $this;

$output
->set_content_type('application/json')
->set_output(json_encode(array('foo' => 'bar')));

public function set_content_type($mime_type, $charset = NULL)
{
......
return $this;
}
        CI框架要想启用缓存功能,在控制器(controller)的方法(function)内加入一句话:$this->output->cache(n),其中n是缓存更新的分钟数。Output类主要功能负责向浏览器输出最终结果,其中包括从缓存加载内容输出,根据控制器方法产生的内容输出,还包括写缓存、设置头信息、加载CI内部分析器,其结构和方法属性功能描述如下:



现在重点分析一下其主要成员方法:

1、构造函数(__construct())

① 设置压缩标记$_compress_output:

        在构造函数中,CI通过ini_get('zlib.output_compression')获取当前php环境是否开启了GZIP压缩。如果PHP环境没有开启,那么判断配置文件中的压缩设置(compress_output=TRUE),是不是要求框架压缩输出,如果要求的话,只要当前PHP是加载了zlib扩展的,那么就把$_compress_output标记设为TRUE。通常情况下,我们在使用过程中会开启WEB服务器的压缩功能,而关闭程序本身压缩功能。

② 设置$mimes值:

        加载配置application/config/mimes.php中的MIME信息。

2、output函数簇(get_output()、set_output($output)、append_output($output))

        output函数簇,用于设置或获取成员变量$final_output的值。

① get_output()

        获取$this->final_output,允许你手工获取存储在输出类中的待发送的内容。使用示例:$string = $this->output->get_output();注意,只有通过 CodeIgniter 输出类的某个方法设置过的数据,例如 $this->load->view() 方法,才可以使用该方法获取到。

② set_output($output)

        设置$this->final_output,允许你手工设置最终的输出字符串。使用示例:$this->output->set_output($data);

③ append_output($output)

        向输出字符串附加数据。$this->output->append_output($data);

3、header函数簇(get_header()、set_header($header, $replace = TRUE))

① get_header()

        返回请求的 HTTP 头,如果 HTTP 头还没设置,返回 NULL 。 例如:

$this->output->set_content_type('text/plain', 'UTF-8');
echo $this->output->get_header('content-type');
// Outputs: text/plain; charset=utf-8
② set_header($header, $replace = TRUE)

        允许你手工设置服务器的 HTTP 头,输出类将在最终显示页面时发送它。例如:

$this->output->set_header('HTTP/1.1 200 OK');
$this->output->set_header('Last-Modified: '.gmdate('D, d M Y H:i:s', $last_update).' GMT');
$this->output->set_header('Cache-Control: no-store, no-cache, must-revalidate');
$this->output->set_header('Cache-Control: post-check=0, pre-check=0');
$this->output->set_header('Pragma: no-cache');
        如果php开启了zlib.output_compression压缩,就跳过content-length头的设置,这样做的理由是当压缩开启后,实际输出字节数比正常少,误设content-length头后,会使得客户端一直等待服务器发送足够字节的文本,造成无法正常响应。

public function set_header($header, $replace = TRUE)
{
if ($this->_zlib_oc && strncasecmp($header, 'content-length', 14) === 0) {
return $this;
}
$this->headers[] = array($header, $replace);
return $this;
}

4、Content_type函数簇(set_content_type($mime_type, $charset = NULL)、get_content_type())

        每次服务器响应的头信息中都会包括类似这样的信息:Content-Type:"text/html; charset=utf-8",源文件中有Meta信息<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">,服务器在向客户端输出时,会告知客户端我将要给你什么类型的数据,客户端浏览器根据这个信息用对应的方式解析。比如现在服务器要将一组excel表格数据输出给客户端,你就可以用content="application/excel"来告知客户端,这是一个excel文件,你应该用对待excel的方式来对待。有的装了插件的浏览器可能在本身就打开显示了,有的就提示下载EXCEL类型的文件了。那么application/excel就被称作Mime信息。Mime信息与不同文件的对应关系在application/config/mimes.php中都有。

① set_content_type($mime_type, $charset = NULL)

        给Head添加Content_type信息。允许你设置你的页面的 MIME 类型,可以很方便的提供 JSON 数据、JPEG、XML 等等格式。

$this->output
->set_content_type('application/json')
->set_output(json_encode(array('foo' => 'bar')));

//$mime_type是要设置MIME信息的文件扩展名,系统从$mimes数组中找出对应扩展名中的MIME信息
if (strpos($mime_type, '/') === FALSE) {
$extension = ltrim($mime_type, '.');
if (isset($this->mimes[$extension])) {
$mime_type =& $this->mimes[$extension];
if (is_array($mime_type)) {
$mime_type = current($mime_type);
}
}
}
        这里程序用了if (strpos($mime_type, '/') === FALSE)判断,表示如果参数是扩展名(pptx,jpeg)的话,就去$mimes数组进行匹配处理。如果参数中包括了“/”,系统认为方法参数type值就是MIME信息,比如application/octet-stream,接下来就直接$this->mime_type = $mime_type;接下来设置charset信息,如果参数没有设置,就读配置文件的charset设置。

② get_content_type()

        获取当前正在使用的 HTTP 头 Content-Type ,不包含字符集部分。$mime = $this->output->get_content_type();系统从一堆header信息中匹配Content-Type信息,找到了就返回其中的MIME值,没找到,就返回默认的text/html。

5、profiler函数簇(enable_profiler($val = TRUE)、set_profiler_sections($sections))

① enable_profiler($val = TRUE)

        public function enable_profiler($val = TRUE);设置$enable_profiler值是否开启分析器。

② set_profiler_sections($sections)

        public function set_profiler_sections($sections);//设置分析器的内容。允许你启用或禁用程序分析器 ,它可以在你的页面底部显示 基准测试的结果或其他一些数据帮助你调试和优化程序。$this->output->enable_profiler(TRUE);

6、写入缓存(_write_cache($output))

        主要流程是根据访问的URI信息生成一个MD5作为本次访问缓存的KEY,再将内容写入文件。方法流程如下:

Ⅰ 实例$CI控制器对像$CI =& get_instance();

Ⅱ 获取缓存路径:通过配置文件获取缓存路径$CI->config->item('cache_path'),如果没有设置,那么就默认用application/cache路径,如果路径不存在或不可写,记录错误日志,并返回。

Ⅲ 生成缓存的key:

    获得$cachpath:根据uri生成唯一身份字符串,可认为是缓存的key。

    a、获取当前地址$url。这里特别用到了URI类,另外开篇详说。

    b.1 如果配置文件中设置了cache_query_string值(就是设置querystring中允许被缓存的变量),就会取_GET数组与cache_query_string中设置数组的交集。

       举个例子:

       $config['cache_query_string'] = array('cid','page');

       那么如果当前url是:http://mysite/balabala?cid=1&page=2&sort=viewnumber&sorttype=desc

       那么最终uri.=http://mysite/balabala?cid=1&page=2

    b.2 如果没有设置cache_query_string值,那么uri.=$_SERVER['QUERY_STRING'];会把整个地址加载进来。

    c、将uri用md5()函数生成唯一身份字符串,可认为是缓存的key,得到缓存文件最终路径$cache_path。

Ⅳ 创建缓存文件句柄: 打开$cache_path文件获得句柄$fp,对文件进行一个排它锁定flock($fp, LOCK_EX);。

Ⅴ 压缩处理:如果没有在php.ini中开启zlib.output_compression,且配置文件中要求开启压缩,那么就在程序中使用gzencode来压缩(这段是在构造函数中做的),$output = gzencode($output),压缩完成后,设置content-type。

Ⅵ 过期时间$expire设置。

Ⅶ 生成最终输出$output:$output分为三段:a.expire,headers序列化成字符串;b.分隔符:ENDCI--->;c.之前预输出的$output内容。

Ⅷ 写入文件。

(注:代码就不贴了,放在最后一起贴。)

7、显示缓存(_display_cache(&$CFG, &$URI))

        读cache文本,判断过期没有,调用_display输出。方法执行流程如下:

Ⅰ 从配置中获取缓存路径。

Ⅱ 根据uri获取缓存key,拼凑出缓存路径$filepath:$filepath = $cache_path.md5($uri);。

Ⅲ 读取文件到变量$cache。如果没有缓存,返回FALSE,外部CI工作流程会继续往下执行。

Ⅳ 获取$cache_info,也就是之前写缓存时在ENDCI--->前面加的expire,headers信息。

Ⅴ 判断缓存过期时间: if ($_SERVER['REQUEST_TIME'] >= $expire && is_really_writable($cache_path)) {
@unlink($filepath);
log_message('debug', 'Cache file has expired. File deleted.');
return FALSE;
} else {
$this->set_cache_header($last_modified, $expire);
}
        如果过期,则删除原缓存文件,并返回FALSE; 外部CI工作流程会继续往下执行。如果没有过期,则调用set_cache_header($last_modified, $expiration)。如果设置了HTTP_IF_MODIFIED_SINCE头,且文件最后修改时间没有超过HTTP_IF_MODIFIED_SINCE时间,则直接发304状态码给客户端,让客户端调用本地缓存,关于HTTP_IF_MODIFIED_SINCE及304状态,可以参考《HTTP权威指南》一书。如果文件修改时间超过了HTTP_IF_MODIFIED_SINCE时间,就重新发送头信息,告诉客户端缓存该次请求的结果到本地。

Ⅵ 根据缓存中的expire设置头部信息。

Ⅶ 调用_display输出$cache中的内容部分:$this->_display(substr($cache, strlen($match[0])));。

8、输出(_display($output = ''))

        发送最终输出结果以及服务器的 HTTP 头到浏览器,同时它也会停止基准测试的计时器。方法执行流程如下:

Ⅰ 加载和实例化Benchmark,Config类。使用load_class()而没有使用$CI =& get_instance()控制器的实例来加载类库,原因是因为该方法有时侯是被缓存机制调用,也就是上面说的,_display_cache()函数调用,这时当前请求的上下文根本没有加载控制器类,所以无法正确实例化控制器。

Ⅱ 实例化CI_Controller,如果是缓存,那就不会实像化了。这个很重要,接下来都是以isset($CI)来区分是否是缓存。缓存与非缓存处理方式截然不同。 if (class_exists('CI_Controller', FALSE)) {
$CI =& get_instance();
}
Ⅲ 写缓存(响应cache(n)方法):判断$cache_expiration属性,这个值可由方法cache(n)设置;再判断控制器中有没有用到_output()扩展自己的输出,如果没有,就写缓存。

Ⅳ 解析替换伪变量0.0363, 1.15MB。

Ⅴ 判断是否执行PHP代码端的压缩,如果是缓存就跳过(因为已经压缩过了),如果是正常控制器且_compress_output为真,同时客户端浏览器告诉服务器支持的压缩格式为gzip,就执行ob_start('ob_gzhandler');isset($CI)表明的这一段不会对缓存生效。 if (isset($CI) && $this->_compress_output === TRUE && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE){
ob_start('ob_gzhandler');
}
Ⅵ 输出头信息。

Ⅶ 如果! isset($CI),代表是缓存。那么根据客户端$_SERVER['HTTP_ACCEPT_ENCODING'信息,判断是否输出压缩内容,还是输出原始已解压内容。 如果是缓存,进行输出后,整个CI流程就完成了。 if ($this->_compress_output === TRUE) {
//如果客户端支持压缩
if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) {
header('Content-Encoding: gzip');
header('Content-Length: ' . strlen($output));
} else {
//如果不支持压缩,就要把缓存文件解压输出。substr($output, 10, -8)很多人不理解为什么要这样处理
// PHP 5.4 之后新增的 gzip 解压函数 gzdecode .目前很多的空间服务商的 PHP 版本都没有达到 5.4,
//这也导致使用此函数之后发生函数未定义错误,PHP 官方网站用户提交的日志中有人给出了很好的解决方案,
//使用 gzinflate 函数代替,但对数据要进行处理。
$output = gzinflate(substr($output, 10, -8));
}
}
echo $output;
Ⅷ 生成分析数据 ($this->enable_profiler) 将html加到$output后面,profiler类库另开篇。

Ⅸ 调用控制器中的自定义方法_output对最终结果进行最后一道处理。当然也可不处理。

        最后,贴一下整个输出类Output.php文件的源码(注释版):

<?php

/**
* =======================================
* Created by Pocket Knife Technology.
* User: ZhiHua_W
* Date: 2016/10/26 0014
* Time: 上午 9:16
* Project: CodeIgniter框架—源码分析
* Power: Analysis for Output.php
* =======================================
*/

defined('BASEPATH') OR exit('No direct script access allowed');

/**
* Output类:发送最终的Web页面到所请求的浏览器
* Output组件其实有很多有用的方法,不过一般情况下,你不会直接去用到它们。
* 这里主要以Output::_display_cache()和Output::_display()为两条主线来探究。
*/
class CI_Output
{
//最终输出结果字符串
public $final_output;
//缓存时间
public $cache_expiration = 0;
//头信息
public $headers = array();
//mime类型
public $mimes = array();
protected $mime_type = 'text/html';
//是否开启评测器
public $enable_profiler = FALSE;
//是否开启gzip研所
protected $_zlib_oc = FALSE;
//输出标签
protected $_compress_output = FALSE;
//开启评测模块
protected $_profiler_sections = array();
//是否替换变量0.0363耗时 and 1.15MB内存耗用
public $parse_exec_vars = TRUE;

/**
* 在构造函数中,CI通过ini_get('zlib.output_compression')获取当前php环境是否开启了GZIP压缩。
* 如果PHP环境没有开启,那么判断配置文件中的压缩设置(compress_output=TRUE),
* 是不是要求框架压缩输出,如果要求的话,只要当前PHP是加载了zlib扩展的,
* 那么就把$_compress_output标记设为TRUE。
* 通常情况下,我们在使用过程中会开启WEB服务器的压缩功能,而关闭程序本身压缩功能。
*/
public function __construct()
{
//初始化$_compress_output和$mimes值
$this->_zlib_oc = (bool)ini_get('zlib.output_compression');
$this->_compress_output = ($this->_zlib_oc === FALSE && config_item('compress_output') === TRUE && extension_loaded('zlib'));
//引入mime类型配置文件
$this->mimes =& get_mimes();
log_message('info', 'Output Class Initialized');
}

/**
* 获取$this->final_output
* 允许你手工获取存储在输出类中的待发送的内容。
*/
public function get_output()
{
return $this->final_output;
}

/**
* 设置$this->final_output
* 允许你手工设置最终的输出字符串
*/
public function set_output($output)
{
$this->final_output = $output;
return $this;
}

/**
* 向输出字符串附加数据。
*/
public function append_output($output)
{
// 将数据追加到最终输出结果,此方法在Loader.php中被调用。
$this->final_output .= $output;
return $this;
}

/**
* 允许你手工设置服务器的 HTTP 头,输出类将在最终显示页面时发送它
*/
public function set_header($header, $replace = TRUE)
{
//如果php开启了zlib.output_compression压缩,就跳过content-length头的设置
//这样做的理由是当压缩开启后,实际输出字节数比正常少,误设content-length头后,
//会使得客户端一直等待服务器发送足够字节的文本,造成无法正常响应。
if ($this->_zlib_oc && strncasecmp($header, 'content-length', 14) === 0) {
return $this;
}
$this->headers[] = array($header, $replace);
return $this;
}

/**
* 设置Content Type
* 给Head添加Content_type信息。允许你设置你的页面的 MIME 类型,
* 可以很方便的提供 JSON 数据、JPEG、XML 等等格式。
*/
public function set_content_type($mime_type, $charset = NULL)
{
//$mime_type是要设置MIME信息的文件扩展名,
//系统从$mimes数组中找出对应扩展名中的MIME信息
if (strpos($mime_type, '/') === FALSE) {
$extension = ltrim($mime_type, '.');
if (isset($this->mimes[$extension])) {
$mime_type =& $this->mimes[$extension];
if (is_array($mime_type)) {
$mime_type = current($mime_type);
}
}
}
$this->mime_type = $mime_type;
if (empty($charset)) {
$charset = config_item('charset');
}
$header = 'Content-Type: ' . $mime_type . (empty($charset) ? '' : '; charset=' . $charset);
$this->headers[] = array($header, TRUE);
return $this;
}

/**
* 获取当前正在使用的 HTTP 头 Content-Type ,不包含字符集部分。
*/
public function get_content_type()
{
//系统从一堆header信息中匹配Content-Type信息,找到了就返回其中的MIME值,
//没找到,就返回默认的text/html
for ($i = 0, $c = count($this->headers); $i < $c; $i++) {
if (sscanf($this->headers[$i][0], 'Content-Type: %[^;]', $content_type) === 1) {
return $content_type;
}
}
return 'text/html';
}

/**
* 返回请求的 HTTP 头,如果 HTTP 头还没设置,返回 NULL 。
*/
public function get_header($header)
{
$headers = array_merge(
array_map('array_shift', $this->headers),
headers_list()
);
if (empty($headers) OR empty($header)) {
return NULL;
}
for ($i = 0, $c = count($headers); $i < $c; $i++) {
if (strncasecmp($header, $headers[$i], $l = strlen($header)) === 0) {
return trim(substr($headers[$i], $l + 1));
}
}
return NULL;
}

/**
* Set HTTP Status Header
*/
public function set_status_header($code = 200, $text = '')
{
set_status_header($code, $text);
return $this;
}

/**
* 设置$enable_profiler值是否开启分析器
*/
public function enable_profiler($val = TRUE)
{
$this->enable_profiler = is_bool($val) ? $val : TRUE;
return $this;
}

/**
* 设置分析器的内容
*/
public function set_profiler_sections($sections)
{
//允许你启用或禁用程序分析器 ,
//它可以在你的页面底部显示
//基准测试的结果或其他一些数据帮助你调试和优化程序。
if (isset($sections['query_toggle_count'])) {
$this->_profiler_sections['query_toggle_count'] = (int)$sections['query_toggle_count'];
unset($sections['query_toggle_count']);
}
foreach ($sections as $section => $enable) {
$this->_profiler_sections[$section] = ($enable !== FALSE);
}
return $this;
}

/**
* 设置缓存时长,开启文件缓存
*/
public function cache($time)
{
$this->cache_expiration = is_numeric($time) ? $time : 0;
return $this;
}

/**
* 将最终结果输出到浏览器
*/
public function _display($output = '')
{
$BM =& load_class('Benchmark', 'core');
$CFG =& load_class('Config', 'core');

//当然如果可以拿到超级控制器,我们先拿过来。
if (class_exists('CI_Controller', FALSE)) {
$CI =& get_instance();
}
//如果$output为空,其实往往这是非缓存方式调用的时候。我们将使用Output::final_output。
//(如果是正常流程的输出方式,而不是缓存的话,
//这个属性其实在Loader::view()的时候调用Output::append_output()获得输出内容。)
if ($output === '') {
$output =& $this->final_output;
}

//Output::$cache_expiration其实就是缓存时长,就是平时我们在控制器里面$this->output->cache(n)设置的时长
//现实手段就是使这个Output::$cache_expiration有一定的值,然后程序执行到这里时根据此值判断是否要缓存,
//如果要缓存就生成缓存文件。(注意如果是_display_cache间接调用的话,$this->cache_expiraton是一定为0的,因为
//没有经历过在控制器中调用$this->output->cache(n)。)
if ($this->cache_expiration > 0 && isset($CI) && !method_exists($CI, '_output')) {
//上面有个判断$CI是否有_output方法,其实是提供一个机会让我们自定义处理输出。
//生成缓存文件。
$this->_write_cache($output);
}
$elapsed = $BM->elapsed_time('total_execution_time_start', 'total_execution_time_end');
if ($this->parse_exec_vars === TRUE) {
//系统的总体运行时间和内存消耗就是在这里替换的。
//上面的Output::$parse_exec_vars就是设置要不要替换。
$memory = round(memory_get_usage() / 1024 / 1024, 2) . 'MB';
$output = str_replace(array('0.0363', '1.15MB'), array($elapsed, $memory), $output);
}
//压缩传输的处理。
if (isset($CI) && $this->_compress_output === TRUE
&& isset($_SERVER['HTTP_ACCEPT_ENCODING'])
&& strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE
) {
ob_start('ob_gzhandler');
}
if (count($this->headers) > 0) {
foreach ($this->headers as $header) {
@header($header[0], $header[1]);
}
}
//如果没有超级控制器,可以证明当前是在处理一个缓存的输出。不过利用这个方式来判断,真的有点那个。。。
if (!isset($CI)) {
if ($this->_compress_output === TRUE) {
if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) {
header('Content-Encoding: gzip');
header('Content-Length: ' . strlen($output));
} else {
$output = gzinflate(substr($output, 10, -8));
}
}
//输出缓存内容。结束本函数。
echo $output;
log_message('info', 'Final output sent to browser');
log_message('debug', 'Total execution time: ' . $elapsed);
return;
}
//这里是一个评测器,如果有开启就调用,会生成一些报告到页面尾部用于辅助我们调试。
//我用CI的时候其实没有开启过,厄。
if ($this->enable_profiler === TRUE) {
$CI->load->library('profiler');
if (!empty($this->_profiler_sections)) {
$CI->profiler->set_sections($this->_profiler_sections);
}
$output = preg_replace('|</body>.*?</html>|is', '', $output, -1, $count) . $CI->profiler->run();
if ($count > 0) {
$output .= '</body></html>';
}
}
//如果我们有在当前的控制器里面定义了_output这个方法,
//那么可以利用这个输出做你想做的东西,这个也是很不错的功能。
if (method_exists($CI, '_output')) {
$CI->_output($output);
} else {
echo $output; // Send it to the browser!
}
log_message('info', 'Final output sent to browser');
log_message('debug', 'Total execution time: ' . $elapsed);
}

/**
* 写入缓存
* 在Output::_display()中,判断需要缓存页面时,则调用此方法写入缓存。
*/
public function _write_cache($output)
{
//实例$CI控制器对像$CI =& get_instance();
$CI =& get_instance();
$path = $CI->config->item('cache_path');
$cache_path = ($path === '') ? APPPATH . 'cache/' : $path;
//通过配置文件获取缓存路径$CI->config->item('cache_path'),如果没有设置,那么就默认用application/cache路径
//如果路径不存在或不可写,记录错误日志,并返回。
if (!is_dir($cache_path) OR !is_really_writable($cache_path)) {
log_message('error', 'Unable to write cache file: ' . $cache_path);
return;
}
$uri = $CI->config->item('base_url') . $CI->config->item('index_page') . $CI->uri->uri_string();
//如果配置文件中设置了cache_query_string值
//(就是设置querystring中允许被缓存的变量)
//就会取_GET数组与cache_query_string中设置数组的交集。
if (($cache_query_string = $CI->config->item('cache_query_string')) && !empty($_SERVER['QUERY_STRING'])) {
if (is_array($cache_query_string)) {
$uri .= '?' . http_build_query(array_intersect_key($_GET, array_flip($cache_query_string)));
} else {
$uri .= '?' . $_SERVER['QUERY_STRING'];
}
}
//获得$cachpath:根据uri生成唯一身份字符串,可认为是缓存的key。
$cache_path .= md5($uri); //md5($uri)是文件名
//打开$cache_path文件获得句柄$fp
if (!$fp = @fopen($cache_path, 'w+b')) {
log_message('error', 'Unable to write cache file: ' . $cache_path);
return;
}
//对文件进行一个排它锁定flock($fp, LOCK_EX);
if (flock($fp, LOCK_EX)) {
if ($this->_compress_output === TRUE) {
//如果没有在php.ini中开启zlib.output_compression,
//且配置文件中要求开启压缩,那么就在程序中使用gzencode来压缩(这段是在构造函数中做的)
$output = gzencode($output);
//压缩完成后,设置content-type
if ($this->get_header('content-type') === NULL) {
$this->set_content_type($this->mime_type);
}
}
//过期时间$expire设置
$expire = time() + ($this->cache_expiration * 60);
$cache_info = serialize(array(
'expire' => $expire,
'headers' => $this->headers
));
//生成最终输出$output。
//$output分为三段:
//1、expire,headers序列化成字符串
//2、分隔符:ENDCI--->
//3、之前预输出的$output内容。
$output = $cache_info . 'ENDCI--->' . $output;
for ($written = 0, $length = strlen($output); $written < $length; $written += $result) {
//写入文件
if (($result = fwrite($fp, substr($output, $written))) === FALSE) {
break;
}
}
//解锁flock($fp, LOCK_EX);
flock($fp, LOCK_UN);
} else {
log_message('error', 'Unable to secure a file lock for file at: ' . $cache_path);
return;
}
fclose($fp);
if (is_int($result)) {
chmod($cache_path, 0640);
log_message('debug', 'Cache file written: ' . $cache_path);
$this->set_cache_header($_SERVER['REQUEST_TIME'], $expire);
} else {
@unlink($cache_path);
log_message('error', 'Unable to write the complete cache content at: ' . $cache_path);
}
}

/**
* 在CodeIgniter.php里面有调用此方法,此方法是负责缓存的输出,
* 如果在CodeIgniter.php中调用此方法有输出,则本次请求的运行将直接结束,
* 直接以缓存输出作为响应。
*/
public function _display_cache(&$CFG, &$URI)
{
//取得保存缓存的路径
$cache_path = ($CFG->item('cache_path') === '') ? APPPATH . 'cache/' : $CFG->item('cache_path');
//一条准确的路由都会对应一个缓存文件,缓存文件是对应路由字符串的md5密文。
$uri = $CFG->item('base_url') . $CFG->item('index_page') . $URI->uri_string;
if (($cache_query_string = $CFG->item('cache_query_string')) && !empty($_SERVER['QUERY_STRING'])) {
if (is_array($cache_query_string)) {
$uri .= '?' . http_build_query(array_intersect_key($_GET, array_flip($cache_query_string)));
} else {
$uri .= '?' . $_SERVER['QUERY_STRING'];
}
}
//计算出当前请求对应缓存文件的完整文件路径。
$filepath = $cache_path . md5($uri);
//读取文件到变量$cache。如果没有缓存,返回FALSE,外部CI工作流程会继续往下执行
if (!file_exists($filepath) OR !$fp = @fopen($filepath, 'rb')) {
return FALSE;
}
//打开到缓存文件,并以$fp作为句柄。下一步先取得共享锁(读取)。
flock($fp, LOCK_SH);
//读cache并保存到$cache中
$cache = (filesize($filepath) > 0) ? fread($fp, filesize($filepath)) : '';
//解锁
flock($fp, LOCK_UN);
//关闭文件连接。
fclose($fp);

//下面这个ENDCI--->字样,只是因为CI的缓存文件里面的内容是规定以数字+TS--->开头而已。这个数字是代表创建时间。
//如果不符合此结构,可视为非CI的缓存文件,或者文件已损坏,获取缓存内容失败,返回FALSE。
//如果匹配成功,则$match[1]中保存的是"1346901048TS--->"字样。其实在CI的这个版本$match[0]保存的是和
//$match[1]相同的内容,为什么要分开?我觉得是为了以后扩展和方便改动吧,理解成
//$match[0]是除页面内容之外的附加信息。
//$match[1]是附加信息中和时间有关的信息。
if (!preg_match('/^(.*)ENDCI--->/', $cache, $match)) {
return FALSE;
}
$cache_info = unserialize($match[1]);
$expire = $cache_info['expire'];
$last_modified = filemtime($filepath);

//利用剩下的数字判断缓存是否已经过期,如果过期了,就把它删除,同样视为获取缓存内容失败(过期),
//返回FALSE
if ($_SERVER['REQUEST_TIME'] >= $expire && is_really_writable($cache_path)) {
@unlink($filepath);
log_message('debug', 'Cache file has expired. File deleted.');
return FALSE;
} else {
$this->set_cache_header($last_modified, $expire);
}
foreach ($cache_info['headers'] as $header) {
$this->set_header($header[0], $header[1]);
}
//来到这里,说明了能够顺利获得缓存,则去掉附加信息($match[0])后,
//调用Output::_display()方法输出缓存。并返回
$this->_display(substr($cache, strlen($match[0])));
log_message('debug', 'Cache file is current. Sending it to browser.');
return TRUE;
}

/**
* 删除缓存
* 此方法和上面两个基本所用都是差不多的,
* 可以按照上面的方法流程进行理解
*/
public function delete_cache($uri = '')
{
$CI =& get_instance();
$cache_path = $CI->config->item('cache_path');
if ($cache_path === '') {
$cache_path = APPPATH . 'cache/';
}
if (!is_dir($cache_path)) {
log_message('error', 'Unable to find cache path: ' . $cache_path);
return FALSE;
}
if (empty($uri)) {
$uri = $CI->uri->uri_string();
if (($cache_query_string = $CI->config->item('cache_query_string')) && !empty($_SERVER['QUERY_STRING'])) {
if (is_array($cache_query_string)) {
$uri .= '?' . http_build_query(array_intersect_key($_GET, array_flip($cache_query_string)));
} else {
$uri .= '?' . $_SERVER['QUERY_STRING'];
}
}
}
$cache_path .= md5($CI->config->item('base_url') . $CI->config->item('index_page') . ltrim($uri, '/'));
if (!@unlink($cache_path)) {
log_message('error', 'Unable to delete cache file for ' . $uri);
return FALSE;
}
return TRUE;
}

/**
* 设置缓存头信息
*/
public function set_cache_header($last_modified, $expiration)
{
$max_age = $expiration - $_SERVER['REQUEST_TIME'];
//如果设置了HTTP_IF_MODIFIED_SINCE头,且文件最后修改时间没有超过HTTP_IF_MODIFIED_SINCE时间,
//则直接发304状态码给客户端,让客户端调用本地缓存,关于HTTP_IF_MODIFIED_SINCE及304状态,
//可以参考《HTTP权威指南》一书。
//如果文件修改时间超过了HTTP_IF_MODIFIED_SINCE时间,就重新发送头信息,告诉客户端缓存该次请求的结果到本地。
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $last_modified <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
$this->set_status_header(304);
exit;
} else {
header('Pragma: public');
header('Cache-Control: max-age=' . $max_age . ', public');
header('Expires: ' . gmdate('D, d M Y H:i:s', $expiration) . ' GMT');
header('Last-modified: ' . gmdate('D, d M Y H:i:s', $last_modified) . ' GMT');
}
}

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息