2012-05-05 01:53

[轉載] 让PHP更快的提供文件下载

作者:Laruence
本文地址:http://www.laruence.com/2012/05/02/2613.html

一般来说, 我们可以通过直接让URL指向一个位于Document Root下面的文件, 来引导用户下载文件.

但是, 这样做, 就没办法做一些统计, 权限检查, 等等的工作. 于是, 很多时候, 我们采用让PHP来做转发, 为用户提供文件下载.

<?php
    $file = "/tmp/dummy.tar.gz";
    header("Content-type: application/octet-stream");
    header('Content-Disposition: attachment; filename="' 
        . basename($file) . '"');
    header("Content-Length: ". filesize($file));
    readfile($file);

但是这个有一个问题, 就是如果文件是中文名的话, 有的用户可能下载后的文件名是乱码.

于是, 我们做一下修改(参考: :

<?php
    $file = "/tmp/中文名.tar.gz";
 
    $filename = basename($file);
 
    header("Content-type: application/octet-stream");
 
    //处理中文文件名
    $ua = $_SERVER["HTTP_USER_AGENT"];
    $encoded_filename = urlencode($filename);
    $encoded_filename = str_replace("+", "%20", $encoded_filename);
    if (preg_match("/MSIE/", $ua)) {
        header('Content-Disposition: attachment; filename="'
            . $encoded_filename . '"');
    } else if (preg_match("/Firefox/", $ua)) {
        header("Content-Disposition: attachment; filename*=\"utf8''"
            . $filename . '"');
    } else {
        header('Content-Disposition: attachment; filename="'
            . $filename . '"');
    }
 
    header('Content-Disposition: attachment; filename="'
        . $filename . '"');
    header("Content-Length: ". filesize($file));
    readfile($file);

恩, 现在看起来好多了, 不过还有一个问题, 那就是readfile, 虽然PHP的readfile尝试实现的尽量高效, 不占用PHP本身的内存, 但是实际上它还是需要采用MMAP(如果支持), 或者是一个固定的buffer去循环读取文件, 直接输出.

输出的时候, 如果是Apache + PHP mod, 那么还需要发送到Apache的输出缓冲区. 最后才发送给用户. 而对于Nginx + fpm如果他们分开部署的话, 那还会带来额外的网络IO.

那么, 能不能不经过PHP这层, 直接让Webserver直接把文件发送给用户呢?

今天, 我看到了一个有意思的文章: How I PHP: X-SendFile.

我们可以使用Apache的module mod_xsendfile, 让Apache直接发送这个文件给用户:

<?php
    $file = "/tmp/中文名.tar.gz";
 
    $filename = basename($file);
 
    header("Content-type: application/octet-stream");
 
    //处理中文文件名
    $ua = $_SERVER["HTTP_USER_AGENT"];
    $encoded_filename = urlencode($filename);
    $encoded_filename = str_replace("+", "%20", $encoded_filename);
    if (preg_match("/MSIE/", $ua)) {
        header('Content-Disposition: attachment; filename="'
            . $encoded_filename . '"');
    } else if (preg_match("/Firefox/", $ua)) {
        header("Content-Disposition: attachment; filename*=\"utf8''"
            . $filename . '"');
    } else {
        header('Content-Disposition: attachment; filename="'
            . $filename . '"');
    }
 
    header('Content-Disposition: attachment; filename="'
            . basename($file) . '"');
 
    //让Xsendfile发送文件
    header("X-Sendfile: $file");

X-Sendfile头将被Apache处理, 并且把响应的文件直接发送给Client.

Lighttpd和Nginx也有类似的模块, 大家有兴趣的可以去找找看


mod-xsendfile 在 Ubuntu 的安裝方法:
sudo apt-get install libapache2-mod-xsendfile
2012-05-05 01:27

[PHP] 使用 php5-ffmpeg 擷取影片圖片

前幾天在玩 FFmpeg 的時後,突然發現 Ubuntu 上多了 php5-ffmpeg 這個擴充套件,就想來玩玩看,看好不好用,有兩個結論:
  1. 讀取影片取決於 FFmpeg 的支援性,如果想要什麼格式都支援的話,建議自己重新編譯 FFmpeg。
  2. 效率並沒有我想像中的快,兩分鐘的影片取十張圖,大約 30 秒。
安裝方法:
sudo apt-get install ffmpeg php5-ffmpeg php5-gd

擷圖測試範例:
<?php
$page = 10;
$prefix = 'screencap';

$mov = new ffmpeg_movie('gt.avi');
$count = $mov->getFrameCount();
$range = (int)round($count/($page+1));

for($i=1; $i<=$page; $i++){
    $frameNum = $range*$i;
    $imgFile = $prefix.'_'.$i.'.png';

    $frame = $mov->getFrame($frameNum);
    if(!$frame){ continue; }

    $gdImage = $frame->toGDImage();
    if(!$gdImage){ continue; }

    imagepng($gdImage, $imgFile);
    imagedestroy($gdImage);

    echo '<img src="'.$imgFile.'" border="1" /><br />';
}

參考文件:
ffmpeg_movie object methods
FFmpeg and PHP
2012-05-03 14:49

[Ubuntu 11] HTTP Live Streaming 安裝與測試

安裝 Apache
apt-get install apache2

/etc/apache2/mods-available/mime.conf 加入以下內容:
# HLS file type
AddType application/x-mpegURL .m3u8
AddType video/MP2T .ts


Ubuntu 11.04 預設的 FFmpeg 是沒有啟用 h.264 的支援,所以要自己編譯。

安裝編譯時所需要的套件
apt-get update
apt-get install build-essential checkinstall subversion git libfaac-dev libjack-jackd2-dev libmp3lame-dev libopencore-amrnb-dev libopencore-amrwb-dev libsdl1.2-dev libtheora-dev libva-dev libvdpau-dev libvorbis-dev libx11-dev libxfixes-dev libxvidcore-dev texi2html yasm zlib1g-dev libx264-dev librtmp-dev


編譯 FFmpeg
cd /opt
git clone git://git.videolan.org/ffmpeg
cd ffmpeg

./configure --prefix=/usr --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libfaac --enable-libmp3lame --enable-libtheora --enable-libvorbis --enable-libx264 --enable-libxvid --disable-ffplay --enable-shared --enable-gpl --enable-postproc --enable-version3 --enable-nonfree --enable-avfilter --enable-pthreads --enable-vdpau --enable-librtmp 

make -j$(grep processor /proc/cpuinfo |wc -l)

checkinstall --pkgname=ffmpeg --pkgversion="5.0.1" --backup=no --deldoc=yes --default


segmenter 是用來切割 FFmpeg 轉好的 ts 檔
編譯 segmenter
cd /opt
svn co http://httpsegmenter.googlecode.com/svn/trunk
cd trunk

sed 's/\/local//g' Makefile.txt > Makefile

make -j$(grep processor /proc/cpuinfo |wc -l)
checkinstall --pkgname=segmenter --pkgversion="2" --backup=no --deldoc=yes --default



測試影片轉檔
cd /var/www
ffmpeg -i gt4.avi -f mpegts -vcodec libx264 -acodec libmp3lame gt4_pre.ts

ffmpeg -i gt4.avi -f mpegts -acodec libmp3lame -ar 48000 -ab 64k -s 720x480 -vcodec libx264 -b 800k -flags +loop -cmp +chroma -partitions +parti4x4+partp8x8+partb8x8 -subq 5 -trellis 1 -refs 1 -coder 0 -me_range 16 -keyint_min 25 -sc_threshold 40 -i_qfactor 0.71 -bt 200k -maxrate 800k -bufsize 800k -rc_eq 'blurCplx^(1-qComp)' -qcomp 0.6 -qmin 10 -qmax 51 -qdiff 4 -level 30 -aspect 720:480 -g 30 -async 2 gt4_pre.ts


測試 RTMP 串接轉檔
ffmpeg -i rtmp://flashstream.adobe.com/ondemand/flash/cs5/prod/production-performance_400x224 -f mpegts -vcodec libx264 -acodec libmp3lame rtmp_pre.ts


測試影片切割
segmenter -i gt4_pre.ts -d 10 -o gt4_pre -x stream.m3u8 -p http://192.168.0.10/


利用 pipe 從轉檔到切割
ffmpeg -i gt4.avi -f mpegts -vcodec libx264 -acodec libmp3lame - |segmenter -i - -d 10 -o gt4_pre -x stream.m3u8 -p http://192.168.0.10/


m3u8 檔案格式
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXTINF:11,
http://192.168.0.10/gt4_pre-1.ts
#EXTINF:11,
http://192.168.0.10/gt4_pre-2.ts
#EXTINF:11,
http://192.168.0.10/gt4_pre-3.ts
#EXTINF:11,
http://192.168.0.10/gt4_pre-4.ts
#EXTINF:5,
http://192.168.0.10/gt4_pre-5.ts
#EXT-X-ENDLIST


以 HTML5 播放影片
<html>
  <head><title>Video Test</title></head>
  <body>
    <center>
      <video tabindex="0" width="720" height="480"><source src="/stream.m3u8"></video>
    </center>
  </body>
</html>


參考文件:
How to compile and install ffmpeg in Ubuntu 11.04
Install FFmpeg and x264 on Ubuntu Lucid Lynx 10.04 LTS
iPhone HTTP Streaming with FFMpeg and an Open Source Segmenter
HTTP Live Streaming draft-pantos-http-live-streaming-08
http live streaming(m3u8 streaming)(m3u8)
2012-04-29 21:16

[Linux] 檢查 SAMBA 與 NFS Server 是否存在

通常會透過 /etc/fstab 來處理掛載的設定,然後再使用 mount -a 來重新確認掛載,最好在排程的程序用到掛載目錄時也執行一次 mount -a,掛載目錄在斷線後是不會自動回復的,mount -a 的 Timeout 其實還蠻久的,尤其是 Server 不存在的時候,所以最好還是用對應的 client 先確認 server 是否存在。

而檢查 NFS 的 client 可以用 showmount 來處理,在 Ubuntu 上的安裝方式如下:
sudo aptitude install nfs-common

而 SAMBA 的 client 則是用 smbclient,在 Ubuntu 上的安裝方式如下:
sudo aptitude install smbclient


檢查 NFS Server 是否存在的流程

以 Shell 的方式檢查
# 先以 client 確認 server 是否存在
/sbin/showmount 192.168.0.6 >/dev/null 2>&1
if [ "j$?" != "j0" ]; then  
    echo "NFS Server is not exist"
    exit 1
fi

# 重新確認掛載 
mount -a >/dev/null 2>&1
if [ "j$?" != "j0" ]; then
    echo "NFS Server mount failed"
    exit 1;
fi

以 PHP 的方式檢查
/*先以 client 確認 server 是否存在*/ 
$state = shell_exec('/sbin/showmount 192.168.0.6 >/dev/null 2>&1; echo $?');
if(trim($state)!='0'){
    echo "NFS Server is not exist";
    exit;
}

/*重新確認掛載*/ 
if(shell_exec('mount -a 2>&1')){
    echo "NFS Server mount failed"
    exit;
}



檢查 SAMBA Server 是否存在的流程

以 Shell 的方式檢查
# 先以 client 確認 server 是否存在
smbclient -NL //192.168.0.6 >/dev/null 2>&1
if [ "j$?" != "j0" ]; then  
    echo "SAMBA Server is not exist"
    exit 1
fi

# 重新確認掛載 
mount -a >/dev/null 2>&1
if [ "j$?" != "j0" ]; then
    echo "SAMBA Server mount failed"
    exit 1;
fi

以 PHP 的方式檢查
/*先以 client 確認 server 是否存在*/ 
$state = shell_exec('smbclient -NL //192.168.0.6 >/dev/null 2>&1; echo $?');
if(trim($state)!='0'){
    echo "SAMBA Server is not exist";
    exit;
}

/*重新確認掛載*/ 
if(shell_exec('mount -a 2>&1')){
    echo "SAMBA Server mount failed"
    exit;
}
2012-04-23 16:47

[PHP] 簡易的圖片相似度比較

由於相似图片搜索的php实现的 API 不怎麼符合我的用途,所以我重新定義 API 的架構,改寫成比較簡單的函數方式,雖然還是用物件的方式包裝。

<?php
/**
 * 圖片相似度比較
 *
 * @version     $Id: ImageHash.php 4429 2012-04-17 13:20:31Z jax $
 * @author      jax.hu
 *
 * <code>
 *  //Sample_1
 *  $aHash = ImageHash::hashImageFile('wsz.11.jpg');
 *  $bHash = ImageHash::hashImageFile('wsz.12.jpg');
 *  var_dump(ImageHash::isHashSimilar($aHash, $bHash));
 *
 *  //Sample_2
 *  var_dump(ImageHash::isImageFileSimilar('wsz.11.jpg', 'wsz.12.jpg'));
 * </code>
 */

class ImageHash {

    /**取樣倍率 1~10
     * @access public
     * @staticvar int
     * */
    public static $rate = 2;

    /**相似度允許值 0~64
     * @access public
     * @staticvar int
     * */
    public static $similarity = 80;

    /**圖片類型對應的開啟函數
     * @access private
     * @staticvar string
     * */
    private static $_createFunc = array(
        IMAGETYPE_GIF   =>'imageCreateFromGIF',
        IMAGETYPE_JPEG  =>'imageCreateFromJPEG',
        IMAGETYPE_PNG   =>'imageCreateFromPNG',
        IMAGETYPE_BMP   =>'imageCreateFromBMP',
        IMAGETYPE_WBMP  =>'imageCreateFromWBMP',
        IMAGETYPE_XBM   =>'imageCreateFromXBM',
    );


    /**從檔案建立圖片
     * @param string $filePath 檔案位址路徑
     * @return resource 當成功開啟圖片則回傳圖片 resource ID,失敗則是 false
     * */
    public static function createImage($filePath){
        if(!file_exists($filePath)){ return false; }

        /*判斷檔案類型是否可以開啟*/
        $type = exif_imagetype($filePath);
        if(!array_key_exists($type,self::$_createFunc)){ return false; }

        $func = self::$_createFunc[$type];
        if(!function_exists($func)){ return false; }

        return $func($filePath);
    }


    /**hash 圖片
     * @param resource $src 圖片 resource ID
     * @return string 圖片 hash 值,失敗則是 false
     * */
    public static function hashImage($src){
        if(!$src){ return false; }

        /*缩小圖片尺寸*/
        $delta = 8 * self::$rate;
        $img = imageCreateTrueColor($delta,$delta);
        imageCopyResized($img,$src, 0,0,0,0, $delta,$delta,imagesX($src),imagesY($src));

        /*計算圖片灰階值*/
        $grayArray = array();
        for ($y=0; $y<$delta; $y++){
            for ($x=0; $x<$delta; $x++){
                $rgb = imagecolorat($img,$x,$y);
                $col = imagecolorsforindex($img, $rgb);
                $gray = intval(($col['red']+$col['green']+$col['blue'])/3)& 0xFF;

                $grayArray[] = $gray;
            }
        }
        imagedestroy($img);

        /*計算所有像素的灰階平均值*/
        $average = array_sum($grayArray)/count($grayArray);

        /*計算 hash 值*/
        $hashStr = '';
        foreach ($grayArray as $gray){
            $hashStr .= ($gray>=$average) ? '1' : '0';
        }

        return $hashStr;
    }


    /**hash 圖片檔案
     * @param string $filePath 檔案位址路徑
     * @return string 圖片 hash 值,失敗則是 false
     * */
    public static function hashImageFile($filePath){
        $src = self::createImage($filePath);
        $hashStr = self::hashImage($src);
        imagedestroy($src);

        return $hashStr;
    }


    /**比較兩個 hash 值,是不是相似
     * @param string $aHash A圖片的 hash 值
     * @param string $bHash B圖片的 hash 值
     * @return bool 當圖片相似則回傳 true,否則是 false
     * */
    public static function isHashSimilar($aHash, $bHash){
        $aL = strlen($aHash); $bL = strlen($bHash);
        if ($aL !== $bL){ return false; }

        /*計算容許落差的數量*/
        $allowGap = $aL*(100-self::$similarity)/100;

        /*計算兩個 hash 值的漢明距離*/
        $distance = 0;
        for($i=0; $i<$aL; $i++){
            if ($aHash{$i} !== $bHash{$i}){ $distance++; }
        }

        return ($distance<=$allowGap) ? true : false;
    }


    /**比較兩個圖片檔案,是不是相似
     * @param string $aHash A圖片的路徑
     * @param string $bHash B圖片的路徑
     * @return bool 當圖片相似則回傳 true,否則是 false
     * */
    public static function isImageFileSimilar($aPath, $bPath){
        $aHash = ImageHash::hashImageFile($aPath);
        $bHash = ImageHash::hashImageFile($bPath);
        return ImageHash::isHashSimilar($aHash, $bHash);
    }

}
2012-04-12 14:03

[Ubuntu] 時間同步設定

繼上次Ubuntu 時間及時區設定 [Linux]那篇文章後,最近有學到新的時間同步的設定方式。

vim /etc/cron.daily/ntpdate
#!/bin/bash

# Sync NTP Server
ntpdate -s tock.stdtime.gov.tw watch.stdtime.gov.tw time.stdtime.gov.tw clock.stdtime.gov.tw tick.stdtime.gov.tw ntp.ubuntu.com pool.ntp.org

# Update BIOS time
hwclock --systohc

chmod +x /etc/cron.daily/ntpdate
2012-04-11 21:09

[MySQL] 整數型態的大小

類型Byte肯定位數最大位數最大值無符號最大值
TINYINT123127255
SMALLINT2453276765535
MEDIUMINT378838860716777215
INT491021474836474294967296
BIGINT81920922337203685477580718446744073709551615
2012-03-16 22:55

[PHP] 檢查 XML 文件結構

利用 SimpleXML 去檢查 XML 結構是否符合規格,為了讓這個程式可以多用途,採用了一個基準文件的作為結構準則,依據裡面定義的節點跟屬性,去檢查文件是否符合基本要求的格式。

<?php

/**檢查 XML 文件結構
 * @param string $baseFilePath 基準結構文件
 * @param string $checkFilePath 待檢查文件
 * @return bool 當結構與基準文件相符合時則回傳 true,否則是 false
 * */
function checkXmlFileStructure($baseFilePath,$checkFilePath){
    /*開啟 Base File*/
    if(!file_exists($baseFilePath)){ return false; }
    $base = simplexml_load_file($baseFilePath);
    if($base===false){ return false; }

    /*開啟 Check File*/
    if(!file_exists($checkFilePath)){ return false; }
    $check = simplexml_load_file($checkFilePath);
    if($check===false){ return false; }

    /*比較起始點的名稱*/
    if($base->getName() != $check->getName()){ return false; }

    /*比較結構*/
    return checkXmlStructure($base,$check);
}

/**檢查 XML 結構
 * @param SimpleXMLElement $base 基準結構物件
 * @param SimpleXMLElement $check 待檢查 XML 物件
 * @return bool 當結構與基準物件相符合時則回傳 true,否則是 false
 * */
function checkXmlStructure($base,$check){
    /*檢查屬性*/
    foreach ($base->attributes() as $name => $baseAttr){
        /*必要的屬性不存在*/
        if(!isset($check->attributes()->$name)){ return false; }
    }

    /*當沒有子節點時,則檢查對象也不能有子節點*/
    if(count($base->children())==0){
        return (count($check->children())==0);
    }

    /*將檢查對象的子節點分群*/
    $checkChilds = array();
    foreach($check->children() as $name => $child){
        $checkChilds[$name][] = $child;
    }

    /*檢查子節點*/
    $checked = array();
    foreach($base->children() as $name => $baseChild){
        /*跳過已經檢查的子節點*/
        if(in_array($name, $checked)){ continue; }
        $checked[] = $name;

        /*檢查必要的子節點是否存在*/
        if(empty($checkChilds[$name])){ return false; }

        foreach ($checkChilds[$name] as $child){
            /*遞迴檢查子節點*/
            if( !checkXmlStructure($baseChild, $child) ){ return false; }
        }
    }

    return true;
}


/*==============================================================================*/

if(isset($_SERVER['argv'])){
    parse_str(preg_replace('/&[\-]+/','&',join('&',$_SERVER['argv'])), $_GET);

    if(empty($_GET['base_file']) || empty($_GET['check_file'])){
        echo "Run: ".basename(__FILE__)." base_file=base.xml check_file=check.xml\n"; exit(1);
    }

    exit( checkXmlFileStructure($_GET['base_file'],$_GET['check_file']) ? 0 : 1);

}else{
    if(empty($_GET['base_file']) || empty($_GET['check_file'])){
        echo "Run: ".basename(__FILE__)."?base_file=base.xml&check_file=check.xml<br />"; exit;
    }

    echo( checkXmlFileStructure($_GET['base_file'],$_GET['check_file']) ? '1' : '0');
}


使用方式(shell)
php check_xml_file_structure.php base_file=base.xml check_file=check.xml

if [ "j$?" != "j0" ]; then
    echo "Run Error"
fi


測試範例 1
base_1.xml
<?xml version="1.0" encoding="UTF-8"?>
<items>
    <item>
        <Category>Category文字</Category>
        <Title>Title文字</Title>
    </item>
</items>

check_1.xml
<?xml version="1.0" encoding="UTF-8"?>
<items>
    <item>
        <Category>Category文字</Category>
        <Title>Title文字</Title>
    </item>
    <item>
        <Category>Category文字</Category>
        <Title>Title文字</Title>
        <Description>Description文字</Description>
    </item>
</items>


測試範例 2
base_2.xml
<?xml version="1.0" encoding="UTF-8"?>
<items>
    <item category="Category文字" Title="Title文字"/>
</items>

check_2.xml
<?xml version="1.0" encoding="UTF-8"?>
<items>
    <item category="Category文字" Title="Title文字" Description="Description文字" />
    <item category="Category文字" Title="Title文字" />
    <item category="Category文字" Title="Title文字" Description="Description文字" />
</items>