PHP 缩略图图像生成器缓存:如何在 PHP 中正确设置 If-Last-Modified/Max-Age/Last-Modified HEADERS?

Posted

技术标签:

【中文标题】PHP 缩略图图像生成器缓存:如何在 PHP 中正确设置 If-Last-Modified/Max-Age/Last-Modified HEADERS?【英文标题】:PHP Thumbnail Image Generator Caching: How to set If-Last-Modified/Max-Age/Last-Modified HEADERS correctly in PHP? 【发布时间】:2011-07-13 18:31:16 【问题描述】:

即使在 Google PageSpeed(97) 和 Yahoo! 的得分很高之后YSlow(92) php 生成的缩略图似乎不是被动地来自旧缓存:它们似乎每次都会生成......一次又一次......新鲜出炉,消耗大量腰部时间。

这个问题将只专注于如何解决生成拇指的 PHP 代码的 CACHE 问题

看看这些只有 3 ~ 5 kb 的微小缩略图!

瀑布详情:http://www.webpagetest.org/result/110328_AM_8T00/1/details/

任何和所有建议都是对我的 +1 帮助并热烈欢迎,因为在过去的几个月里,我在这个问题上变得非常绝望。感谢一千!

使用或不使用 Modrewrite 不会影响速度:两者都是一样的。我使用这些重写条件:RewriteCond %REQUEST_URI ^/IMG-.*$ & RewriteCond %REQUEST_FILENAME !-f

original default URL 和 beautified rewritten URL 都产生相同的延迟!!因此,让我们不要将故障归咎于闪电般快速的 Apache:它的 PHP 缓存 / 标头以某种方式错误编码...


webpagetest.org 警告:利用浏览器缓存静态资产:69/100

失败 - (没有最大年龄或过期):http://aster.nu/imgcpu?src=aster_bg/124.jpg&w=1400&h=100&c=p


每次刷新后,您都会看到two warnings appear on random at REDbot.org


代码的相关部分:

// Script is directly called
if(isset($_GET['src']) && (isset($_GET['w']) || isset($_GET['h']) || isset($_GET['m']) || isset($_GET['f']) || isset($_GET['q'])))
    $ImageProcessor = new ImageProcessor(true);
    $ImageProcessor->Load($_GET['src'], true);
    $ImageProcessor->EnableCache("/var/www/vhosts/blabla.org/httpdocs/tmp/", 345600);
    $ImageProcessor->Parse($quality);


/* Images processing class
 * - create image thumbnails on the fly
 * - Can be used with direct url imgcpu.php?src=
 * - Cache images for efficiency 
 */
class ImageProcessor

    private $_image_path;      # Origninal image path
    protected $_image_name;    # Image name   string
    private $_image_type;      # Image type  int    
    protected $_mime;          # Image mime type  string    
    private $_direct_call = false;   # Is it a direct url call?  boolean        
    private $_image_resource;  # Image resource   var Resource      
    private $_cache_folder;    # Cache folder strig
    private $_cache_ttl;        # Cache time to live  int
    private $_cache = false;    # Cache on   boolean
    private $_cache_skip = false;   # Cache skip   var boolean

    private function cleanUrl($image)   # Cleanup url
        $cimage = str_replace("\\", "/", $image);
        return $cimage;
       

    /** Get image resource
     * @access private, @param string $image, @param string $extension, @return resource  */
    private function GetImageResource($image, $extension)
        switch($extension)
            case "jpg":
                @ini_set('gd.jpeg_ignore_warning', 1);
                $resource = imagecreatefromjpeg($image);
                break;
        
        return $resource;
    


    /* Save image to cache folder
     * @access private, @return void  */
    private function cacheImage($name, $content)

        # Write content file
        $path = $this->_cache_folder . $name;
        $fh = fopen($path, 'w') or die("can't open file");
        fwrite($fh, $content);
        fclose($fh);

        # Delete expired images
        foreach (glob($this->_cache_folder . "*") as $filename) 
            if(filemtime($filename) < (time() - $this->_cache_ttl))
                unlink( $filename );
            
        
    

    /* Get an image from cache
     * @access public, @param string $name, @return void */
    private function cachedImage($name)
        $file = $this->_cache_folder . $name;
        $fh = fopen($file, 'r');
        $content = fread($fh,  filesize($file));
        fclose($fh);
        return $content;
    

    /* Get name of the cache file
     * @access private, @return string  */
    private function generateCacheName()
        $get = implode("-", $_GET);
        return md5($this->_resize_mode . $this->_image_path . $this->_old_width . $this->_old_height . $this->_new_width . $this->_new_height . $get) . "." . $this->_extension;
    

    /* Check if a cache file is expired
     * @access private,  @return bool  */
    private function cacheExpired()
        $path = $this->_cache_folder . $this->generateCacheName();
        if(file_exists($path))
            $filetime = filemtime($path);
            return $filetime < (time() - $this->_cache_ttl);
        else
            return true;
        
    

    /* Lazy load the image resource needed for the caching to work
     * @return void */
    private function lazyLoad()
        if(empty($this->_image_resource))
            if($this->_cache && !$this->cacheExpired())
                $this->_cache_skip = true;
                return;
            
            $resource = $this->GetImageResource($this->_image_path, $this->_extension);
            $this->_image_resource = $resource;
            
    

    /* Constructor
     * @access public, @param bool $direct_call, @return void */
    public function __construct($direct_call=false)

    # Check if GD extension is loaded
        if (!extension_loaded('gd') && !extension_loaded('gd2')) 
            $this->showError("GD is not loaded");
        

        $this->_direct_call = $direct_call;
    

    /* Resize
     * @param int $width, @param int $height, @param define $mode
     * @param bool $auto_orientation houd rekening met orientatie wanneer er een resize gebeurt */
    public function Resize($width=100, $height=100, $mode=RESIZE_STRETCH, $auto_orientation=false)

        // Validate resize mode
        $valid_modes = array("f", "p");
        
                     // .... omitted .....

        // Set news size vars because these are used for the
        // cache name generation
                 // .... omitted .....          
        $this->_old_width = $width;
        $this->_old_height = $height;

        // Lazy load for the directurl cache to work
        $this->lazyLoad();
        if($this->_cache_skip) return true;

        // Create canvas for the new image
        $new_image = imagecreatetruecolor($width, $height);

        imagecopyresampled($new_image, $this->_image_resource, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);

             // .... omitted .....

        $this->_image_resource = $new_image;
    

    /* Create image resource from path or url
     * @access public, @param string $location, @param bool $lazy_load, @return */
    public function Load($image,$lazy_load=false)

        // Cleanup image url
        $image = $this->cleanUrl($image);

        // Check if it is a valid image
        if(isset($mimes[$extension]) && ((!strstr($image, "http://") && file_exists($image)) || strstr($image, "http://")) )

            // Urlencode if http
            if(strstr($image, "http://"))
                $image = str_replace(array('http%3A%2F%2F', '%2F'), array('http://', '/'), urlencode($image));
            
            $image = str_replace("+", "%20", $image);

            $this->_extension = $extension;
            $this->_mime = $mimes[$extension];
            $this->_image_path = $image;
            $parts = explode("/", $image);
            $this->_image_name = str_replace("." . $this->_extension, "", end($parts));

            // Get image size
            list($width, $height, $type) = getimagesize($image);
            $this->_old_width = $width;
            $this->_old_height = $height;
            $this->_image_type = $type;
        else
            $this->showError("Wrong image type or file does not exists.");
        
        if(!$lazy_load)
            $resource = $this->GetImageResource($image, $extension);
            $this->_image_resource = $resource;
                   
    

    /* Save image to computer
     * @access public, @param string $destination, @return void  */
    public function Save($destination, $quality=60)
        if($this->_extension == "png" || $this->_extension == "gif")
            imagesavealpha($this->_image_resource, true); 
        
        switch ($this->_extension) 
            case "jpg": imagejpeg($this->_image_resource,$destination, $quality);   break;
            case "gif": imagegif($this->_image_resource,$destination);      break;
            default: $this->showError('Failed to save image!');             break;
                   
    

    /* Print image to screen
     * @access public, @return void */
    public function Parse($quality=60)
        $name = $this->generateCacheName();
        $content = "";
        if(!$this->_cache || ($this->_cache && $this->cacheExpired()))
            ob_start();
            header ("Content-type: " . $this->_mime);
            if($this->_extension == "png" || $this->_extension == "gif")
                imagesavealpha($this->_image_resource, true); 
            

            switch ($this->_extension) 
                case "jpg": imagejpeg($this->_image_resource, "", $quality);    break;
                case "gif": imagegif($this->_image_resource);   break;
                default: $this->showError('Failed to save image!');             break;
            

            $content = ob_get_contents();
            ob_end_clean();
        else

            if (isset ($_SERVER['HTTP_IF_MODIFIED_SINCE'])) 
                if (strtotime ($_SERVER['HTTP_IF_MODIFIED_SINCE']) < strtotime('now')) 
                    header ('HTTP/1.1 304 Not Modified');
                    die ();
                
            

            // change the modified headers
            $gmdate_expires = gmdate ('D, d M Y H:i:s', strtotime ('now +10 days')) . ' GMT';
            $gmdate_modified = gmdate ('D, d M Y H:i:s') . ' GMT';

            header ("Content-type: " . $this->_mime);
            header ('Accept-Ranges: bytes');
            header ('Last-Modified: ' . $gmdate_modified);
            header ('Cache-Control: max-age=864000, must-revalidate');
            header ('Expires: ' . $gmdate_expires);

            echo $this->cachedImage($name);
            exit();
        

        // Save image content
        if(!empty($content) && $this->_cache)
            $this->cacheImage($name, $content);
        

        // Destroy image
        $this->Destroy();

        echo $content;
        exit();
    

    /* Destroy resources
     * @access public,  @return void */
    public function Destroy()
        imagedestroy($this->_image_resource); 
    


    /* Get image resources
     * @access public,  @return resource */
    public function GetResource()
        return $this->_image_resource;
    

    /* Set image resources
     * @access public, @param resource $image, @return resource */
    public function SetResource($image)
        $this->_image_resource = $image;
    

    /* Enable caching
     * @access public, @param string $folder, @param int $ttl,   * @return void */
    public function EnableCache($folder="/var/www/vhosts/blabla.org/httpdocs/tmp/", $ttl=345600)
        if(!is_dir($folder))
            $this->showError("Directory '" . $folder . "' does'nt exist");
        else
            $this->_cache           = true;
            $this->_cache_folder    = $folder;
            $this->_cache_ttl       = $ttl;
        
        return false;
    

原作者允许我在这里放置部分代码来解决这个问题。


【问题讨论】:

这些图像是否通过某种 PHP 前端控制器运行? 这似乎是您之前问题的转贴。 @drewish:上一个问题的答案并没有真正解决 this 的具体问题。 我不明白他为什么不能发布一些代码。让我们尝试通过一些非常酷的截图来调试它是非常疯狂的。我确信代码正在做一些愚蠢的事情,比如动态调整图像的大小,而不是将它们写入文件并发送缓存的文件。 @drewish:我在有问题的图像上加载了 0.5 秒到 1 秒的时间,其中几乎 100% 的时间都花在了服务器上。不可怕,但比必要的要多。 【参考方案1】:

如果我正确理解了这个问题,这完全可以预料。图像处理很慢。

黄色是您的浏览器发送请求。绿色是您的浏览器在服务器上等待实际创建缩略图,无论服务器使用什么库,这都会花费大量时间。蓝色是发送响应的服务器,与前面的步骤不同,它受文件大小的影响。

对于图像处理固有的缓慢性,没有什么可做的。缓存这些缩略图是明智的,这样它们只生成一次,然后静态提供。这样一来,您的用户中很少有人需要等待绿色延迟,您的服务器也会很高兴。

编辑:如果问题是文件存在于这些 URL 中,但您的 RewriteRule 无论如何都会启动,请记住,默认情况下,规则运行时不会检查文件是否存在.

使用RewriteRule 上方的以下条件来确保文件存在。

RewriteCond %REQUEST_FILENAME !-f
RewriteRule # ...etc...

【讨论】:

+1。 @Matchu,对不起,我忘了提:拇指被缓存在他们单独的目录中,当我签入 ftp 时,它们是在那里生成的……所以这似乎有效……这些信息对你有帮助吗?让我猜猜:你想看源代码……会尝试在这里获得上传权限,暂时。 但是这里没有缩略图生成,是吗?这些是静态图像,不是吗? @Pekka: title 包含“PHP 缩略图生成器”,所以我会把钱用于加载时间的来源。 @Sam,如果您无法获得发布源代码的许可,请尝试一些标准的调试技术:在这里和那里放置一些 echo 语句以查看代码最终的去向以及原因。它显然没有命中缓存副本。 @Sam:脚本是如何工作的?是否给出了图像应该存在的 URL?请记住,mod_rewrite 默认情况下会在不检查文件是否存在的情况下启动。在RewriteRule 之前使用RewriteCond %REQUEST_FILENAME !-f 使规则仅在文件不存在时适用。 @Sam:嗯,我们可以合理地确定 PHP 正在提供该文件,因为这是唯一会真正导致该问题的事情......请求是以某种方式得到重定向到 PHP。 (除非您的服务器明确禁用它,否则您可能会从该图像请求中看到 X-Powered-By 标头。)那么,问题是为什么 PHP 会收到该请求,即使重写规则已关闭。有时像images.php 这样的PHP 文件也会处理对imagesimages/anything 的请求。这可能是这里的问题吗?【参考方案2】:

imgcpu.php?src=foo/foo.jpg&w=100&h=100

所以imgcpu.php 正在为每个图像请求运行?

在这种情况下,如果您担心性能,则:

脚本需要对其创建的缩略图进行一些缓存。如果它根据每个请求调整大小,那就是你的问题。

脚本需要向浏览器发送一些缓存头 - 纯 PHP 脚本不会这样做,并且会在每次页面加载时刷新

PHP 脚本中的session_start() 调用可能会因为会话锁定而导致并发问题。

您需要展示一些 PHP 代码。不过,也许在一个单独的问题中。

【讨论】:

+1 亲爱的@Pekka 我更新了我的分数:97 Page Speed 和 93 YSlow,据我所知非常高。 gtmetrix.com/reports/asterdesign.com/f4NK8SwG 所以你可以看到除了缩略图之外的所有东西都是完美的。我感觉它确实会根据每个请求调整大小。虽然缩略图保存在缓存文件夹中,但我检查了那里有尽可能多的 jpg 文件名,如 8237928379 代码作者尚未回复,希望很快...继续。 @Pekka,我终于获得了将部分代码放到网上的许可!耶!你有什么建议:有没有像jsfiddle.net 这样的地方,但是对于 PHP 代码,我可以将它粘贴到那里并提供一个测试链接?还是我应该把它们放在我的问题中?谢谢 @Sam 把它放在小提琴中可能是最好的主意,这个问题已经很拥挤了:) @Pekka,什么是小提琴,我该如何把它放在小提琴中?关联?谢谢。 @Sam 我的意思是jsfiddle.net。还有ideone.com 可以运行一些 PHP 代码,但不太可能与复杂的调整大小脚本一起使用。【参考方案3】:

Apache 从您的硬盘提供文件的速度比 PHP 快得多,看来您正在使用后者来处理缓存:

 /**
     * Get an image from cache
     * 
     * @access public
     * @param string $name
     * @return void
     */
    private function cachedImage($name)
        $file = $this->_cache_folder . $name;
        $fh = fopen($file, 'r');
        $content = fread($fh,  filesize($file));
        fclose($fh);
        return $content;
    

有一种更好的方法来执行该函数正在执行的操作 (passthru),但最好的选择是设置一个正则表达式,仅当文件不存在时才会重写对缩略图脚本的请求:

RewriteEngine On
RewriteCond %REQUEST_FILENAME -s [OR]
RewriteCond %REQUEST_FILENAME -l [OR]
RewriteCond %REQUEST_FILENAME -d
RewriteRule ^images/.*$ - [NC,L]
RewriteRule ^images/(.*)$ /imgcpu.php/$1 [NC,L]

然后引入逻辑来将请求解析为图像并进行相应的格式化。例如,您可以说拇指应该以原始文件命名并附加 W x H 尺寸,如“***_logo_100x100.jpg”。

有意义吗?


根据请求(在评论中),“s”、“l”和“d”标志的描述如下(引用文档):

'-d'(是目录) TestString 作为路径名和测试 不管它是否存在,并且是一个 目录。

'-s'(是 常规文件,有大小) TestString 作为路径名和测试 不管它是否存在,并且是一个 大小大于的常规文件 零。

'-l'(是符号链接) TestString 作为路径名和测试 不管它是否存在,并且是一个 符号链接。

【讨论】:

+1 并感谢@coreyward,您能否评论一下-s -l -d 的作用? (ps 我已经完全重写了我的问题,因为我现在有更多信息)谢谢! @Sam 我在答案中添加了对标志的描述。 +1,这是最好的方法,因为它可以避免您在每次请求时启动 PHP 进程。【参考方案4】:

Matchu 给了你答案。如果要修复它,请保存创建的缩略图,这样就不会在每次请求时重新创建它们。我使用简单的 404 页面来捕获对尚未创建的缩略图的请求,该脚本从 url 计算出所需的尺寸和文件 - 例如 /thumbs/100x100/cat.png 意味着从 /images/cat.png 创建 100x100 缩略图。

【讨论】:

【参考方案5】:

您在生成图像后检查您的 HTTP_IF_MODIFIED_SINCE 标头和缓存,以便每次加载页面时都会生成和缓存图像。如果在开始处理图像之前将这些检查移到更接近执行开始的位置,则时间会大大减少。

【讨论】:

以上是关于PHP 缩略图图像生成器缓存:如何在 PHP 中正确设置 If-Last-Modified/Max-Age/Last-Modified HEADERS?的主要内容,如果未能解决你的问题,请参考以下文章

PHP PHP的图像缩略图生成器

PHP 图像缩放器(缩略图生成器)

PHP GD缩略图生成图像像素失真

PHP gd 从生成的图像制作缩略图

PHP PHP代码生成当前目录中所有图像的缩略图

PHP图像缩略图生成器