diff --git a/.travis.yml b/.travis.yml index abfb6dc..caf91cb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,12 @@ dist: trusty matrix: include: + - php: '5.5' + env: PHPUNIT_VERSION="4" + - php: '5.6' + env: PHPUNIT_VERSION="5" + - php: '7.0' + env: SWOOLE_VERSION="v4.3.6" PHPUNIT_VERSION="6" - php: '7.1' env: SWOOLE_VERSION="v4.3.6" PHPUNIT_VERSION="7" - php: '7.2' diff --git a/README.md b/README.md index 14ffd37..db67f53 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ API 文档:[https://apidoc.gitee.com/yurunsoft/YurunHttp](https://apidoc.gitee > 每个小版本的更新日志请移步到 Release 查看 +v4.2.0 重构 Swoole 处理器,并发请求性能大幅提升 (PHP 版本依赖降为 >= 5.5) + v4.1.0 实现智能识别场景,自动选择适合 Curl/Swoole 环境的处理器 v4.0.0 新增支持 `Swoole` 并发批量请求 (PHP >= 7.1) @@ -51,7 +53,7 @@ v1.0-1.3 初期版本迭代 ```json { "require": { - "yurunsoft/yurun-http": "^4.1.0" + "yurunsoft/yurun-http": "^4.2.0" } } ``` diff --git a/composer.json b/composer.json index 804d021..fd94a2b 100644 --- a/composer.json +++ b/composer.json @@ -2,9 +2,8 @@ "name": "yurunsoft/yurun-http", "description": "YurunHttp 是开源的 PHP HTTP 类库,支持链式操作,简单易用。支持 Curl、Swoole,支持 Http、Http2、WebSocket!", "require": { - "php": ">=7.1.0", - "psr/http-message": "~1.0", - "yurunsoft/swoole-co-pool": "^1.1.0" + "php": ">=5.5.0", + "psr/http-message": "~1.0" }, "require-dev": { "swoft/swoole-ide-helper": "~2.0", diff --git a/src/YurunHttp.php b/src/YurunHttp.php index 1c800bb..76f02f5 100644 --- a/src/YurunHttp.php +++ b/src/YurunHttp.php @@ -23,7 +23,7 @@ abstract class YurunHttp /** * 版本号 */ - const VERSION = '4.1'; + const VERSION = '4.2'; /** * 设置默认处理器类 diff --git a/src/YurunHttp/Attributes.php b/src/YurunHttp/Attributes.php index eaa5f88..a871744 100644 --- a/src/YurunHttp/Attributes.php +++ b/src/YurunHttp/Attributes.php @@ -168,6 +168,11 @@ abstract class Attributes */ const HTTP2_PIPELINE = 'http2_pipeline'; + /** + * 重试计数 + */ + const PRIVATE_RETRY_COUNT = '__retryCount'; + /** * 重定向计数 */ @@ -178,4 +183,24 @@ abstract class Attributes */ const PRIVATE_WEBSOCKET = '__websocket'; + /** + * Http2 流ID + */ + const PRIVATE_HTTP2_STREAM_ID = '__http2StreamId'; + + /** + * 是否为 Http2 + */ + const PRIVATE_IS_HTTP2 = '__isHttp2'; + + /** + * 是否为 WebSocket + */ + const PRIVATE_IS_WEBSOCKET = '__isWebSocket'; + + /** + * 连接对象 + */ + const PRIVATE_CONNECTION = '__connection'; + } diff --git a/src/YurunHttp/Handler/Curl.php b/src/YurunHttp/Handler/Curl.php index a6a2c84..b20fbe9 100644 --- a/src/YurunHttp/Handler/Curl.php +++ b/src/YurunHttp/Handler/Curl.php @@ -320,6 +320,11 @@ private function getResponse($handler, $curlResult, $isDownload, $receiveHeaders $headerSize = curl_getinfo($handler, CURLINFO_HEADER_SIZE); $headerContent = substr($curlResult, 0, $headerSize); $body = substr($curlResult, $headerSize); + // PHP 7.0.0开始substr()的 string 字符串长度与 start 相同时将返回一个空字符串。在之前的版本中,这种情况将返回 FALSE 。 + if(false === $body) + { + $body = ''; + } // body $result = new Response($body, curl_getinfo($handler, CURLINFO_HTTP_CODE)); diff --git a/src/YurunHttp/Handler/Swoole.php b/src/YurunHttp/Handler/Swoole.php index ebbf592..b6cf184 100644 --- a/src/YurunHttp/Handler/Swoole.php +++ b/src/YurunHttp/Handler/Swoole.php @@ -12,7 +12,6 @@ use Yurun\Util\YurunHttp\Exception\WebSocketException; use Yurun\Util\YurunHttp\Handler\Swoole\HttpConnectionManager; use Yurun\Util\YurunHttp\Handler\Swoole\Http2ConnectionManager; -use function Yurun\Swoole\Coroutine\batch; class Swoole implements IHandler { @@ -180,6 +179,22 @@ public function buildRequest($request, $connection, &$http2Request) * @return bool */ public function send($request) + { + $deferRequest = $this->sendDefer($request); + if($request->getAttribute(Attributes::PRIVATE_IS_HTTP2) && $request->getAttribute(Attributes::HTTP2_NOT_RECV)) + { + return true; + } + return !!$this->recvDefer($deferRequest); + } + + /** + * 发送请求,但延迟接收 + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * @return \Yurun\Util\YurunHttp\Http\Request + */ + public function sendDefer($request) { if([] !== ($queryParams = $request->getQueryParams())) { @@ -196,73 +211,86 @@ public function send($request) $connection = $this->httpConnectionManager->getConnection($uri->getHost(), Uri::getServerPort($uri), 'https' === $uri->getScheme() || 'wss' === $uri->getScheme()); $connection->setDefer(true); } - $redirectCount = $request->getAttribute(Attributes::PRIVATE_REDIRECT_COUNT, 0); - $statusCode = 0; $isWebSocket = $request->getAttribute(Attributes::PRIVATE_WEBSOCKET); - $retry = $request->getAttribute(Attributes::RETRY, 0); - for($i = 0; $i <= $retry; ++$i) + // 构建 + $this->buildRequest($request, $connection, $http2Request); + // 发送 + $path = $uri->getPath(); + if('' === $path) { - // 构建 - $this->buildRequest($request, $connection, $http2Request); - // 发送 - $path = $uri->getPath(); - if('' === $path) - { - $path = '/'; - } - $query = $uri->getQuery(); - if('' !== $query) + $path = '/'; + } + $query = $uri->getQuery(); + if('' !== $query) + { + $path .= '?' . $query; + } + if($isWebSocket) + { + if($isHttp2) { - $path .= '?' . $query; + throw new \RuntimeException('Http2 swoole handler does not support websocket'); } - if($isWebSocket) + if(!$connection->upgrade($path)) { - if($isHttp2) - { - throw new \RuntimeException('Http2 swoole handler does not support websocket'); - } - if(!$connection->upgrade($path)) - { - throw new WebSocketException(sprintf('WebSocket connect faled, error: %s, errorCode: %s', swoole_strerror($connection->errCode), $connection->errCode), $connection->errCode); - } + throw new WebSocketException(sprintf('WebSocket connect faled, error: %s, errorCode: %s', swoole_strerror($connection->errCode), $connection->errCode), $connection->errCode); } - else if(null === ($saveFilePath = $request->getAttribute(Attributes::SAVE_FILE_PATH))) + } + else if(null === ($saveFilePath = $request->getAttribute(Attributes::SAVE_FILE_PATH))) + { + if($isHttp2) { - if($isHttp2) - { - $result = $connection->send($http2Request); - } - else - { - $connection->execute($path); - } + $result = $connection->send($http2Request); + $request = $request->withAttribute(Attributes::PRIVATE_HTTP2_STREAM_ID, $result); } else { - if($isHttp2) - { - throw new \RuntimeException('Http2 swoole handler does not support download file'); - } - $connection->download($path, $saveFilePath); - } - if($isHttp2 && $request->getAttribute(Attributes::HTTP2_NOT_RECV)) - { - return $result; + $connection->execute($path); } - $this->getResponse($request, $connection, $isWebSocket, $isHttp2); - $statusCode = $this->result->getStatusCode(); - // 状态码为5XX或者0才需要重试 - if(!(0 === $statusCode || (5 === (int)($statusCode/100)))) + } + else + { + if($isHttp2) { - break; + throw new \RuntimeException('Http2 swoole handler does not support download file'); } + $connection->download($path, $saveFilePath); + } + + return $request->withAttribute(Attributes::PRIVATE_IS_HTTP2, $isHttp2) + ->withAttribute(Attributes::PRIVATE_IS_WEBSOCKET, $isHttp2) + ->withAttribute(Attributes::PRIVATE_CONNECTION, $connection); + } + + /** + * 延迟接收 + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * @return \Yurun\Util\YurunHttp\Http\Response + */ + public function recvDefer($request) + { + /** @var \Swoole\Coroutine\Http\Client|\Swoole\Coroutine\Http2\Client $connection */ + $connection = $request->getAttribute(Attributes::PRIVATE_CONNECTION); + $retryCount = $request->getAttribute(Attributes::PRIVATE_RETRY_COUNT, 0); + $redirectCount = $request->getAttribute(Attributes::PRIVATE_REDIRECT_COUNT, 0); + $isHttp2 = '2.0' === $request->getProtocolVersion(); + $isWebSocket = $request->getAttribute(Attributes::PRIVATE_WEBSOCKET); + $this->getResponse($request, $connection, $isWebSocket, $isHttp2); + $statusCode = $this->result->getStatusCode(); + // 状态码为5XX或者0才需要重试 + if((0 === $statusCode || (5 === (int)($statusCode/100))) && $retryCount < $request->getAttribute(Attributes::RETRY, 0)) + { + $request = $request->withAttribute(Attributes::RETRY, ++$retryCount); + $deferRequest = $this->sendDefer($request); + return $this->recvDefer($deferRequest); } if(!$isWebSocket && $statusCode >= 300 && $statusCode < 400 && $request->getAttribute(Attributes::FOLLOW_LOCATION, true)) { if(++$redirectCount <= ($maxRedirects = $request->getAttribute(Attributes::MAX_REDIRECTS, 10))) { // 自己实现重定向 - $uri = $this->parseRedirectLocation($this->result->getHeaderLine('location'), $uri); + $uri = $this->parseRedirectLocation($this->result->getHeaderLine('location'), $request->getUri()); if(in_array($statusCode, [301, 302, 303])) { $method = 'GET'; @@ -276,11 +304,11 @@ public function send($request) else { $this->result = $this->result->withErrno(-1) - ->withError(sprintf('Maximum (%s) redirects followed', $maxRedirects)); + ->withError(sprintf('Maximum (%s) redirects followed', $maxRedirects)); return false; } } - return true; + return $this->result; } /** @@ -556,17 +584,20 @@ public function getHttp2ConnectionManager() */ public function coBatch($requests, $timeout = null) { - $callbacks = []; - foreach($requests as $k => $request) + $handlers = []; + $results = []; + foreach($requests as $i => &$request) + { + $results[$i] = null; + $handlers[$i] = $handler = new Swoole; + $request = $handler->sendDefer($request); + } + unset($request); + foreach($requests as $i => $request) { - $callbacks[$k] = function() use($request){ - $swooleHandler = new Swoole; - $swooleHandler->send($request); - $response = $swooleHandler->recv(); - return $response; - }; + $results[$i] = $handlers[$i]->recvDefer($request); } - return batch($callbacks, $timeout ?? -1); + return $results; } } \ No newline at end of file