我们在做WEB应用开发时,经常会遇到文件上传的需求,文件作为一种中间介质将一些信息传递给我们。以PHP为例,如果我们需要实现一个简单的文件上传(假设我们的测试服务器为Apache),首先我们需要有一个前台页面让用户选择文件,这里以一个文件的上传为例:
<form name="upload" action="upload_test.php" method="POST" enctype="multipart/form-data"> <input type="hidden" value="1024" name="MAX_FILE_SIZE" /> 请选择文件:<input name="ufile" type="file" /> <input type="submit" value="Just Upload it" /> </form> |
当我们选择点击提交按钮时,浏览器会将数据提交给服务器。通过Filddle我们可以看到其提交的请求头如下:
POST http://localhost/test/upload_test.php HTTP/1.1 Host: localhost Connection: keep-alive Content-Length: 1347 Cache-Control: max-age=0 Origin: http://localhost User-Agent: //省略若干 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBq7AMhcljN14rJrU // 上面的是关键 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Referer: http://localhost/test/test.html Accept-Encoding: gzip,deflate,sdch Accept-Language: zh-CN,zh;q=0.8 Accept-Charset: GBK,utf-8;q=0.7,*;q=0.3 // 以下为POST提交的内容 ------WebKitFormBoundaryBq7AMhcljN14rJrU Content-Disposition: form-data; name="MAX_FILE_SIZE" 10240 ------WebKitFormBoundaryBq7AMhcljN14rJrU Content-Disposition: form-data; name="ufile"; filename="logo.png" Content-Type: image/png //这里就是我们想要的文件类型 //以下为文件内容 |
如果我们在upload_test.php文件中打印$_FILES,可以看到上传文件类型为image/png。
对应上面的请求头,image/png在文件内容输出的前面的Content-Type字段中。
基本上我们知道了上传的文件类型是浏览器自己识别,直接以文件的Content-Type字段传递给服务器。那么这些内容在PHP中是如何解析的呢?
文件类型获取过程
当客户端发起文件提交请求时,Apache会将所接收到的内容转交给mod_php5模块。
当PHP接收到请求后,首先会调用sapi_activate,在此函数中程序会根据请求的方法处理数据,如我们示例的POST的方法:
if(!strcmp(SG(request_info).request_method, "POST") && (SG(request_info).content_type)) { /* HTTP POST -> may contain form data to be read into variables depending on content type given */ sapi_read_post_data(TSRMLS_C); } |
sapi_read_post_data在main/SAPI.c中实现,它会根据POST内容的Content-Type类型来选择处理POST内容的方法。
if (zend_hash_find(&SG(known_post_content_types), content_type, content_type_length+1, (void **) &post_entry) == SUCCESS) { /* found one, register it for use */ SG(request_info).post_entry = post_entry; post_reader_func = post_entry->post_reader; } |
以上代码的关键在于SG(known_post_content_types)变量在哪里初始化,其基本过程如下:
sapi_startup sapi_globals_ctor(&sapi_globals); php_setup_sapi_content_types(TSRMLS_C); sapi_register_post_entries(php_post_entries TSRMLS_CC); |
这里的的php_post_entries定义在main/php_content_types.c文件。如下:
/* {{{ php_post_entries[] */ static sapi_post_entry php_post_entries[] = { { DEFAULT_POST_CONTENT_TYPE, sizeof(DEFAULT_POST_CONTENT_TYPE)-1, sapi_read_standard_form_data, php_std_post_handler }, { MULTIPART_CONTENT_TYPE, sizeof(MULTIPART_CONTENT_TYPE)-1, NULL, rfc1867_post_handler }, { NULL, 0, NULL, NULL } }; /* }}} */ #define MULTIPART_CONTENT_TYPE "multipart/form-data" #define DEFAULT_POST_CONTENT_TYPE "application/x-www-form-urlencoded" |
嗯,这里的MULTIPART_CONTENT_TYPE(multipart/form-data)所对应的rfc1867_post_handler方法就是我们今天要找的核心函数,其定义在main/rfc1867.c文件:SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler)
后面获取Content-Type的过程就比较简单了:
- 通过multipart_buffer_eof控制循环,遍历所有的multipart部分
- 通过multipart_buffer_headers获取multipart部分的头部信息
- 通过php_mime_get_hdr_value(header, “Content-Type”)获取类型
- 通过register_http_post_files_variable(lbuf, cd, http_post_files, 0 TSRMLS_CC);将数据写到$_FILES变量。
SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) { //若干省略 while (!multipart_buffer_eof(mbuff TSRMLS_CC)){ if (!multipart_buffer_headers(mbuff, &header TSRMLS_CC)) { goto fileupload_done; } //若干省略 /* Possible Content-Type: */ if (cancel_upload || !(cd = php_mime_get_hdr_value(header, "Content-Type"))) { cd = ""; } else { /* fix for Opera 6.01 */ s = strchr(cd, ';'); if (s != NULL) { *s = '\0'; } } //若干省略 /* Add $foo[type] */ if (is_arr_upload) { snprintf(lbuf, llen, "%s[type][%s]", abuf, array_index); } else { snprintf(lbuf, llen, "%s[type]", param); } register_http_post_files_variable(lbuf, cd, http_post_files, 0 TSRMLS_CC); //若干省略 } } |
其它的$_FILES中的size、name等字段,其实现过程与type类似