nginx上,http状态200响应,PHP空白返回的问题
最近的大半年中,编程语言从PHP换到了Golang后,就很少接触PHP,当然,更多的还是恋恋不舍。尽管如此,每当有人在群里聊起PHP的话题时,我总是想插几句,怀念怀念,同时也温故温故知识点,可不能把她给忘了。
昨天朋友tywei问我一个关于PHP奇怪问题,查到原因解决后,没有详细的解释。夜里睡觉时,老是回想这事,早上醒来,决定还是认真记录一下这些问题。也让自己回归正常状态,多写点博客,总结自己,记录自己。
问题描述
PHP+nginx的环境,任何PHP处理的结果,都是空白页面。OS是ubuntu 14.10 ,nginx 1.6.2 ,PHP5.5.12, 问任何PHP的页面,返回的HTTP状态200,但页面内容是空的,什么都没有,不管PHP页面里写的是什么,正常响应。初步怀疑这是拓展的问题,处理请求后,输出内容有冲突,输出为空之类。想查看拓展列表,看看加载了哪些拓展,但任何PHP代码都返回空,不得已,只要CLI跑了下,确定ini是同一个之后,粗略的看了加载的模块
Configuration File (php.ini) Path => /etc/php5/cli Loaded Configuration File => /etc/php5/cli/php.ini Scan this dir for additional .ini files => /etc/php5/cli/conf.d Additional .ini files parsed => /etc/php5/cli/conf.d/05-opcache.ini, /etc/php5/cli/conf.d/10-mysqlnd.ini, /etc/php5/cli/conf.d/10-pdo.ini, /etc/php5/cli/conf.d/20-curl.ini, /etc/php5/cli/conf.d/20-gd.ini, /etc/php5/cli/conf.d/20-imagick.ini, /etc/php5/cli/conf.d/20-json.ini, /etc/php5/cli/conf.d/20-memcache.ini, /etc/php5/cli/conf.d/20-memcached.ini, /etc/php5/cli/conf.d/20-mysql.ini, /etc/php5/cli/conf.d/20-mysqli.ini, /etc/php5/cli/conf.d/20-pdo_mysql.ini, /etc/php5/cli/conf.d/20-readline.ini, /etc/php5/cli/conf.d/20-redis.ini, /etc/php5/cli/conf.d/20-xdebug.ini
大约如上的模块加载,怀疑xdebug跟opcache冲突,尝试关闭后,仍未解决。
strace看系统调用信息
10:02:03.432445 accept(0, {sa_family=AF_INET, sin_port=htons(49617), sin_addr=inet_addr("127.0.0.1")}, [16]) = 4 10:02:06.125142 times({tms_utime=0, tms_stime=0, tms_cutime=0, tms_cstime=0}) = 1718358917 10:02:06.125177 poll([{fd=4, events=POLLIN}], 1, 5000) = 1 ([{fd=4, revents=POLLIN}]) 10:02:06.125332 read(4, "\1\1\0\1\0\10\0\0", 8) = 8 10:02:06.125363 read(4, "\0\1\0\0\0\0\0\0", 8) = 8 10:02:06.125381 read(4, "\1\4\0\1\3)\7\0", 8) = 8 10:02:06.125394 read(4, "\f\0QUERY_STRING\16\3REQUEST_METHODGET\f\0CONTENT_TYPE\16\0CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\v\1REQUEST_URI/\f\nDOCUMENT_URI/index.php\r\22DOCUMENT_ROOT/data/web/test.com\17\10SERVER_PROTOCOLHTTP/1.1\21\7GATEWAY_INTERFACECGI/1.1\17\vSERVER_SOFTWAREnginx/1.6.2\v\10REMOTE_ADDR10.0.2.2\v\5REMOTE_PORT50057\v\tSERVER_ADDR10.0.2.15\v\2SERVER_PORT80\v\10SERVER_NAMEtest.com\17\3REDIRECT_STATUS200\t\10HTTP_HOSTtest.com\17\nHTTP_CONNECTIONkeep-alive\22\tHTTP_CACHE_CONTROLmax-age=0\vJHTTP_ACCEPTtext/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\17yHTTP_USER_AGENTMozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36\24\23HTTP_ACCEPT_ENCODINGgzip, deflate, sdch\24\27HTTP_ACCEPT_LANGUAGEzh-CN,zh;q=0.8,en;q=0.6\v\6HTTP_RA_VER2.10.1\v&HTTP_RA_SIDB4A714D2-20150327-061728-7c932d-97f1f0\0\0\0\0\0\0\0", 816) = 816 10:02:06.125416 read(4, "\1\4\0\1\0\0\0\0", 8) = 8 10:02:06.125439 setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={60, 0}}, NULL) = 0 10:02:06.125468 rt_sigaction(SIGPROF, {0x6ec3d0, [PROF], SA_RESTORER|SA_RESTART, 0x7f3c26740da0}, {0x6ec3d0, [PROF], SA_RESTORER|SA_RESTART, 0x7f3c26740da0}, 8) = 0 10:02:06.125508 rt_sigprocmask(SIG_UNBLOCK, [PROF], NULL, 8) = 0 10:02:06.125570 times({tms_utime=0, tms_stime=0, tms_cutime=0, tms_cstime=0}) = 1718358917 10:02:06.125601 setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={0, 0}}, NULL) = 0 10:02:06.125637 fcntl(3, F_SETLK, {type=F_UNLCK, whence=SEEK_SET, start=0, len=0}) = 0 10:02:06.125668 write(4, "\1\6\0\1\0@\0\0X-Powered-By: PHP/5.5.12-2ubuntu4.4\r\nContent-type: text/html\r\n\r\n\1\3\0\1\0\10\0\0\0\0\0\0\0\0\0\0", 88) = 88 10:02:06.125697 shutdown(4, SHUT_WR) = 0 10:02:06.125713 recvfrom(4, "\1\5\0\1\0\0\0\0", 8, 0, NULL, NULL) = 8 10:02:06.125728 recvfrom(4, "", 8, 0, NULL, NULL) = 0 10:02:06.125797 close(4) = 0
觉得好简短,好奇怪,访问的是SCRIPT_NAME index.php,怎么都没 lstat \open 这个PHP文件呢?直接返回了?起码要判断SCRIPT_FILENAME是否存在吧,要读取SCRIPT_FILENAME,解析里面的代码吧? 等下…..SCRIPT_FILENAME全部地址是啥?发来的CGI协议包中怎么没有SCRIPT_FILENAME?
仔细看下CGI包的内容
QUERY_STRING REQUEST_METHODGET CONTENT_TYPE CONTENT_LENGTH SCRIPT_NAME /index.php REQUEST_URI / DOCUMENT_URI /index.php DOCUMENT_ROOT /data/web/test.com SERVER_PROTOCOL HTTP/1.1 GATEWAY_INTERFACE CGI/1.1 SERVER_SOFTWARE nginx/1.6.2 REMOTE_ADDR 10.0.2.2 REMOTE_PORT 50057 SERVER_ADDR 10.0.2.15 SERVER_PORT 80 SERVER_NAME test.com REDIRECT_STATUS 200 HTTP_HOST test.com HTTP_CONNECTION keep-alive
缺少:
SCRIPT_FILENAME //PATH_TRANSLATED //这个暂时无视
那么问题来了
PHP-FPM接收CGI请求时,如果没有SCRIPT_FILENAME怎么处理的?
发来的CGI协议包中,为啥没有SCRIPT_FILENAME? (SCRIPT_FILENAME是什么,干啥用的,这个就不要问了吧。)
问题1:PHP-FPM接收CGI请求时,如果没有SCRIPT_FILENAME怎么处理的?
fpm源码里(PHP5.5.x 为例)
//fpm_main.c /* {{{ main */ int main(int argc, char *argv[]) { //... //fpm_main.c 1820行 while (fcgi_accept_request(&request) >= 0) { request_body_fd = -1; SG(server_context) = (void *) &request; init_request_info(); //这里对应986行左右的的init_request_info 函数中代码 char *primary_script = NULL; fpm_request_info(); /* request startup only after we've done all we can to * get path_translated */ if (php_request_startup() == FAILURE) { fcgi_finish_request(&request, 1); SG(server_context) = NULL; php_module_shutdown(); return FPM_EXIT_SOFTWARE; } /* check if request_method has been sent. * if not, it's certainly not an HTTP over fcgi request */ if (!SG(request_info).request_method) {//这里判断request.method是否存在,在init_request_info方法里,最上面设置了默认的NULL goto fastcgi_request_done; } //... fastcgi_request_done: //结束当前request请求,给及响应 } }
同样是 fpm_main.c中init_request_info函数的代码如下:
//fpm_main.c 986行 static void init_request_info(void) { char *env_script_filename = sapi_cgibin_getenv("SCRIPT_FILENAME", sizeof("SCRIPT_FILENAME") - 1); char *env_path_translated = sapi_cgibin_getenv("PATH_TRANSLATED", sizeof("PATH_TRANSLATED") - 1); char *script_path_translated = env_script_filename; char *ini; int apache_was_here = 0; /* some broken servers do not have script_filename or argv0 * an example, IIS configured in some ways. then they do more * broken stuff and set path_translated to the cgi script location */ if (!script_path_translated && env_path_translated) { script_path_translated = env_path_translated; } /* initialize the defaults */ SG(request_info).path_translated = NULL; SG(request_info).request_method = NULL; SG(request_info).proto_num = 1000; SG(request_info).query_string = NULL; SG(request_info).request_uri = NULL; SG(request_info).content_type = NULL; SG(request_info).content_length = 0; SG(sapi_headers).http_response_code = 200; // 这里默认给了200的响应 /* script_path_translated being set is a good indication that * we are running in a cgi environment, since it is always * null otherwise. otherwise, the filename * of the script will be retreived later via argc/argv */ if (script_path_translated) { if (CGIG(fix_pathinfo)) { //对pathinfo做处理,剥离出SCRIPT_FILENAME,并重置SCRIPT_FILENAME } else { } if (is_valid_path(script_path_translated)) { //这里如果script_path_translated是合法路径,就给转化一下,赋值给SG(request_info).path_translated SG(request_info).path_translated = estrdup(script_path_translated); } SG(request_info).request_method = sapi_cgibin_getenv("REQUEST_METHOD", sizeof("REQUEST_METHOD") - 1);//这里从CGI包李获取method,赋值给request_method // ... } }
从代码里看出,script_path_translated变量就是cgi协议包中SCRIPT_FILENAME的结果,其中1115行左右,判断如果script_path_translated为空,并且env_path_translated不为空,则用env_path_translated赋值到script_path_translated上。
之后,对request_info的几个属性给予了默认值,包括request_method为null,以及http_response_code默认200的http响应。
在后面的代码中if (script_path_translated) ,因为CGI包中没有SCRIPT_FILENAME,也没有PATH_TRANSLATED,即script_path_translated为空,故没有对request_method进行赋值,其默认值为NULL。
回到main函数中1839行附近if (!SG(request_info).request_method) ,则直接goto到了fastcgi_request_done,直接结束当前request请求,由于之前有设置过默认的http 响应状态为200 ,也就导致了每次返回http状态200成功响应的空白页面的问题。 同时也解释了strace系统调用中,没出现lstat、open 操作SCRIPT_FILENAME的记录了。
问题2,发来的CGI协议包中,为啥没有SCRIPT_FILENAME?
PHP-FPM接收到的CGI协议包都是来自前面nginx的,cgi协议包中没有这个,肯定是nginx没发来,查看nginx配置看到fastcgi_params中没有这项。加上后就可以了。
解决办法:
在nginx配置的 fastcgt_params中加上SCRIPT_FILENAME的配置(在ubuntu的apt-get形式安装nginx配置中,默认是有这条的),比如
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
题外话:
在FPM的fpm_main.c文件的main函数中,1848行对SG(request_info).path_translated的判断,晚于1849行SG(request_info).request_method的判断。而在init_request_info函数中,对SG(request_info).path_translated的赋值却是比SG(request_info).request_method早的。 而且,命名找不到需要执行的脚本,却还返回200的http响应,很奇怪,也不方便排查。我觉得把对SG(request_info).request_method的判断放到SG(request_info).path_translated后面更合适一些。
新的问题:
为什么如果SCRIPT_FILENAME不存在时,用PATH_TRANSLATED来代替它?PATH_TRANSLATED是每个CGI前端都要发送的吗?
这个问题,后来认真看了下,感觉还挺复杂,跟CGI客户顿有关,PHPFPM针对IIS\APACHE\NGINX的处理都不一样。以后再写吧。
如果你在阅读PHP源码,或者阅读PHP SAPI、PHP拓展源码,可以关注一下PHP源码执行流程图关于FPM的执行流程,可以看下下面这幅图:
参考资料:
RFC3875 - The Common Gateway Interface (CGI) Version 1.1
建议继续学习:
- HTTPS, SPDY和 HTTP/2性能的简单对比 (阅读:15911)
- 浅析http协议、cookies和session机制、浏览器缓存 (阅读:15800)
- 从输入 URL 到页面加载完成的过程中都发生了什么事情? (阅读:14502)
- HTTP协议Keep-Alive模式详解 (阅读:10618)
- 各种浏览器审查、监听http头工具介绍 (阅读:6250)
- nginx中对http请求处理的各个阶段分析 (阅读:6070)
- 你不知道的 HTTP (阅读:5369)
- 计算机网络协议赏析-HTTP (阅读:5012)
- libevent源码浅析: http库 (阅读:4824)
- HTTP幂等性概念和应用 (阅读:4365)
扫一扫订阅我的微信号:IT技术博客大学习
- 作者:CFC4N 来源: 莿鸟栖草堂
- 标签: 200 http 空白
- 发布时间:2015-05-11 23:50:44
- [53] IOS安全–浅谈关于IOS加固的几种方法
- [52] 如何拿下简短的域名
- [51] 图书馆的世界纪录
- [50] android 开发入门
- [50] Oracle MTS模式下 进程地址与会话信
- [49] Go Reflect 性能
- [46] 【社会化设计】自我(self)部分――欢迎区
- [46] 读书笔记-壹百度:百度十年千倍的29条法则
- [36] 程序员技术练级攻略
- [29] 视觉调整-设计师 vs. 逻辑