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

PHP实现并发请求

时间:2023-03-29 15:34:25 PHP

后端服务开发经常需要并发请求。比如你需要获取10个供应商的带宽数据(每个供应商提供不同的url),然后返回一个整合后的数据。你会做什么?在PHP中,最直观的方式就是foreach遍历url,保存每次请求的结果。那么如果供应商提供的接口平均耗时5s,你的接口请求就耗时50s,这对于追求速度来说是非常重要的。而且该站点的性能是不可接受的。这时候就需要并发请求。PHP请求PHP是单进程同步模型,一个请求对应一个进程,I/O是同步阻塞的。PHP通过扩展nginx/apache/php-fpm等服务,提供高并发服务。原理是维护一个进程池,每次请求服务独立启动一个新进程,每个进程独立存在。PHP不支持多线程模式和回调处理,所以PHP内部脚本是同步阻塞的。如果发起5s请求,程序会阻塞I/O5s,直到请求返回结果后才会继续执行代码。所以做爬虫这样的高并发请求需求是非常困难的。那么如何解决并发请求的问题呢?除了内置的file_get_contents和fsockopen请求方法外,PHP还支持cURL扩展来发起请求。支持常规的单次请求:PHPcURL请求详情,也支持并发请求。并发原则是cURL扩展使用多线程来管理多个请求。直接看PHP并发请求的代码demo://简单demo,默认支持GET请求publicfunctionmultiRequest($urls){$mh=curl_multi_init();$urlHandlers=[];$urlData=[];//初始化多个请求处理器为一个foreach($urlsas$value){$ch=curl_init();$url=$value['url'];$url.=strpos($url,'?')?'&':'?';$params=$value['params'];$url.=is_array($params)?http_build_query($params):$params;curl_setopt($ch,CURLOPT_URL,$url);//设置数据按字符串返回而不是直接输出curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);$urlHandlers[]=$ch;curl_multi_add_handle($mh,$ch);}$活动=空;//检查操作的初始状态是否OK,CURLM_CALL_MULTI_PERFORM为常量值-1do{//返回的$active为活跃连接数,$mrc为返回值,正常为0,异常为-1$mrc=curl_multi_exec($mh,$active);}while($mrc==CURLM_CALL_MULTI_PERFORM);//如果还有活跃的请求,运行状态为OK,CURLM_OK为常数0while($active&&$mrc==CURLM_OK){//持续查询状态不利于处理任务,检查每一个50ms,此时释放CPU,降低机器服务器负载usleep(50000);//如果批处理句柄OK,则反复检查运行状态,直到OKselect返回值异常为-1,正常为1(因为只有1个批处理句柄)if(curl_multi_select($mh)!=-1){do{$mrc=curl_multi_exec($mh,$active);}while($mrc==CURLM_CALL_MULTI_PERFORM);}}//获取返回结果foreach($urlHandlersas$index=>$ch){$urlData[$index]=curl_multi_getcontent($ch);//删除单个curl句柄curl_multi_remove_handle($mh,$ch);}curl_multi_close($mh);return$urlData;}在本次并发请求中,先创建一个批处理句柄,然后将url的cURL句柄添加到批处理句柄中,并不断查询批处理句柄的执行状态,获取执行后的返回结果完成了。curl_multi相关函数/**函数作用:返回一个新的cURL批处理句柄@returnresource成功返回cURL批处理句柄,返回false*/resourcecurl_multi_init(void)/**函数作用:添加一个单独的curl_multi_init会话到curl批处理会话curl句柄@param$mhcurl_multi_init返回的批处理句柄@param$chcurl_init返回的curl句柄@returnresourcecURL批处理成功时,失败时为false*/intcurl_multi_add_handle(resource$mh,resource$ch)/**功能:运行当前cURL句柄的子连接@param$mhcurl_multi_init返回的批处理句柄@param$still_running用于判断操作是否仍在执行的标志的引用@return定义的预定义常量incURLcURLcodein*/intcurl_multi_exec(resource$mh,int&$still_running)/**功能:等待所有cURL批处理中的活动连接@param$mh返回的批处理句柄bycurl_multi_init@param$timeout以秒为单位,等待响应的时间@return成功时返回描述符集合中描述符的个数。失败时,select失败时返回-1,否则返回timeout(从底层select系统调用)。*/intcurl_multi_select(resource$mh[,float$timeout=1.0])/**功能:去除cURL批处理句柄resourceinhandleresources描述:从给定的批处理句柄mh中删除ch句柄。删除ch句柄后,用curl_exec()执行句柄仍然是合法的。如果要移除的句柄正在使用中,则该句柄涉及的所有传输任务都将中止。@param$mhcurl_multi_init返回的批句柄@param$chcurl_init返回的cURL句柄@return成功返回0,失败返回CURLM_XXX之一*/intcurl_multi_remove_handle(resource$mh,resource$ch)/**功能:关闭一组cURL句柄@param$mhcurl_multi_init返回的批处理句柄@returnvoid*/voidcurl_multi_close(resource$mh)/**功能:如果设置了CURLOPT_RETURNTRANSFER,返回获取的输出流的文本@param$chcURLhandle由curl_init返回@returnstring如果设置了CURLOPT_RETURNTRANSFER,则返回获取输出的文本流。*/stringcurl_multi_getcontent(resource$ch)本例中使用的预定义常量:CURLM_CALL_MULTI_PERFORM:(int)-1CURLM_OK:(int)0首次请求使用上述curl_multi_init方法的PHP并发请求耗时比较,105个并发请求。第二个请求使用传统的foreach方法,使用curl_init方法遍历105个请求。实际请求耗时结果为:剔除下载约765ms的耗时,简单请求耗时优化达到了39.83/1.58,为25倍。如果继续排除与建立连接相关的耗时,应该会更高。耗时:场景一:界面最慢达到1.58s。场景二:105个接口平均耗时384ms。这次测试的请求是我环境的内部接口,所以耗时很短。实际的爬虫Request环境优化会更加明显。注意ItemConcurrencylimitcurl_multi会消耗大量的系统资源。并发请求有一定的阈值,一般是512。这是由于CURL内部的限制。超过最大并发数会导致失败。在我的测试中,发起了2000个相同的请求,输出了每个请求的响应结果。测试结果显示,2000次请求中有366次成功,前331次全部成功,331-410次依次成功35次,第410次请求后全部失败。所以一定要注意并发数的限制,不要超过300,或者你可以在自己的机器上做个测试来设置你的阈值。使用前请务必注意并发限制!!超时为了防止慢请求影响整个服务,可以设置CURLOPT_TIMEOUT来控制超时,防止一些挂起的请求无限期阻塞进程,最后杀死机器服务。CPU负载满代码示例中,如果继续查询并发执行状态,会导致CPU负载过高,需要添加usleep(50000);代码中的声明。同时,curl_multi_select还可以控制CPU使用率。它将处于等待状态,直到数据响应。一旦有新数据到来就会被唤醒继续执行,减少不必要的CPU消耗。参考资料PHP手册curl_multi_init:http://php.net/manual/zh/func...PHP手册curl预定义常量:http://php.net/manual/zh/curl...PHP中foreachcurl的实现多线程:http://www.111cn.net/phper/ph...正确执行curl_multi_exec:http://www.adrianworlddesign....SegmentfaultPHPcURL请求详情:https://segmentfault.com/a/11...CSDNcurlmulti每次使用多少个并发请求合适:https://blog.csdn.net/loophom...简书curl多线程及原理:https://www.jianshu.com/p/f50...