小结文件的锁定机制、上传和下载php
1.文件锁定html
如今都在讲究什么分布式、并发等,实际上文件的操做也是并发的,在网络环境下,多个用户在同一时刻访问页面,对同一服务器上的同一文件进行着读取,若是,这个用户恰好读到一半,另外一个用户就写入了消息,那么前一个用户读到的就是错误数据,在数据库里面好像是称为脏数据,而若是某用户写到一半时,另外一用户也对该文件进行写操做,那么就形成了写入数据的混乱和错误,所以才php有一个锁机制,相似于数据库的锁,当某用户在对文件操做时就加上某种锁,使得在同一时间其余用户不能对该文件进行操做或只能进行有限的操做,来保证在这些状况下的文件数据的正确性。linux
主要使用flock函数,原型:bool flock(resource $handle , int $operation [, int &$wouldblock ]),第一个参数是指向文件的句柄变量,第二个是加锁的方式,分别为web
LOCK_SH:共享锁(share),在读取文件时加的锁,加锁后就其余用户不能再对该文件进行写,但能够读取该文件内容;数据库
LOCK_EX:排他锁(exclude),或者叫独占锁,在写文件时使用,加了该锁后,只能是当前用户进行写操做,其余的用户不能读取和写入;windows
LOCK_NB:附加锁,在文件锁定短期大量用户的访问操做可能会形成flock在锁定时堵塞,若是再加上该锁后可避免该状况(是否是这么一弄就能解决大量读写操做的问题,怕不行...);数组
LOCK_UN:释放锁,对前面的各类锁进行一次性释放,解锁。浏览器
若是容易堵塞,还可以使用第三个参数wouldblock,若是把它设置为1,在锁定后就会阻挡其余进程来进行一些操做,可是windows上不支持,另外附加锁LOCK_NB,windows也是不支持的。缓存
另外,关闭句柄变量的fclose操做也能够释放这些锁。安全
废话少说,看代码
<?php function readFileData($filename){ if(true == ($handle = fopen($filename, 'r'))){ if(flock($handle, LOCK_SH+LOCK_NB)){ // 加共享锁和附加锁,附加锁防止阻塞 $str = ''; while(!feof($handle)){ $str .= fread($handle, 128); } flock($handle, LOCK_UN); // 释放该锁 fclose($handle); return $str; } else{ echo 'add a share lock failed'; return ''; } } else{ return ''; } }
注意使用多重锁的方式,是LOCK_SH+LOCK_NB,,在排他锁时也可这么用,它们都是枚举变量。对于写操做相似
<?php function writeInFile($filename, $data){ if(true == ($handle = fopen($filename, 'a'))){ if(flock($handle, LOCK_EX)){ //加上排他锁 fwrite($handle, $data); flock($handle, LOCK_UN); //释放锁 fclose($handle); } else{ echo 'add exclusive lock failed'; return; } } }
实际上这也是解决php来实现多进程/线程读取文件的方式,php没有多线程这么一说,但加锁机制能够来模拟这种方式,实现对文件某些操做的队列处理。固然技术老是进步的,如今php有了pthreads扩展后,直接调用一系列的API也能在程序中直接写多线程了。
2.文件上传
文件上传就是将本地的文件上传到服务器上,咱们在用度场的云、鹅场的云上传文件时均是如此,固然实际状况确定比这简单程序复杂。HTTP协议实现了文件的上传机制,首先要在本地选择上传的文件,上传到服务器后,服务端又要作一些处理,为此客户端和服务端均要作一些设置。
一、客户端
文件上传最基本的方法是经过form表单进行POST传递文件,实际上经过PUT方法也能够上传文件,只不过这种方法不安全,须要配置一些安全验证机制,这里只写最经常使用的方式。form表单的input标签能够设置成文件上传按钮type="file",直接解决了如何选择文件的问题,接下来须要设置form的两个属性:enctype和method
enctype:设置成multipart/form-data
method:设置成post
关于enctype属性设置可参考W3School的解释
第一条是默认的值,在咱们使用HTTP协议传递通常的表单数据时,实际上默认对数据进行了分块编码,好比默认的urlencode方式(空格转为+,其余非字母字符转为%加两个十六进制大写数);当enctype设置为第二个时,不会进行字符编码,使用上传控件(input标签type设为file时便是)上传文件,必须设定为这个值;第三个则是值对空格编码为+,但不对非字母字符进行编码。
咱们知道GET方法通常用于获取数据,且传递数据大小有限,并且POST方法能够传递比GET大得多的数据。
form的属性设置完成后,还要传递一个值过去,使用隐藏域(<input type="hidden">),它的name属性设为MAX_FILE_SIZE,之因此要设定这个值,是先大概定一个文件尺寸值,避免在用户传一个大文件传了半天再告诉他:sorry,你的文件太大了-_-它的value属性值就是文件的size,以字节为单位。固然某些书上说,这个值只是做为参考,可轻易进行欺骗,这里只是象征性的表示,很惋惜我这只菜鸟对安全了解甚少,只知道普通注入、XSS等,暂且用着吧。
那么就能够写一个简单得不能再简单的页面了,做为客户端用:
<form method="post" action="upload.php" enctype="multipart/form-data"> <input type="hidden" name="MAX_FILE_SIZE" value="1000000" /> 选择文件: <input type="file" name="uploadFile" value="upload"/><br/> <input type="submit" name="submit" value="上传" /> </form>
二、服务端
文件上传到了服务器上还要通过一些处理过程,就像网购派送快递,到了目的地也还得分个类,确认下目的地对错吧。到了目的地的后续处理须要php脚本,上面在提交表单时的action属性就指定了提交的处理脚本。咱们知道在php中,$_POST保存的是post传递的数据,而上传文件的相关信息保存在$_FILES里边,假设服务端脚本是这样的:
<?php echo '_FILES: <pre>'; print_r($_FILES); echo '_POST: <pre>'; print_r($_POST);
无论服务端如何处理的,先看看这两个数组里面有什么:
看FILES数组的选项就猜获得,这些就是上传文件的名字、类型、尺寸、错误信息等等,还有这个FILES是二维数组。在弄清楚这些选项以前有必要了解几个php配置选项,打开php.ini文件,找到下面四项(其实看注释也明白了):
file_uploads:是否容许经过HTTP传递文件,默认是On容许;
upload_max_filesize:容许传递文件的最大大小,以M为单位,这是服务端配置文件设定的选项;
max_file_uploads:一次请求所容许传递的作多文件个数;
post_max_size:经过POST传递数据的最大大小,由于文件传递也是post方式,也算post传递,须要特别注意的是,它必需要大于upload_max_filesize选项,由于在一次post传递过程当中不只会上传文件,还会传递其余数值,好比上面的POST数组中的数据,必须考虑到,好比upload_max_filesize设为150M,这个就能够设为200M;
upload_tmp_dir:上传文件的临时目录,配置文件里面默认为空,会使用操做系统默认的临时目录,所以上面的FILES数组中的tmp_name中的眼熟的路径就能够解释了,使用windows默认的存放临时文件的目录,并且服务器默认对文件名做了修改。
那么FILES数组中的uploadFile哪里来的,为何要用它作键名,这是由于在上传控件的name属性就是uploadFile,它标记的是这个控件的上传文件信息,所以咱们能够放多个上传控件,设置不一样的name,固然设置同样的name也能够,彻底能够把它们全放在一个数组里边,如<input type="file" name="upload[]">。
如今回过头看FILE数组的键名表明的信息,type是MIME类型,以/分隔,前面是主要类型,后面是具体文件类型,error确定表示错误,有这么几种状况,0:没有错误,上传成功; 1:文件超过了PHP配置指令中的upload_max_filesize规定的大小; 2:文件超过HTML表单中MAX_FILE_SIZE规定的大小,3:文件只有部分上传; 4:没有文件上传。如今关于FILES数组的问题所有明白了。
问题是,是否是上传成功就不作任何处理了,固然不是,总不能全堆在一个临时目录里面,上传多了必然就要将文件移到别的地方,而php提供了专门而安全的函数。is_uploaded_file函数,判断是否经过HTTP POST上传,能够确保恶意的用户去欺骗脚本而管理这些文件,例如/etc/pass(又是linux...),至于具体怎样,我还不清楚。move_uploaded_file函数,将上传文件移动到新位置,同时还可判断文件是否为合法上传,即经过HTTP POST方式,他们运行成功均返回布尔类型true。
扯了半天,上传文件大概要通过这样几个步骤:
一、客户端写好上传控件脚本,并传递一个限制文件大小的隐藏值;
二、服务端首先判断FILES数组error值,看是否出错;
三、判断是否为容许上传的类型(能够不判断);
四、判断在服务端脚本里边是否超过指定的文件大小;
五、上传到临时位置,生成新文件名(防止把已有同名文件覆盖掉),检查并移动到新目录下。
客户端准备工做刚已作,看服务端处理代码:
<?php $typeWhiteList = array('txt', 'doc', 'php', 'zip', 'exe'); // 类型白名单,过滤不容许上传的文件类型 $max_size = 1000000; // 大小限制 为1M $upload_path = 'D:/WAMP/upload/'; // 指定移至的目录 // 一、判断是否成功上传到服务器 $error = $_FILES['uploadFile']['error']; if($error > 0){ switch($error){ case 1: exit('超过php配置的最大文件上传限制'); case 2: exit('超过HTML表单的最大文件上传限制'); case 3: exit('文件只有部分被上传'); case 4: exit('没有上传任何文件'); default: exit('未知类型错误'); } } // 二、判断是否为容许上传的类型 $extension = pathinfo($_FILES['uploadFile']['name'], PATHINFO_EXTENSION); // 获取扩展名 if(!in_array($extension, $typeWhiteList)){ if($extension == '') exit('不容许上传空类型文件'); else exit('不容许上传'.$extension.'类型文件'); } // 三、判断是否为容许大小 if($_FILES['uploadFile']['size'] > $max_size){ exit('超过了容许上传到的'.$max_size.'字节'); } // 四、已到指定位置 $filename = date('Ymd').rand(1000, 9999); // 生成一个新文件名,防止覆盖 if(is_uploaded_file($_FILES['uploadFile']['tmp_name'])){ // 判断是否经过HTTP POST上传 if(!move_uploaded_file($_FILES['uploadFile']['tmp_name'], $upload_path.$filename.'.'.$extension)){ exit('没法移动到指定位置'); } else{ echo '文件上传成功<br/>'; echo '文件名: '.$upload_path.$filename.'.'.$extension.'<br>'; } } else{ exit('文件未经过合法途径上传'); }
本想迅速体验一把,结果报了个Warning,说时间设置依赖系统...bug老是这么不期而遇,设置好时间后,再试,perfect!
3.文件下载
文件下载就比较简单了,简单的文件下载只须要用一个HTML连接就够了,使用<a>标签,href属性指定资源位置,一点就可。但这种方式只能处理浏览器默认没法识别的MIME类型,好比rar、7z等压缩的数据。
<html> <head> <title>donwload file</title> <meta http-equiv="Content-Type" content="text/html"; charset="utf-8" /> </head> <body> <a href="resource/header.txt">header.txt</a><br/> <a href="resource/php.zip">php.zip</a><br/> <a href="resource/pic.ico">pic.ico</a> </body> </html>
对于这些浏览器不认识的类型文件,点连接,它直接弹框让你下载,有的浏览器甚至直接就下了,那么对于文本txt、jpg等浏览器默认识别的类型的文件,一点击则会直接展示在页面上,好比上面header.txt、pic.ico。如何不展现在页面上而去下载它们呢,使用header函数。
header函数会经过发送头信息告知,请把该文件当成一个附件,这样点击的时候,就也会下载了。
<?php $filename = 'header.txt'; header('Content-Type: text/plain'); // 类型为普通文本 header('Content-Disposition:attachment; filename="$filename"'); // Content-Disposition:attachment,告诉它这是附件 header('Content-Length:'.filesize($filename)); // 告知文件大小 readfile($filename); // 读取文件直接输出,便于下载
若是待下载的文件与脚本在同一目录下,貌似不要最后一行也可。但假设脚本在/data0/projects/code/web/php/下边,待下载文件在/data0/projects/patches/apks/下边,就应该制定清晰的路径,好比
$filename = '/data0/projects/patches/apks/com.test.apk';
header('Content-Type: application/octet-stream');
header('Content-Disposition:attachment; filename='.basename($filename));
header('Content-Length:'.filesize($filename));
readfile($filename);
在头信息Content-Disposition里面使用basename取基础文件名,不然下载的文件将是带绝对路径的名字,最后readfile输出文件到缓存进行下载。
涉及到头信息的东西,要补补HTTP协议的知识~