当前位置: 首页 > 后端技术 > PHP

【PHP源码分析】FastCGI协议分析

时间:2023-03-30 02:45:02 PHP

顺风车运营研发团队陈磊FastCGI是一个协议,它是基于CGI/1.1的,在CGI/1.1中要传输的数据是由FastCGI协议定义的顺序和交付格式。为了更好的理解PHP-FPM的工作,下面对FastCGI协议的内容进行详细说明。1、消息类型FastCGI协议分为10种类型,定义如下:typedefenum_fcgi_request_type{FCGI_BEGIN_REQUEST=1,/*[in]*/FCGI_ABORT_REQUEST=2,/*[in](不支持)*/FCGI_END_REQUEST=3,/*[out]*/FCGI_PARAMS=4,/*[in]环境变量*/FCGI_STDIN=5,/*[in]发布数据*/FCGI_STDOUT=6,/*[out]响应*/FCGI_STDERR=7,/*[out]errors*/FCGI_DATA=8,/*[in]过滤数据(不支持)*/FCGI_GET_VALUES=9,/*[in]*/FCGI_GET_VALUES_RESULT=10/*[out]*/}fcgi_request_type;整个FastCGI是以二进制方式连续传输的,定义了一个统一结构的消息头,用来读取每条消息的消息体,方便消息包的切割。一般先发送FCGI_BEGIN_REQUEST类型消息,然后发送FCGI_PARAMS和FCGI_STDIN类型消息。FastCGI响应处理完成后,发送FCGI_STDOUT和FCGI_STDERR类型消息,最后使用FCGI_END_REQUEST表示请求结束。FCGI_BEGIN_REQUEST和FCGI_END_REQUEST分别表示请求的开始和结束,关系到整个协议。2.消息头对于10种消息,都以消息头开头,其结构定义如下:typedefstruct_fcgi_header{unsignedcharversion;无符号字符类型;无符号字符requestIdB1;无符号字符requestIdB0;无符号字符内容长度B1;无符号字符内容长度B0;无符号字符填充长度;unsignedcharreserved;}fcgi_header;其中,version标识FastCGI协议版本type标识FastCGI记录类型requestId标识消息所属的FastCGI请求。范围为0~2的16次方-1,即0~65535;contentLength标识消息的contentData组件的字节数,计算方式与requestId类似,范围也是0~65535:(contentLengthB1<<8)|contentLengthB0paddingLength标识消息的paddingData组件的字节数,范围从0到255;协议为发送方提供了通过paddingData填充发送记录的功能,方便接收方通过paddingLength快速跳过paddingData。填充的目的是让发送方更有效地处理对齐数据。内容长度超过65535怎么办?答案是它可以在多条消息中发送。3.FCGI_BEGIN_REQUESTFCGI_BEGIN_REQUEST结构体定义如下:typedefstruct_fcgi_begin_request{unsignedcharroleB1;无符号字符角色B0;无符号字符标志;保留无符号字符[5];Yes:(roleB1<<8)+roleB0对于PHP7,处理了三个角色,分别是FCGI_RESPONDER、FCGI_AUTHORIZER和FCGI_FILTER。flags&FCGI_KEEP_CONN:如果为0,则响应本次请求后关闭连接。如果非零,则在响应此请求后不会关闭连接。4.对于名称-值对,类型是FCGI_PARAMS类型。FastCGI协议提供了名值对来满足读写变长名和值的需求。格式如下:nameLength+valueLength+name+value为了节省空间,对于长度为0~127的值,Length使用一个char来表示,首位为0,对于长度大于127的值,Length使用4个字符表示,第一位为1;如图:长度计算代码如下:if(UNEXPECTED(name_len>=128)){if(UNEXPECTED(p+3>=end))return0;name_len=((name_len&0x7f)<<24);name_len|=(*p++<<16);name_len|=(*p++<<8);name_len|=*p++;}这可以表示0~2的31次方的最长长度。5、请求协议FastCGI协议的定义结构如下:typedefstruct_fcgi_begin_request_rec{fcgi_headerhdr;fcgi_begin_request主体;}fcgi_begin_request_rec;分析完FastCGI协议后,我们对请求的FastCGI报文的内容有了一个整体的把握。通过访问相应的接口,我们使用Gdb抓取内容:首先我们修改php-fpm.conf的参数,保证只启动一个worker:pm.max_children=1然后重启php-fpm:./sbin/php-fpm-yetc/php-fpm.conf然后gdb工人:psaux|grepphp-fpmroot300140.00.01423084724?SsNov260:03php-fpm:主进程(etc/php-fpm.conf)chenlei300150.00.01425085500?0:00php-fpm:poolwwwgdb–p30015(gdb)bfcgi_read_request然后通过浏览器访问nginx,nginx转发给php-fpm的worker,根据gdb打印出FastCGI报文的内容:(gdb)bfcgi_read_requestfor第一条消息内容如图:type对应FCGI_BEGIN_REQUEST,requestid为1,length为8,正好是fcgi_begin_request结构体的大小。内容如图:角色对应FCGI_RESPONDER。继续阅读,报文内容如图:类型对应FCGI_PARAMS,requestid为1,长度为:(contentLengthB1<<8)|contentLengthB0==987paddingLength=5,987+5=992,恰好是.根据contentLength+paddingLength向后读取992长度的字节流,我们打印一下:(gdb)p*p@987$1="\017TSCRIPT_FILENAME/home/xiaoju/webroot/beatles/application/mis/mis/src/index.php/admin/operation/index\f\016QUERY_STRINGactivity_id=89\016\003REQUEST_METHODGET\f\000CONTENT_TYPE\016\000CONTENT_LENGTH\vSCRIPT_NAME/index.php/admin/operation/index\v%REQUEST_URI/admin/operation/index?activity_id=89\fDOCUMENT_URI/index.php/admin/operation/index\r4DOCUMENT_ROOT/home/xiaoju/webroot/beatles/application/mis/mis/src\017\bSERVER_PROTOCOLHTTP/1.1\021\aGATEWAY_INTERFACECGI/1.1\017\vSERVER_SOFTWAREnginx/1.2.5\v\rREMOTE_ADDR172.22.32.131\v\005REMOTE_PORT50973\v\fSERVER_ADDR10.94.98.116\v\004SERVER_PORT8085\v\000SERVER_NAME\017\003REDIRECT_STATUS200\t\021HTTP_HOST10.94.98.116:8085\017\nHTTP_CONNECTIONkeep-alive\017xHTTP_USER_AGENTMozilla/5.0(Macintosh;IntelMacOSX10_11_6)AppleWebKit/537.36(KHTML,likeGecko)Chrome/62.0.3202.94Safari/537.36\036\001HTTP_UPGRADE_INSECURE_REQUESTS1\vUHTTP_ACCEPTtext/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\024\rHTTP_ACCEPT_ENCODINGgzip,deflate\024\027HTTP_ACCEPT_LANGUAGEzh-CN,zh;q=0.9,en;q=0.8》根据我们上一节提到的名值对的长度规则,可以看出Fastcgi协议封装了类似于http协议的键值对,看完继续totrack消息可以通过打印得到,得到的消息如图所示,type对应FCGI_PARAMS,requestid为1,length为0,此时FastCGI协议消息的读取过程为完成后说说处理完请求后返回6.响应协议在fcgi_finish_request中调用fcgi_flush,fcgi_flush封装了一个FCGI_END_REQUEST消息体,然后通过safe_write写入socket连接的客户端描述符。intfcgi_flush(fcgi_request*req,intclose){intlen;关闭数据包(请求);len=(int)(req->out_pos-req->out_buf);如果(关闭){fcgi_end_request_rec*rec=(fcgi_end_request_rec*)(req->out_pos);//创建FCGI_END_REQUEST的头fcgi_make_header(&rec->hdr,FCGI_END_REQUEST,req->id,sizeof(fcgi_end_request));//写入appStatusrec->body.appStatusB3=0;rec->body.appStatusB2=0;rec->body.appStatusB1=0;rec->body.appStatusB0=0;//修改协议状态为FCGI_REQUEST_COMPLETE;rec->body.protocolStatus=FCGI_REQUEST_COMPLETE;len+=sizeof(fcgi_end_request_rec);}if(safe_write(req,req->out_buf,len)!=len){req->keep=0;req->out_pos=req->out_buf;返回0;}请求->out_pos=req->out_buf;return1;}至此我们已经完全掌握了FastCGI协议