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

OpensearchPHPSDK协程兼容改造

时间:2023-03-29 21:55:58 PHP

摘要本文简单介绍了协程的概念和基本原理,以及PHP(PECL/Swoole)中协程的一个实现。最后结合OpensearchPHPSDK的协程转换过程,对具体使用方法进行演示。协程与进程和线程一样,协程是一种在逻辑代码行之间进行隔离的方法。只不过进程和线程是操作系统直接支持的,负责调度;协程的粒度比线程小,操作系统无法感知,所以调度工作必须由程序自己完成。从目标来看,协程和epoll等模型基本相同:都是为了减少进程(线程)调度带来的频繁上下文切换的资源消耗,最终提高系统效率。使用epoll模型编写的代码大量使用回调函数(类似于以下伪代码):connect(uri,connected(){send(data,sent(){receive(received(response){//...});});})在实际写的时候一般不会用到这么深的函数嵌套结构,但是上面的例子从侧面说明了写异步代码的困境:效率高,难读。与epoll模型不同,协程代码不需要写很多回调函数,代码逻辑看起来和同步代码一样:connect(uri);send(data);response=receive();//...协程调度器完成了其中的调度工作:感知暂停,调度完成。协程的概念很早就被提出,但最近一些编程语言原生支持协程(如:Go)使其更受欢迎。PHP解释器严重依赖各种C库,代码中使用了大量的同步方法。所以直接在ZendEngine中支持协程是很困难的。幸运的是,一些扩展开发人员已经编写了大量的实现代码来为我们解决这个问题。PECL/SwoolePECL/Swoole是一个用C/C++开发的PHP异步网络通信扩展,提供异步非阻塞网络通信支持。基于PECL/Swoole扩展,我们可以实现PHP非线程安全模式下的多线程网络通信,提高PHP程序的吞吐量。从2.0开始,PECL/Swoole就提供了原生的协程支持。开发人员可以使用一组新编写的类和方法来实现基于单线程协程的网络通信。从4.0开始,PECL/Swoole重写了协程部分的所有代码,放弃了(未发布的3.0版本)基于微信C++协程库的协程实现方案,独立实现了一个相对稳定的协程方案。下面的代码展示了如何通过PECL/Swoole(独立于PECL/Swoole版本)实现一个简单的HTTP客户端请求:go(function(){$cli=new\Swoole\Coroutine\Http\Client('127.0.0.1',9501);$cli->setHeaders(['Host'=>'localhost']);$cli->set(['http_proxy_host'=>HTTP_PROXY_HOST,'http_proxy_port'=>HTTP_PROXY_PORT]);$result=$cli->get('/get?json=true');var_dump($cli->body);});代码中的匿名函数首先通过IP地址和端口号创建HTTP客户端对象,然后分别设置头部信息和代理信息,最后通过GET方法获取URI的响应结果并输出。示例代码中的go()函数是PECL/Swoole协程实现的核心:其中执行的代码全部由协程调度器控制,当协程运行挂起时自动切换到其他协程处理代码段。以下伪代码显示了如何使用go()函数同时发出多个请求:for($i=0;$i<10;++$i){go(function()use($i){$response=request('/region');echo"#{$i}:".$response.PHP_EOL;});}由于协程调度器的存在,代码不会停留在request()函数,所有请求几乎是同时发出。这意味着获取响应的顺序不会严格按照#0,#1,...的顺序:哪个请求先返回,哪个请求的echo语句先执行。当然PECL/Swoole目前只支持其自制和修改的网络通信类,不能支持其他未修改的阻塞函数(或方法)。Retrofitnotes与大多数用PHP编写的HTTP客户端程序一样,OpensearchPHPSDK使用cURL作为默认的HTTP请求工具。通过ext/curl,我们可以实现大部分阻塞式HTTP请求(包括HTTPS请求)。但是对于协程程序来说,这里就是需要重点改造的地方。1.改造原代码在OpenSearch\Client\OpenSearchClient类中,我们找到了前人提取的公共请求方法_curl():privatefunction_curl($url,$items){$method=strtoupper($items['method']);$options=array(CURLOPT_HTTP_VERSION=>'CURL_HTTP_VERSION_1_1',CURLOPT_CONNECTTIMEOUT=>$this->connectTimeout,CURLOPT_TIMEOUT=>$this->timeout,CURLOPT_CUSTOMREQUEST=>$method,CURLOPT_HEADER=>false,CURLOPT_RENStrue,CURLOPT_USERAGENT=>"opensearch/phpsdk".self::SDK_VERSION."/".PHP_VERSION,CURLOPT_HTTPHEADER=>$this->_getHeaders($items),);如果($method==self::METHOD_GET){$query=$this->_buildQuery($items['query_params']);$url.=preg_match('/\?/i',$url)?'&'。$查询:'?.$查询;}else{if(!empty($items['body_json'])){$options[CURLOPT_POSTFIELDS]=$items['body_json'];}}if($this->gzip){$options[CURLOPT_ENCODING]='gzip';}if($this->debug){$out=fopen('php://temp','rw');$options[CURLOPT_VERBOSE]=true;$options[CURLOPT_STDERR]=$out;}$session=curl_init($url);curl_setopt_array($session,$options);$response=curl_exec($session);curl_close($session);$openSearchResult=newOpenSearchResult();$openSearchResult->result=$response;如果($this->debug){$openSearchResult->traceInfo=$this->getDebugInfo($out,$items);}返回$openSearchResult;}上面代码的大致流程是:设置cURL请求参数;请求并获取响应主体;构建并返回OpenSearch\Generated\Common\OpenSearchResult对象;首先,我们需要提供一个User-switched开关,方便协程开发者从cURL模式切换到Swoole模式:/**@varIHttpHandler*/private$httpHandler=null;公共函数__construct($accessKey,$secret,$host,$options=array()){//...$this->httpHandler=newCUrlHttpHandler();//...}公共函数setHttpHandler(IHttpHandler$httpHandler){$this->httpHandler=$httpHandler;}其次,定义IHttpHandler接口:interfaceIHttpHandler{/***执行HTTP请求并返回响应体**@returnstring|false*/publicfunctionrequest($url,$items,$connectTimeout,$timeout,$gzip,$debug);}接口方法request()的参数和返回值与原来的_curl()方法保持一致,只是增加了一些可以通过$this->获取的配置参数,可以考虑把这些$this->参数移到IHttpHandler的抽象实现中。使用这个接口改造原来的_curl()方法:privatefunction_curl($url,$items){$response=$this->httpHandler->request($url,$items,$this->connectTimeout,$this->超时,$this->gzip,$this->调试);//...}由于原来的_curl()方法包含对OpenSearchClient类私有方法的调用,考虑创建一个IHttpHandler的抽象实现来共享这部分方法:abstractclassAbstractHttpHandlerimplementsIHttpHandler{//ExtractfromOpenSearchClientpublicfunction_getHeaders($items){//...}//从OpenSearchClientpublicfunction_buildQuery($params){//...}}中提取出原来的_curl()方法,原代码可以拼接出CUrlHttpHandler:类CUrlHttpHandler扩展AbstractHttpHandler{公共函数请求($url,$items,$connectTimeout,$timeout,$gzip,$debug){$method=strtoupper($items['method']);$options=array(CURLOPT_HTTP_VERSION=>'CURL_HTTP_VERSION_1_1',CURLOPT_CONNECTTIMEOUT=>$connectTimeout,CURLOPT_TIMEOUT=>$timeout,CURLOPT_CUSTOMREQUEST=>$method,CURLOPT_HEADER=>false,CURLOPT_RETURNTRANSFER=>true,CURLOPT_USERAGENT=>"opensearch/phpsdk"。OpenSearchClient::SDK_VERSION。“/”。PHP_VERSION,CURLOPT_HTTPHEADER=>$this->_getHeaders($items),);如果($method==OpenSearchClient::METHOD_GET){$query=$this->_buildQuery($items['query_params']);$url.=preg_match('/\?/i',$url)?'&'。$查询:'?.$查询;}else{if(!empty($items['body_json'])){$options[CURLOPT_POSTFIELDS]=$items['body_json'];}}if($gzip){$options[CURLOPT_ENCODING]='gzip';}if($debug){$out=fopen('php://temp','rw');$options[CURLOPT_VERBOSE]=true;$options[CURLOPT_STDERR]=$out;}$session=curl_init($url);curl_setopt_array($session,$options);$response=curl_exec($session);curl_close($session);返回$响应;使用对局部变量的所有更改,例如:$this->debug被替换为$debug;将原来的self::常量的使用全部改为OpenSearchClient::;最后,就是我们这次的重头戏SwooleHttpHandler2.New方法PECL/Swoole的更新迭代速度非常快,所以其文档远远落后于最新版本。很多时候,我们只能通过分析它的源代码来找到可以使用的属性或方法。首先,创建一个请求类对象:$host=parse_url($url,PHP_URL_HOST);$client=new\Swoole\Coroutine\Http\Client($host);然后,配置cURL对应的各种参数://...//SkipCURLOPT_HTTP_VERSION(Swoole默认使用HTTP/1.1)//SkipCURLOPT_CONNECTTIMEOUT(注意:连接超时暂时不能设置)//CURLOPT_TIMEOUT$client->set(['超时'=>$超时]);//CURLOPT_CUSTOMREQUEST$client->setMethod($method);//SkipCURLOPT_HEADER(Swoole默认将响应头和响应体分开)//SkipCURLOPT_RETURNTRANSFER(Swoole默认返回响应体)//CURLOPT_USERAGENT$headers['User-Agent']="opensearch/phpsdk".OpenSearchClient::SDK_VERSION。“/”。PHP_VERSION;//CURLOPT_ENCODINGif($gzip){$headers['Accept-Encoding']='gzip';}//CURLOPT_HTTPHEADER$client->setHeaders($headers);//NAME=>VALUE接着根据请求类型存储请求体:if($method==OpenSearchClient::METHOD_GET){$query=$this->_buildQuery($items['query_params']);$url.=preg_match('/\?/i',$url)?'&'。$查询:'?.$查询;}else{if(!empty($items['body_json'])){$client->setData($items['body_json']);//请求体}}最后,请求并返回结果:$result=$client->execute($url);//布尔值if(!$result){returnfalse;}返回$client->body;至此,改造完成(){$coClient=OpensearchClientBuilder::build();$coClient->setHttpHandler(newOpenSearch\Client\SwooleHttpHandler());//替换请求处理程序$coClient=newOpensearchClientResponseParser($coClient);$result=$coClient->get('/region');fprintf(STDOUT,"name=%s".PHP_EOL,$result['result']['name']);});后记虽然OpensearchPHPSDK中支持协程并不是用户提出的需求,但作为一家技术型公司,为用户提供更多的技术选择也是我们应该提倡和去做的。本文提到的PHP协程并不是PECL/Swoole的唯一解决方案。PHP开发团队也在考虑构建协程的可能性。但从功能完整性(即使存在上述无法设置“连接超时”等问题)和稳定性来看,PECL/Swoole无疑是目前最好的。参考文档Swoole源码SwooleWiki本文作者:timandes阅读原文本文为云栖社区原创内容,未经允许不得转载。