[php]一个发送文件的函数

这是前段时间在live-share.com上使用的文件发送函数,后来由于这种方式在高并发下负载过大,而放弃使用。此函数支持多线及断点续传,在非windows服务器下可限制速度,windows服务器因为不支持usleep函数,但用sleep函数效果不理想,所以放弃。此函数并不完全符合HTTP1.1标准,其中的断点续传部分没有考虑多个range的情况(事实上我还没见过哪个下载软件这么变态)。

下载: sendfile.php
  1. /**
  2. * 发送文件
  3. *
  4. * @author: legend(legendsky@hotmail.com)
  5. * @link: http://www.ugia.cn/?p=109
  6. * @description: send file to client
  7. * @version: 1.0
  8. *
  9. * @param string   $fileName      文件名称或路径
  10. * @param string   $fancyName     自定义的文件名,为空则使用filename
  11. * @param boolean  $forceDownload 是否强制下载
  12. * @param integer  $speedLimit    速度限制,单位为字节,0为不限制,不支持windows服务器
  13. * @param string   $$contentType  文件类型,默认为application/octet-stream
  14. *
  15. * @return boolean
  16. */
  17. function sendFile($fileName, $fancyName = '', $forceDownload = true, $speedLimit = 0, $contentType = '')
  18. {
  19.     if (!is_readable($fileName))
  20.     {
  21.         header("HTTP/1.1 404 Not Found");
  22.         return false;
  23.     }
  24.  
  25.     $fileStat = stat($fileName);
  26.     $lastModified = $fileStat['mtime'];
  27.    
  28.     $md5 = md5($fileStat['mtime'] .'='. $fileStat['ino'] .'='. $fileStat['size']);
  29.     $etag = '"' . $md5 . '-' . crc32($md5) . '"';
  30.  
  31.     header('Last-Modified: ' . gmdate("D, d M Y H:i:s", $lastModified) . ' GMT');
  32.     header("ETag: $etag");
  33.    
  34.     if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= $lastModified)
  35.     {
  36.         header("HTTP/1.1 304 Not Modified");
  37.         return true;
  38.     }
  39.  
  40.     if (isset($_SERVER['HTTP_IF_UNMODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_UNMODIFIED_SINCE']) < $lastModified)
  41.     {
  42.         header("HTTP/1.1 304 Not Modified");
  43.         return true;
  44.     }
  45.  
  46.     if (isset($_SERVER['HTTP_IF_NONE_MATCH']) &&  $_SERVER['HTTP_IF_NONE_MATCH'] == $etag)
  47.     {
  48.         header("HTTP/1.1 304 Not Modified");
  49.         return true;
  50.     }
  51.  
  52.     if ($fancyName == '')
  53.     {
  54.         $fancyName = basename($fileName);
  55.     }
  56.    
  57.     if ($contentType == '')
  58.     {
  59.         $contentType = 'application/octet-stream';
  60.     }
  61.  
  62.     $fileSize = $fileStat['size'];  
  63.    
  64.     $contentLength = $fileSize;
  65.     $isPartial = false;
  66.  
  67.     if (isset($_SERVER['HTTP_RANGE']))
  68.     {
  69.         if (preg_match('/^bytes=(\d*)-(\d*)$/', $_SERVER['HTTP_RANGE'], $matches))
  70.         {   
  71.             $startPos = $matches[1];
  72.             $endPos = $matches[2];
  73.  
  74.             if ($startPos == '' && $endPos == '')
  75.             {
  76.                 return false;
  77.             }
  78.            
  79.             if ($startPos == '')
  80.             {
  81.                 $startPos = $fileSize - $endPos;
  82.                 $endPos = $fileSize - 1;
  83.             }
  84.             else if ($endPos == '')
  85.             {
  86.                 $endPos = $fileSize - 1;
  87.             }
  88.  
  89.             $startPos = $startPos < 0 ? 0 : $startPos;
  90.             $endPos = $endPos > $fileSize - 1 ? $fileSize - 1 : $endPos;
  91.  
  92.             $length = $endPos - $startPos + 1;
  93.  
  94.             if ($length < 0)
  95.             {
  96.                 return false;
  97.             }
  98.  
  99.             $contentLength = $length;
  100.             $isPartial = true;
  101.         }
  102.     }
  103.    
  104.     // send headers
  105.     if ($isPartial)
  106.     {
  107.         header('HTTP/1.1 206 Partial Content');
  108.         header("Content-Range: bytes $startPos-$endPos/$fileSize");
  109.        
  110.     }
  111.     else
  112.     {
  113.         header("HTTP/1.1 200 OK");
  114.         $startPos = 0;
  115.         $endPos = $contentLength - 1;
  116.     }
  117.  
  118.     header('Pragma: cache');
  119.     header('Cache-Control: public, must-revalidate, max-age=0');
  120.     header('Accept-Ranges: bytes');
  121.     header('Content-type: ' . $contentType);
  122.     header('Content-Length: ' . $contentLength);
  123.    
  124.     if ($forceDownload)
  125.     {
  126.         header('Content-Disposition: attachment; filename="' . rawurlencode($fancyName). '"');
  127.     }
  128.  
  129.     header("Content-Transfer-Encoding: binary");
  130.    
  131.     $bufferSize = 2048;
  132.  
  133.     if ($speedLimit != 0)
  134.     {
  135.         $packetTime = floor($bufferSize * 1000000 / $speedLimit);
  136.     }
  137.  
  138.     $bytesSent = 0;
  139.     $fp = fopen($fileName, "rb");
  140.     fseek($fp, $startPos);
  141.  
  142.     //fpassthru($fp);
  143.    
  144.     while ($bytesSent < $contentLength && !feof($fp) && connection_status() == 0 )
  145.     {
  146.         if ($speedLimit != 0)
  147.         {
  148.             list($usec, $sec) = explode(" ", microtime());
  149.             $outputTimeStart = ((float)$usec + (float)$sec);
  150.         }
  151.  
  152.         $readBufferSize = $contentLength - $bytesSent < $bufferSize ? $contentLength - $bytesSent : $bufferSize;
  153.         $buffer = fread($fp, $readBufferSize);       
  154.  
  155.         echo $buffer;
  156.  
  157.         ob_flush();
  158.         flush();
  159.  
  160.         $bytesSent += $readBufferSize;
  161.        
  162.         if ($speedLimit != 0)
  163.         {
  164.             list($usec, $sec) = explode(" ", microtime());
  165.             $outputTimeEnd = ((float)$usec + (float)$sec);
  166.            
  167.             $useTime = ((float) $outputTimeEnd - (float) $outputTimeStart) * 1000000;
  168.             $sleepTime = round($packetTime - $useTime);
  169.             if ($sleepTime > 0)
  170.             {
  171.                 usleep($sleepTime);
  172.             }
  173.         }
  174.     }
  175.    
  176.  
  177.     return true;
  178. }

demo: 下载:周杰伦-《千里之外》

<?php
sendFile
('./wp-data/jay_chou_-_outside_great_distance.mp3''outside_great_distance.mp3');
?>

33 Comments »

  1. 司马易风 said,

    September 21, 2006 @ 8:04 am

    晕晕~~~我也常碰到断线,然后又得重新来过,但是目前我的程序水平太差,还看不太懂,继续努力中……

  2. 靓华 said,

    September 21, 2006 @ 9:20 am

    晕,怎么下载了113K就完成了……说是有6MB多的

  3. legend said,

    September 21, 2006 @ 10:07 am

    是这样的,这个服务器有个设置,超过一定请求数,重启apache。可能是不巧让你碰上了,重新下,呵呵。

  4. Phzzy said,

    September 21, 2006 @ 10:48 am

    没看懂..-.-”
    header 太多..N多没见过..哈..第一见到 ETag, 刚查了下才知道是啥意思:)

  5. longbill said,

    September 26, 2006 @ 4:27 pm

    太强了,我以前也做过类似的东西,但是都没有实现短点续传功能,,

  6. rf said,

    September 27, 2006 @ 5:51 pm

    cache真是无处不在啊

  7. Jay said,

    October 10, 2006 @ 12:53 am

    服务器需要怎么配置吗?

  8. legend said,

    October 10, 2006 @ 1:16 am

    不需要

  9. feifengxlq said,

    October 27, 2006 @ 11:45 pm

    强悍~

  10. ahu said,

    November 26, 2006 @ 6:01 pm

    嗯,不错!

  11. rec0n said,

    January 2, 2007 @ 7:57 pm

    非常好。

  12. gzty said,

    March 7, 2007 @ 1:37 pm

    不错
    不知道对超过100M的文件支持怎么样

  13. legend said,

    March 7, 2007 @ 1:53 pm

    呵呵,几个G的都发过。

  14. cc said,

    March 12, 2007 @ 11:01 am

    没有注释,看不懂!

  15. gg said,

    March 23, 2007 @ 1:45 am

    这个类有问题,下载文件,头部会多出几个字节,

  16. legend said,

    March 23, 2007 @ 6:42 am

    把你的文件发到这里看看。

  17. cglc63 said,

    April 25, 2007 @ 3:51 pm

    哎呀,我的妈呀,很是有写难度哦,加油努力ing…

  18. xutao said,

    May 9, 2007 @ 10:30 am

    我用了你这个类,确实下载下来的文件,在头部会多出一个字节,是空格。
    怎么解决?望指教阿。
    我试验过下载xml文件,txt文件,tar文件,都是一个现象。
    如果你们用了没有问题,可能和php或者apache的设置有关,不懂,高手指教。

    非常感谢。

  19. legend said,

    May 10, 2007 @ 12:20 am

    告诉我你们的服务器版本和php版本,并把出问题的php文件和下载过的错误文件前几个字节贴上来看看,谢谢!

    我这里是没有问题。

  20. huhu said,

    June 14, 2007 @ 8:31 pm

    不知道如何使用 下载地址该放到哪里? 还有就是上面的代码该设置到什么地方!

  21. 啊哈 said,

    August 26, 2007 @ 5:10 pm

    唉,用这个函数,下载人数一多,内存使用量飙升啊

  22. 啊哈 said,

    August 26, 2007 @ 5:10 pm

    有没有更好的解决方法?

  23. baidu said,

    November 20, 2007 @ 8:50 am

    强..

  24. redlz2500 said,

    December 27, 2007 @ 11:02 pm

    非常感谢!

  25. juli said,

    January 19, 2008 @ 2:46 pm

    请问你与 Live-Share.com 是什么关系的呢??

  26. juli said,

    January 19, 2008 @ 4:19 pm

    最近才发现这里,四处看了一下,觉得挺好.
    刚接触这东西,有很多不懂的.也来发表一下意见.错了请各位莫见怪.
     1,”其中的断点续传部分没有考虑多个range的情况”
       我觉得这个不是你函数该处理的问题,也不是服务器该处理的问题.从最基本的层面进行设计,很多东西都是一问一答的简单实现.就好比移动的客服与客户,通常都是每个客户拿自己的手机拨客服电话,而客服也只需要一个服务员接一个电话,没必要假设:当这个问完了,如果不挂机,换另一个人再来接着说,是否要再报一下工号的问题.
    因此,我认为服务端只需要处理从哪一点开始取,到哪点结束就行了.现在的下载软件多线程分段也是这样,每线程只负责不同的起点请求,而不同起点间的区段问题,它自己处理,协调,在适当的时候中止线程. 如果是这样,那么这个函数和IIS的机制一样,请求越多,对应的处理越多,点资源越多.这不是一个(仅)发送功能函数的问题.
     2,既然这是已不采用的,那么现在采用的是什么呢?

  27. legend said,

    January 19, 2008 @ 8:32 pm

    只处理一个range的情况

  28. peak said,

    August 21, 2008 @ 2:20 pm

    看看。不错。

  29. diybl said,

    August 24, 2008 @ 11:47 am

    有下载的吗

  30. chenwl said,

    November 25, 2008 @ 2:55 pm

    这个是下载的啊?不是上传的哦

  31. junguo said,

    March 18, 2009 @ 9:42 pm

    请问楼主,这个程序怎么用啊?
    我用了之后出现错误

    Warning: Cannot modify header information – headers already sent by (output started at E:\APMServ5.2.0\www\htdocs\bohuafair.com\down.php:2) in E:\APMServ5.2.0\www\htdocs\bohuafair.com\down.php on line 10

  32. jmzz said,

    May 11, 2009 @ 7:10 pm

    无法使用迅雷和flashget等下载工具下载?

  33. huting said,

    May 4, 2012 @ 10:24 am

    虽然很强大,但这只是下载文件代码

RSS feed for comments on this post

Leave a Comment