所举的例子包体为//开始POST数据内容--------------------------------------------------
---------------7d51863950254
content-disposition: form-data; name="description"
laruence的个人介绍
---------------7d51863950254
content-disposition: form-data; name="userfile"; filename="laruence.txt"
Content-Type: text/plain
... laruence.txt 的内容...
---------------7d51863950254
这个是错误的,RFC1867 规定的分隔符号是每次出现是要加上起始位置 [--] 的,如果整个包体结束则是前后都要加上[--]
我看了多个对 RFC1867 的分析都忽略了这一点,或者是根本没有强调,这个问题花了我两天半时间,所以写在这里。
另外有一点要注意的是,这时候传递给 php 的 content-type 并不是固定的 multipart/form-data 而是整个
content-type: multipart/form-data; +空格+boundary=---------------------------7d52b133509e2
就是说它必须包括动态的分隔符本身,否则 php 是不会正确分隔出变量和文件信息的。
文件上传,一般分为俩种方式FTP和HTTP, 对于我们的互联网应用来说: FTP上传虽然传输稳定, 但是易用性和安全性都是个问题. 你总不至于在用户要上传头像的时候告诉用户”请打开FTP客户端,上传文件到http://www.laruence.com/uploads/中, 并以2dk433423l.jpg命名”吧?
而基于HTTP的上传,相对来说易用性和安全性上就比FTP要增强了很多. 可以应用的上传方式有PUT, WEBDAV, 和RFC1867三种, 本文将分析在PHP中,是如何基于RFC1867实现文件上传的.
RCF1867是Form-based File Upload in HTML标准协议, RFC1867标准对HTML做出了两处修改:
1 为input元素的type属性增加了一个file选项。
2 input标记可以具有accept属性,该属性能够指定可被上传的文件类型或文件格式列表。
另外,本标准还定义了一种新的mime类型:multipart/form-data,以及当处理一个带有enctype=”multipart
/form-data” 并且/或含有<input type=”file”>的标记的表单时所应该采取的行为。
举例来说,当HTML想让用户能够上传一个或更多的文件时,他可以这么写:
<form enctype="multipart/form-data" action="upload.php" method=post>
选择文件:
<input name="userfile" type="file">
文件描述:
<input name="description" type="text">
<input type="submit" value="上传">
</form>
这个表单, 大家一定不陌生, 而对于PHP来说, 它自己另外定义了一个默认表单元素MAX_FILE_SIZE, 用户可以通过这个隐藏的表单元素来建议PHP最多只容许上传文件的大小, 比如对于上面的例子, 我们希望用户上传的文件不能大于5000(5k)字节, 那么可以如下写:
<form enctype="multipart/form-data" action="upload.php" method=post>
<input type="hidden" value="5000" name="MAX_FILE_SIZE"> <!--文件大小-->
选择文件:
<input name="userfile" type="file">
文件描述:
<input name="description" type="text">
<input type="submit" value="上传">
</form>
姑且不说, 这个MAX_FILE_SIZE是多么的不可靠(所以基于浏览器的控制,都是不可靠的), 单纯从实现来讲, 我会慢慢介绍这个MAX_FILE_SIZE是如何起作用的.
当用户选择了一个文件(laruence.txt), 并填写好文件描述(”laruence的个人介绍”), 点击上传后, 发生了什么呢?
在用户确定提交以后, 浏览器会发送如下类似格式的数据包到form中action属性指定的页面(在本例中是upload.php):
//请求头
POST /upload.php HTTP/1.0\r\n
...
Host: www.laruence.com\r\n
...
Content-length: xxxxx\r\n
...
Content-type: multipart/form-data, boundary=--------------7d51863950254\r\n
...\r\n\r\n
//开始POST数据内容
---------------7d51863950254
content-disposition: form-data; name="description"
laruence的个人介绍
---------------7d51863950254
content-disposition: form-data; name="userfile"; filename="laruence.txt"
Content-Type: text/plain
... laruence.txt 的内容...
---------------7d51863950254
接下来, 就是服务器, 是如何处理这些数据了.
当Web服务器, 此处假设为Apache(另外假设PHP是以module方式安装在Apache上的), 接受到用户的数据时, 首先它根据HTTP请求头, 通过确定MIME TYPE为PHP类型, 然后经过一些过程以后(这部分,可以参看我之前的PHP Life Cycle ppt), 最终会把控制权交给PHP模块.
这个时候, PHP会调用sapi_activate来初始化一个请求, 在这个过程中, 首先判断请求类型, 此时是POST, 从而去调用sapi_read_post_data, 通过Content-type, 找到rfc1867的处理函数rfc1867_post_handler, 从而调用这个handler, 来分析POST来的数据.
关于rfc1867_post_handler这部分的源代码, 可以在mian/rfc1867.c找到, 另外也可以参看我之前的深入理解 PHP之文件上传, 其中也列出的源代码.
然后, PHP通过boundary, 对于每一个分段, 都通过检查, 是否同时定义了:
name和filename属性(有名文件上传)
没有定义name定义了filename(无名上传)
定义了name没有定义filename(普通数据),
从而进行不同的处理.
if ((cd = php_mime_get_hdr_value(header, "Content-Disposition"))) {
char *pair=NULL;
int end=0;
while (isspace(*cd)) {
++cd;
}
while (*cd && (pair = php_ap_getword(&cd, ';')))
{
char *key=NULL, *word = pair;
while (isspace(*cd)) {
++cd;
}
if (strchr(pair, '=')) {
key = php_ap_getword(&pair, '=');
if (!strcasecmp(key, "name")) {
//获取name字段
if (param) {
efree(param);
}
param = php_ap_getword_conf(&pair TSRMLS_CC);
} else if (!strcasecmp(key, "filename")) {
//获取filename字段
if (filename) {
efree(filename);
}
filename = php_ap_getword_conf(&pair TSRMLS_CC);
}
}
if (key) {
efree(key);
}
efree(word);
}
在这个过程中, PHP会去检查普通数据中,是否有MAX_FILE_SIZE.
/* Normal form variable, safe to read all data into memory */
if (!filename && param) {
unsigned int value_len;
char *value = multipart_buffer_read_body(mbuff, &value_len TSRMLS_CC);
unsigned int new_val_len; /* Dummy variable */
......
if (!strcasecmp(param, "MAX_FILE_SIZE")) {
max_file_size = atol(value);
}
efree(param);
efree(value);
continue;
}
有的话, 就会按照它的值来检查文件大小是否超出.
if (PG(upload_max_filesize) > 0 && total_bytes > PG(upload_max_filesize)) {
cancel_upload = UPLOAD_ERROR_A;
} else if (max_file_size && (total_bytes > max_file_size)) {
#if DEBUG_FILE_UPLOAD
sapi_module.sapi_error(E_NOTICE,
"MAX_FILE_SIZE of %ld bytes exceeded - file [%s=%s] not saved",
max_file_size, param, filename);
#endif
cancel_upload = UPLOAD_ERROR_B;
}
通过上面的代码,我们也可以看到, 判断分为俩部, 第一部分是检查PHP默认的上传上限. 第二部分才是检查用户自定义的MAX_FILE_SIZE, 所以表单中定义的MAX_FILE_SIZE并不能超过PHP中设置的最大上传文件大小.
通过对name和filename的判断, 如果是文件上传, 会根据php的设置, 在文件上传目录中创建一个随机名字的临时文件:
if (!skip_upload) {
/* Handle file */
fd = php_open_temporary_fd_ex(PG(upload_tmp_dir),
"php", &temp_filename, 1 TSRMLS_CC);
if (fd==-1) {
sapi_module.sapi_error(E_WARNING,
"File upload error - unable to create a temporary file");
cancel_upload = UPLOAD_ERROR_E;
}
}
返回文件句柄, 和临时随机文件名.
之后, 还会有一些验证,比如文件名合法, name合法等.
如果这些验证都通过, 那么就把内容读入, 写入到这个临时文件中.
.....
else if (blen > 0) {
wlen = write(fd, buff, blen); //写入临时文件.
if (wlen == -1) {
/* write failed */
#if DEBUG_FILE_UPLOAD
sapi_module.sapi_error(E_NOTICE, "write() failed - %s", strerror(errno));
#endif
cancel_upload = UPLOAD_ERROR_F;
}
}
....
当循环读入完成后, 关闭临时文件句柄. 记录临时变量名:
zend_hash_add(SG(rfc1867_uploaded_files), temp_filename,
strlen(temp_filename) + 1, &temp_filename, sizeof(char *), NULL);
并且生成FILE变量, 这个时候, 如果是有名上传, 那么就会设置:
$_FILES['userfile'] //name="userfile"
如果是无名上传, 则会使用tmp_name来设置:
$_FILES['tmp_name'] //无名上传
最终交给用户编写的upload.php处理.
这时在upload.php中, 用户就可以通过move_uploaded_file来操作刚才生成的文件了~
RFC1867协议 主要是在HTTP协议的基础上为INPUT标签增加了file属性,同时限定了Form的method必须为POST,ENCTYPE必须为multipart/form-data。当然还增加了一些与此相关属性,但都不是很重要,我们在此不作讨论。
在一般的基于Web的程序中,我们往往使用<input type=”file”>标签,该标签在被浏览器解析后会产生一个文本框和一个浏览按钮,单击浏览按钮会出现系统的文件选择框。
2. 执行上传及<input type=”file”>标 签的一些特性
在上图选择相应的文件,按Upload按钮即可把选择的文件上传到服务器(服务器端 可用JspSmartUpload等组件接受文件)。归根结底上传的所有操作都是由浏览器作的,用户所做的只是简单地选择了一下文件而 已,接下来的问题是,如何能把一个目录中所有的文件实现一次性上传?
(1) 因为目录下的文件数量是不定的,因此我们基本不可能通 过增加多个<input type=”file”>标签的方式来解决问 题。
(2) 如果在Jsp中我们可以考 虑以下方式来解决:通过Jsp动态创建<input type=”file”>标签,并使所创建的标签不可见。把每个标签的Value属性设置为每个文件的路径。 在按Upload时再实行一次性上传。在我们试验了之后就会发现,对<input type=”file”>的Value属性赋值是徒劳的行为,因为RFC1867协议并没有要求浏览器的实现者一定实现Value属性,而IE恰好忽略了Value属性。
即 以下代码将是徒劳的(IE中)
<script language=”javascript”>
//对Value赋值
Form1.file1.value=”c:\\aa.txt”;
//执行后,IE将忽略此赋值
<.script>
上 述两种方式均无法完成我们需要的功能,接下来我们只能剖析IE是如何完成上传功能,把具体的实现方法用ActiveX或(Applet)来 完成。
3. HTTP协议的简单介绍
一般说来我们认为HTTP协议是构建在TCP/IP之上的协议,其实HTTP协议本身无此限制,但因现实中多数情况均是如此,我们就姑且如此认为。HTTP数据总体说 来分三大部分:
(1) 请求行,如下格式
(Request) POST SP URL SP HTTP/1.1 \r\n
请求方法+空格+请求URL+空格+HTTP协议版本+回车换行
如:POST http://localhost:8080/test/test.jsp HTTP1.1\r\n
(Response)HTTP/1.1 SP 200 SP OK \r\n
HTTP协议应答版 本+空格+状态 码+状态描述+回车 换行
如:HTTP/1.1 200 OK \r\n
请求行主 要是描述请求的URL,HTTP协议版本,应答状态等信息。
(2) 请求头
在HttpServletRequest接口里已经封装了对HTTP头操作的方法。如Content-type,Content-length,User-Agent,Host等都是HTTP头。HTTP头主要描述了HTTP所传输数据的一些信息,如主机,数据内容类型,数据长度,代理类型等。
如:
User-Agent: myselfHttp/1.1\r\n
Accept: www/source; text/html; image/gif; */*\r\n
HTTP头+:+空格+头信息+回车 换行
(3) HTTP实体
HTTP实体存放着,HTTP请求的内容,如参数信息,文本框的内容,隐含控件的值,ListBox的值 等。如果在页面上存在:
<input type=”text” name=”userName” value=”zhangsan”>
<input type=”password” name=”password” value=”123”>
HTTP实体会出现以下形式:(POST提交)
userName=zhangsan&password=123
GET提交的时候需要解析HTTP请求行中的URL,在此不多作讨论。
4. RFC1867协 议的数据格式
(1) RFC1867对HTTP头的变更
RFC1867对HTTP头作了适当地变 更,但变更很小。首先content-type头由以前的:
content-type: application/x-www-form-urlencoded
变为
content-type: multipart/form-data; +空格+
boundary=---------------------------7d52b133509e2
即增加了boundary,所谓的boundary其实就是分割线,下文将看到,RFC1867利用boundary分割HTTP实体 数据。boundary中数字 字符区是随机生成的。
(2) 对HTTP实体的变更
因为RFC1867增加了文件上传得功能,
而上传文件内容自然也会被加入到HTTP的实体中。现在因为既有HTTP一般的参数实体,又有上传文件的实体,所以用boundary把每种实体进行了分割,HTTP的实体看
起来将是下面的样子:
-----------------------------7d52b133509e2
Content-Disposition: form-data; name="file1"; filename="c:\aa.txt"
Content-Type: text/plain
文件内容在此处
-----------------------------7d52b133509e2
Content-Disposition: form-data; name="userName"
zhangsan
-----------------------------7d52b133509e2
Content-Disposition: form-data; name="password"
123
-----------------------------7d52b133509e2—
{关于实体的其他说明:
Content-type: multipart/form-data, boundary=AaB03x
(\r\n)
--AaB03x
//boundary
content-disposition: form-data; name="user"
//form 表单变量名
(\r\n)
Wilson
Peng
//form 表单变量数据
--AaB03x
content-disposition: form-data; name="myfile"
//form 表单变量名
Content-type: multipart/mixed, boundary=BbC04y
//新的描述和新的描述和boundary
(\r\n)
--BbC04y
Content-disposition: attachment; filename="myphoto.gif"
//attachment 图片名字
Content-type:
image/gif
//图片描述
Content-Transfer-Encoding:
binary //编码方式
(\r\n)
(...myphoto.gif)
//图片内容略...
--BbC04y--
--AaB03x--
}
很明显,增加了文件上传后,HTTP实体变 得稍微复杂了,首先是通过boundary把实 体分开,以便于读取,然后对FileUpload的格式也作了限制。
(3) RFC1867协议的数据格式
根据RFC1867协议,在HTTP实体中必须对每个上传得文件有 说明头,如:
Content-Disposition: form-data; name="file1";
filename="c:\aa.txt"
Content-Disposition:指明内容类型是form-data
name="file1":指明页面上<input type=”file”>标签的名字是file1
filename="c:\aa.txt":指明上传文件在客户端上的全路径
空行:文件头说明完毕后,要加一空行,以表示后面的数据是文件的内容
文件内容:再接下来就是文件的内容
从这个角度说,完全可以利用HTTP协 议+RFC1867协议开发基于文档管理应用程序。
5. 协议的实现(客户端)
协 议的好处就是,只要你提供的数据符合协议的要求,Server端就可以正确解析你的请求。而不论数据是由IE产生的,或有你自己的Application 产生的。通过上面的分析,我们已经基本清楚了RFC1867协议的 要求,只要我们打开指定的端口,把数据按照协议的要求写进去就会模拟出IE上传的功能。用程序实现是非常Easy的事。附件将给出Java实现版本,程序 只是简单地实现了上传,根据我们前面的分析实现文件上传,参数传递这种稍麻烦的形式也是比较简单的。另外,该程序并没有实现返回数据的解析,同样根据我们 前面的分析,按照HTTP协议去解析返回的数据也不是难事。总之,希望本程序能起到抛砖引玉的作用,