123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433 |
- <?php
- /**
- * A parallel HTTP client written in pure PHP
- *
- * @author hightman <hightman@twomice.net>
- * @link http://hightman.cn
- * @copyright Copyright (c) 2015 Twomice Studio.
- */
- namespace hightman\http;
- /**
- * Http client
- *
- * @author hightman
- * @since 1.0
- */
- class Client
- {
- use HeaderTrait;
- const PACKAGE = __CLASS__;
- const VERSION = '1.0.0-beta';
- const CRLF = "\r\n";
- private $_cookiePath, $_parser, $_timeout;
- private static $_debugOpen = false;
- private static $_processKey;
- /**
- * Open/close debug mode
- * @param string $msg
- */
- public static function debug($msg)
- {
- if ($msg === 'open' || $msg === 'close') {
- self::$_debugOpen = $msg === 'open';
- } elseif (self::$_debugOpen === true) {
- $key = self::$_processKey === null ? '' : '[' . self::$_processKey . '] ';
- echo '[DEBUG] ', date('H:i:s '), $key, implode('', func_get_args()), self::CRLF;
- }
- }
- /**
- * Decompress data
- * @param string $data compressed string
- * @return string result string
- */
- public static function gzdecode($data)
- {
- return gzinflate(substr($data, 10, -8));
- }
- /**
- * Constructor
- * @param callable $p response parse handler
- */
- public function __construct($p = null)
- {
- $this->applyDefaultHeader();
- $this->setParser($p);
- }
- /**
- * Destructor
- * Export and save all cookies.
- */
- public function __destruct()
- {
- if ($this->_cookiePath !== null) {
- $this->saveCookie($this->_cookiePath);
- }
- }
- /**
- * Set the max network read timeout
- * @param float $sec seconds, decimal support
- */
- public function setTimeout($sec)
- {
- $this->_timeout = floatval($sec);
- }
- /**
- * Set cookie storage path
- * If set, all cookies will be saved into this file, and send to request on need.
- * @param string $file file path to store cookies.
- */
- public function setCookiePath($file)
- {
- $this->_cookiePath = $file;
- $this->loadCookie($file);
- }
- /**
- * Set response parse handler
- * @param callable $p parse handler
- */
- public function setParser($p)
- {
- if ($p === null || $p instanceof ParseInterface || is_callable($p)) {
- $this->_parser = $p;
- }
- }
- /**
- * Run parse handler
- * @param Response $res response object
- * @param Request $req request object
- * @param mixed $key the key string of multi request
- */
- public function runParser($res, $req, $key = null)
- {
- if ($this->_parser !== null) {
- self::debug('run parser: ', $req->getRawUrl());
- if ($this->_parser instanceof ParseInterface) {
- $this->_parser->parse($res, $req, $key);
- } else {
- call_user_func($this->_parser, $res, $req, $key);
- }
- }
- }
- /**
- * Clear headers and apply defaults.
- */
- public function clearHeader()
- {
- parent::clearHeader();
- $this->applyDefaultHeader();
- }
- /**
- * Shortcut of HEAD request
- * @param string $url request URL string.
- * @param array $params query params appended to URL.
- * @return Response result response object.
- */
- public function head($url, $params = [])
- {
- if (is_array($url)) {
- return $this->mhead($url, $params);
- }
- return $this->exec($this->buildRequest('HEAD', $url, $params));
- }
- /**
- * Shortcut of HEAD multiple requests in parallel
- * @param array $urls request URL list.
- * @param array $params query params appended to each URL.
- * @return Response[] result response objects associated with key of URL.
- */
- public function mhead($urls, $params = [])
- {
- return $this->exec($this->buildRequests('HEAD', $urls, $params));
- }
- /**
- * Shortcut of GET request
- * @param string $url request URL string.
- * @param array $params extra query params, appended to URL.
- * @return Response result response object.
- */
- public function get($url, $params = [])
- {
- if (is_array($url)) {
- return $this->mget($url, $params);
- }
- return $this->exec($this->buildRequest('GET', $url, $params));
- }
- /**
- * Shortcut of GET multiple requests in parallel
- * @param array $urls request URL list.
- * @param array $params query params appended to each URL.
- * @return Response[] result response objects associated with key of URL.
- */
- public function mget($urls, $params = [])
- {
- return $this->exec($this->buildRequests('GET', $urls, $params));
- }
- /**
- * Shortcut of DELETE request
- * @param string $url request URL string.
- * @param array $params extra query params, appended to URL.
- * @return Response result response object.
- */
- public function delete($url, $params = [])
- {
- if (is_array($url)) {
- return $this->mdelete($url, $params);
- }
- return $this->exec($this->buildRequest('DELETE', $url, $params));
- }
- /**
- * Shortcut of DELETE multiple requests in parallel
- * @param array $urls request URL list.
- * @param array $params query params appended to each URL.
- * @return Response[] result response objects associated with key of URL.
- */
- public function mdelete($urls, $params = [])
- {
- return $this->exec($this->buildRequests('DELETE', $urls, $params));
- }
- /**
- * Shortcut of POST request
- * @param string|Request $url request URL string, or request object.
- * @param array $params post fields.
- * @return Response result response object.
- */
- public function post($url, $params = [])
- {
- $req = $url instanceof Request ? $url : $this->buildRequest('POST', $url);
- foreach ($params as $key => $value) {
- $req->addPostField($key, $value);
- }
- return $this->exec($req);
- }
- /**
- * Shortcut of PUT request
- * @param string|Request $url request URL string, or request object.
- * @param string $content content to be put.
- * @return Response result response object.
- */
- public function put($url, $content = '')
- {
- $req = $url instanceof Request ? $url : $this->buildRequest('PUT', $url);
- $req->setBody($content);
- return $this->exec($req);
- }
- /**
- * Shortcut of GET restful request as json format
- * @param string $url request URL string.
- * @param array $params extra query params, appended to URL.
- * @return array result json data, or false on failure.
- */
- public function getJson($url, $params = [])
- {
- $req = $this->buildRequest('GET', $url, $params);
- $req->setHeader('accept', 'application/json');
- $res = $this->exec($req);
- return $res === false ? false : $res->getJson();
- }
- /**
- * Shortcut of POST restful request as json format
- * @param string $url request URL string.
- * @param array $params request json data to be post.
- * @return array result json data, or false on failure.
- */
- public function postJson($url, $params = [])
- {
- $req = $this->buildRequest('POST', $url);
- $req->setHeader('accept', 'application/json');
- $req->setJsonBody($params);
- $res = $this->exec($req);
- return $res === false ? false : $res->getJson();
- }
- /**
- * Shortcut of PUT restful request as json format
- * @param string $url request URL string.
- * @param array $params request json data to be put.
- * @return array result json data, or false on failure.
- */
- public function putJson($url, $params = [])
- {
- $req = $this->buildRequest('PUT', $url);
- $req->setHeader('accept', 'application/json');
- $req->setJsonBody($params);
- $res = $this->exec($req);
- return $res === false ? false : $res->getJson();
- }
- /**
- * Execute http requests
- * @param Request|Request[] $req the request object, or array of multiple requests
- * @return Response|Response[] result response object, or response array for multiple requests
- */
- public function exec($req)
- {
- // build recs
- $recs = [];
- if ($req instanceof Request) {
- $recs[] = new Processor($this, $req);
- } elseif (is_array($req)) {
- foreach ($req as $key => $value) {
- if ($value instanceof Request) {
- $recs[$key] = new Processor($this, $value, $key);
- }
- }
- }
- if (count($recs) === 0) {
- return false;
- }
- // loop to process
- while (true) {
- // build select fds
- $rfds = $wfds = $xrec = [];
- $xfds = null;
- foreach ($recs as $rec) {
- /* @var $rec Processor */
- self::$_processKey = $rec->key;
- if ($rec->finished || !($conn = $rec->getConn())) {
- continue;
- }
- if ($this->_timeout !== null) {
- $xrec[] = $rec;
- }
- $rfds[] = $conn->getSock();
- if ($conn->hasDataToWrite()) {
- $wfds[] = $conn->getSock();
- }
- }
- self::$_processKey = null;
- if (count($rfds) === 0 && count($wfds) === 0) {
- // all tasks finished
- break;
- }
- // select sockets
- self::debug('stream_select(rfds[', count($rfds), '], wfds[', count($wfds), ']) ...');
- if ($this->_timeout === null) {
- $num = stream_select($rfds, $wfds, $xfds, null);
- } else {
- $sec = intval($this->_timeout);
- $usec = intval(($this->_timeout - $sec) * 1000000);
- $num = stream_select($rfds, $wfds, $xfds, $sec, $usec);
- }
- self::debug('select result: ', $num === false ? 'false' : $num);
- if ($num === false) {
- trigger_error('stream_select() error', E_USER_WARNING);
- break;
- } elseif ($num > 0) {
- // wfds
- foreach ($wfds as $sock) {
- if (!($conn = Connection::findBySock($sock))) {
- continue;
- }
- $rec = $conn->getExArg();
- /* @var $rec Processor */
- self::$_processKey = $rec->key;
- $rec->send();
- }
- // rfds
- foreach ($rfds as $sock) {
- if (!($conn = Connection::findBySock($sock))) {
- continue;
- }
- $rec = $conn->getExArg();
- /* @var $rec Processor */
- self::$_processKey = $rec->key;
- $rec->recv();
- }
- } else {
- // force to close request
- foreach ($xrec as $rec) {
- self::$_processKey = $rec->key;
- $rec->finish('TIMEOUT');
- }
- }
- }
- // return value
- if (!is_array($req)) {
- $ret = $recs[0]->res;
- } else {
- $ret = [];
- foreach ($recs as $key => $rec) {
- $ret[$key] = $rec->res;
- }
- }
- return $ret;
- }
- /**
- * Build a http request
- * @param string $method
- * @param string $url
- * @param array $params
- * @return Request
- */
- protected function buildRequest($method, $url, $params = [])
- {
- if (count($params) > 0) {
- $url .= strpos($url, '?') === false ? '?' : '&';
- $url .= http_build_query($params);
- }
- return new Request($url, $method);
- }
- /**
- * Build multiple http requests
- * @param string $method
- * @param array $urls
- * @param array $params
- * @return Request[]
- */
- protected function buildRequests($method, $urls, $params = [])
- {
- $reqs = [];
- foreach ($urls as $key => $url) {
- $reqs[$key] = $this->buildRequest($method, $url, $params);
- }
- return $reqs;
- }
- /**
- * @return string default user-agent
- */
- protected function defaultAgent()
- {
- $agent = 'Mozilla/5.0 (Compatible; ' . self::PACKAGE . '/' . self::VERSION . ') ';
- $agent .= 'php-' . php_sapi_name() . '/' . phpversion() . ' ';
- $agent .= php_uname('s') . '/' . php_uname('r');
- return $agent;
- }
- /**
- * Default HTTP headers
- */
- protected function applyDefaultHeader()
- {
- $this->setHeader([
- 'accept' => '*/*',
- 'accept-language' => 'zh-cn,zh',
- 'connection' => 'Keep-Alive',
- 'user-agent' => $this->defaultAgent(),
- ]);
- }
- }
|