本文簡單的介紹了協程的概念及基本原理,以及協程在PHP中的一種實現方案(PECL/Swoole)。最後,結合Opensearch PHP SDK的協程改造過程演示了具體的使用方法。php
與進程、線程同樣,協程是邏輯代碼線之間隔離的一種方法。只不過進程和線程是由操做系統直接支持,並負責調度的;協程的粒度比線程更小,操做系統沒法感知,所以調度工做必須由程序本身完成。git
從目標上來看,協程與epoll等模型基本一致:都是爲了下降進程(線程)調度引起的頻繁上下文切換的資源消耗,最終提升系統效率。使用epoll模型編寫的代碼大量使用回調函數(相似下面的僞代碼):github
connect(uri, connected() { send(data, sent() { receive(received(response) { // ... }); }); })
在實際編寫中,通常不會使用這麼深層次的函數嵌套結構,可是上例從側面描述了異步代碼的編寫困境:效率高,閱讀難。編程
與epoll模型不一樣,協程代碼不須要編寫不少回調函數,代碼邏輯看起來和同步代碼同樣:json
connect(uri); send(data); response = receive(); // ...
協程調度器完成了其中的調度工做:感知掛起,完成調度。安全
協程的概念提出的很早,只是最近有些編程語言原生支持協程(如:Go)才使得其變得較爲熱門。PHP解釋器對各類C類庫的依賴較爲嚴重,代碼中大量使用同步方法。所以直接在Zend Engine中支持協程困難重重。好在有擴展開發人員編寫了大量的實現代碼,爲咱們解決了這個問題。微信
PECL/Swoole是使用C/C++開發的PHP異步網絡通信擴展,提供異步非阻塞網絡通信支持。基於PECL/Swoole擴展,咱們能夠在PHP非線程安全模式下實現多線程的網絡通信,提升PHP程序的吞吐能力。swoole
自2.0開始,PECL/Swoole提供了原生的協程支持。開發者能夠藉助一整套新編寫的類和方法實現單線程的基於協程的網絡通信。自4.0開始,PECL/Swoole重寫了協程部分所有的代碼,棄用了(未發佈的3.0版本)基於微信C++協程庫的對於協程的實現方案,自主實現了較爲穩定的協程方案。網絡
下面的代碼展現瞭如何經過PECL/Swoole實現簡單的HTTP客戶端請求(與PECL/Swoole版本無關):session
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目前只支持其自制的、通過改造的網絡通信類,其餘還沒有改造的阻塞函數(或方法)沒法被支持。
與大部分的PHP編寫的HTTP客戶端程序同樣,Opensearch PHP SDK使用cURL做爲默認的HTTP請求工具。藉助ext/curl,咱們能夠實現絕大多數的阻塞式的HTTP請求(包括HTTPS請求)。可是對於協程程序來講,這裏就是須要重點改造的地方。
在OpenSearch\Client\OpenSearchClient
類中,咱們找到了前輩們提取出的公用請求方法_curl()
:
private function _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_RETURNTRANSFER => true, CURLOPT_USERAGENT => "opensearch/php sdk " . self::SDK_VERSION . "/" . PHP_VERSION, CURLOPT_HTTPHEADER => $this->_getHeaders($items), ); if ($method == self::METHOD_GET) { $query = $this->_buildQuery($items['query_params']); $url .= preg_match('/\?/i', $url) ? '&' . $query : '?' . $query; } 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 = new OpenSearchResult(); $openSearchResult->result = $response; if ($this->debug) { $openSearchResult->traceInfo = $this->getDebugInfo($out, $items); } return $openSearchResult; }
上述代碼的大體流程是:
OpenSearch\Generated\Common\OpenSearchResult
對象;首先,咱們須要提供一個可供用戶切換的開關,便於協程開發者從cURL模式切換爲Swoole模式:
/** @var IHttpHandler */ private $httpHandler = null; public function __construct($accessKey, $secret, $host, $options = array()) { // ... $this->httpHandler = new CUrlHttpHandler(); // ... } public function setHttpHandler(IHttpHandler $httpHandler) { $this->httpHandler = $httpHandler; }
其次,定義IHttpHandler
接口:
interface IHttpHandler { /** * Performs a HTTP request and returns response body * * @return string|false */ public function request($url, $items, $connectTimeout, $timeout, $gzip, $debug); }
接口方法request()
的參數和返回值保持與原_curl()
方法一致,可是追加了一些原來能夠經過$this->
獲取到的配置參數。
注:若是深刻改造的話,能夠考慮將這些$this->
參數移入IHttpHandler
的抽象實現中。
使用該接口改造原_curl()
方法:
private function _curl($url, $items) { $response = $this->httpHandler->request($url, $items , $this->connectTimeout, $this->timeout, $this->gzip, $this->debug); // ... }
因爲原_curl()
方法中包含對OpenSearchClient
類私有方法的調用,考慮創建IHttpHandler
的抽象實現共享這部分方法:
abstract class AbstractHttpHandler implements IHttpHandler { // Extract from OpenSearchClient public function _getHeaders($items) { // ... } // Extract from OpenSearchClient public function _buildQuery($params) { // ... } }
在改造原_curl()
方法時,原有的代碼就能夠拼接出CUrlHttpHandler
:
class CUrlHttpHandler extends AbstractHttpHandler { public function request($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/php sdk " . OpenSearchClient::SDK_VERSION . "/" . PHP_VERSION, CURLOPT_HTTPHEADER => $this->_getHeaders($items), ); if ($method == OpenSearchClient::METHOD_GET) { $query = $this->_buildQuery($items['query_params']); $url .= preg_match('/\?/i', $url) ? '&' . $query : '?' . $query; } 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); return $response; } }
只是須要有兩點修改:
$this->
對屬性的使用所有變動爲局部變量,如:$this->debug
更換爲$debug
;self::
對常量的使用所有變動爲OpenSearchClient::
;最後,就是咱們本次的重頭戲SwooleHttpHandler
了。
PECL/Swoole的更新迭代速度飛快,所以其文檔遠遠追不上最新的版本。不少時候,咱們只可以靠分析其源代碼探尋可使用屬性或者方法。
首先,創建請求類對象:
$host = parse_url($url, PHP_URL_HOST); $client = new \Swoole\Coroutine\Http\Client($host);
而後,對應cURL配置各類參數:
// ... // 跳過CURLOPT_HTTP_VERSION(Swoole默認使用HTTP/1.1) // 跳過CURLOPT_CONNECTTIMEOUT(注意:暫沒法設置鏈接超時時間) // CURLOPT_TIMEOUT $client->set(['timeout' => $timeout]); // CURLOPT_CUSTOMREQUEST $client->setMethod($method); // 跳過CURLOPT_HEADER(Swoole默認將響應頭、體分離) // 跳過CURLOPT_RETURNTRANSFER(Swoole默認返回響應體) // CURLOPT_USERAGENT $headers['User-Agent'] = "opensearch/php sdk " . OpenSearchClient::SDK_VERSION . "/" . PHP_VERSION; // CURLOPT_ENCODING if ($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) ? '&' . $query : '?' . $query; } else { if(!empty($items['body_json'])){ $client->setData($items['body_json']); // Request body } }
最後,請求並返回結果:
$result = $client->execute($url); // Boolean if (!$result) { return false; } return $client->body;
至此,改造完畢。
注:下面的代碼只是展現了改造後的客戶端類如何使用,並不涉及多請求的並行演示:
go(function() { $coClient = OpensearchClientBuilder::build(); $coClient->setHttpHandler(new OpenSearch\Client\SwooleHttpHandler()); // 更換請求處理器 $coClient = new OpensearchClientResponseParser($coClient); $result = $coClient->get('/region'); fprintf(STDOUT, "name=%s" . PHP_EOL, $result['result']['name']); });
雖然在Opensearch PHP SDK中支持協程並不是用戶提出的需求,可是做爲一家技術型公司,爲用戶提供更多的技術選擇可能性也是咱們應該提倡、作到的。
本文中提到的PHP協程並不是只有PECL/Swoole一種解決方案,PHP開發組也在考慮將協程內置的可能性。然而從功能完整性(即便存在上文中提到沒法設置「鏈接超時時間」等問題)和穩定性上來看,PECL/Swoole無疑是當下最出色的。
本文爲雲棲社區原創內容,未經容許不得轉載。