Press "Enter" to skip to content

PHP文件上传源码分析(RFC1867)

文件上传,一般分为俩种方式FTP和HTTP, 对于我们的互联网应用来说: FTP上传虽然传输稳定, 但是易用性和安全性都是个问题. 你总不至于在用户要上传头像的时候告诉用户"请打开FTP客户端,上传文件到http://www.laruence.com/uploads/中, 并以2dk433423l.jpg命名"吧?
而基于HTTP的上传,相对来说易用性和安全性上就比FTP要增强了很多. 可以应用的上传方式有PUT, WEBDAV, 和RFC1867三种, 本文将分析在PHP中,是如何基于RFC1867实现文件上传的.

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"\r\n
laruence的个人介绍
--7d51863950254
content-disposition: form-data; name="userfile"; filename="laruence.txt"
Content-Type: text/plain\r\n
... 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来操作刚才生成的文件了~

20 Comments

  1. […] 在图片上传部分,其实能玩的花样很少,但是编写代码所消耗的时间最多。现在我们再假设一种情景,如果我们的图片服务器前端采用Nginx,上传功能用PHP实现,需要写的代码很少,但是性能如何呢,答案是很差。首先PHP接收到Nginx传过来的请求后,会根据http协议(RFC1867)分离出其中的二进制文件,存储在一个临时目录里,等我们在PHP代码里使用$_FILES["upfile"][tmp_name]获取到文件后计算MD5再存储到指定目录,在这个过程中有一次读文件一次写文件是多余的,其实最好的情况是我们拿到http请求中的二进制文件(最好在内存里),直接计算MD5然后存储。 于是我去阅读了PHP的源代码,自己实现了POST文件的解析,让http层直接和存储层连在了一起,提高了上传图片的性能。关于RFC1867的内容和PHP是如何处理的,感兴趣的读者可以去搜索了解下,这里推荐@Laruence的文章《PHP文件上传源码分析(RFC1867) 》。 除了POST请求这个例子,zimg代码中有多处都体现了这种“减少磁盘I/O,尽量在内存中读写”和“避免内存复制”的思想,一点点的积累,最终将会带来优秀的表现。 […]

  2. Bewyn
    Bewyn March 27, 2014

    文件上传大的数据,不能这样做。

  3. […] 在图片上传部分,其实能玩的花样很少,但是编写代码所消耗的时间最多。现在我们再假设一种情景,如果我们的图片服务器前端采用Nginx,上传功能 用PHP实现,需要写的代码很少,但是性能如何呢,答案是很差。首先PHP接收到Nginx传过来的请求后,会根据http协议(RFC1867)分离出 其中的二进制文件,存储在一个临时目录里,等我们在PHP代码里使用$_FILES["upfile"][tmp_name]获取到文件后计算MD5再存 储到指定目录,在这个过程中有一次读文件一次写文件是多余的,其实最好的情况是我们拿到http请求中的二进制文件(最好在内存里),直接计算MD5然后存储。 于是我去阅读了PHP的源代码,自己实现了POST文件的解析,让http层直接和存储层连在了一起,提高了上传图片的性能。关于RFC1867的内容和PHP是如何处理的,感兴趣的读者可以去搜索了解下,这里推荐@Laruence的文章《PHP文件上传源码分析(RFC1867) 》。 除了POST请求这个例子,zimg代码中有多处都体现了这种“减少磁盘I/O,尽量在内存中读写”和“避免内存复制”的思想,一点点的积累,最终将会带来优秀的表现。 […]

  4. zhuzz
    zhuzz October 25, 2012

    最近在做文件上传请教个问题。
    看了你的文件,我这么理解。通过HTTP上传来的文件,到PHP代码的时候已经完全接收完成。这样在处理大文件的时候很有问题。
    有没有别的方式可以使得代码,可以在接收到HEAD的时候就可以执行一些校验。比如改成PUT。

  5. Anonymous
    Anonymous October 25, 2012

    最近在做文件上传请教个问题。
    看了你的文件,我这么理解。通过HTTP上传来的文件,到PHP代码的时候已经完全接收完成。这样在处理大文件的时候很有问题。
    有没有别的方式可以使得代码,可以在接收到HEAD的时候就可以执行一些校验。比如改成PUT。

  6. 雪候鸟
    雪候鸟 January 4, 2010

    @帅的*** 恩, 改天我系统整理介绍下这块, 这块的设计理念还是很有意思的.

  7. 帅得惊动了党
    帅得惊动了党 November 26, 2009

    哥们,你有空分析一下file_get_contents这个函数底层是怎么实现的吧...
    以下是函数跟踪过程:
    PHP_FUNCTION(file_get_contents)
    php_stream_open_wrapper_ex
    _php_stream_open_wrapper_ex
    wrapper = php_stream_locate_url_wrapper
    wrapper->wops->stream_opener
    typedef struct _php_stream_wrapper_ops php_stream_wrapper_ops
    php_plain_files_wrapper_ops
    php_plain_files_stream_opener
    php_stream_fopen_with_path_rel
    _php_stream_fopen_with_path
    php_stream_fopen_rel
    _php_stream_fopen
    到了后来就整不明白了,php里面用了一个叫做的流的东西,一切与文件,网络有关的东西都是用流实现的,流是个很好很强大的东西.不知道你能不能把这一块分析透,期待ing!

  8. 钓雪
    钓雪 November 10, 2009

    感谢楼主的分享,受益匪浅!

  9. 刘永赞
    刘永赞 October 10, 2009

    你好,拜读了你的文章,受益匪浅
    希望能和你做朋友
    我也正在开始研究php源码
    热切盼望您的邮件

  10. phppan
    phppan October 1, 2009

    风雪,在生成临时文件时,文件的名字有没有规律?或者说是随机?

  11. sychen
    sychen September 28, 2009

    好文章,收藏了…

  12. fybird
    fybird September 26, 2009

    第一次看,没看懂。继续看……

  13. 雪候鸟
    雪候鸟 September 26, 2009

    name是input的name.
    我使用的不是插件, 是shjs的css+js

  14. unixhater.com
    unixhater.com September 26, 2009

    我是php菜鸟,PHP默认的上传上限怎么查看与设置?filename是文件名,那name是怎么指定的,比如你传了文件laruence.txt,name是什么?
    另你用的语法高亮插件是哪种?

  15. 雪候鸟
    雪候鸟 September 26, 2009

    最近越来越觉得,我的文字表达能力差了….sigh, 大家将就的看,看不懂的,尽管问…

  16. phpzxh
    phpzxh September 26, 2009

    你的文章都是讲的很深入的,我要把php的源码下载下来看了。

Comments are closed.