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

        输出类是个核心类,它的功能只有一个:发送 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内部分析器,其结构和方法属性功能描述如下:

CI框架源码解析十之输出类文件Output.php_第1张图片

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

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信息,服务器在向客户端输出时,会告知客户端我将要给你什么类型的数据,客户端浏览器根据这个信息用对应的方式解析。比如现在服务器要将一组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.2048, 2.81MB。

Ⅴ 判断是否执行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文件的源码(注释版):

    _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.2048', '2.81MB'), 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('|.*?|is', '', $output, -1, $count) . $CI->profiler->run();
                if ($count > 0) {
                    $output .= '';
                }
            }
            //如果我们有在当前的控制器里面定义了_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');
            }
        }
    
    }

你可能感兴趣的:(CodeIgniter,CodeIgniter源码解析)