From e02e43217c2bea80043f99ab6d3e722a01dd9343 Mon Sep 17 00:00:00 2001 From: Joop Schilder Date: Sat, 7 Dec 2019 15:03:02 +0100 Subject: [PATCH] Initial commit --- .gitignore | 3 ++ README.md | 47 ++++++++++++++++ bin/app.php | 37 +++++++++++++ composer.json | 19 +++++++ composer.lock | 19 +++++++ src/Http2.php | 117 ++++++++++++++++++++++++++++++++++++++++ src/Request.php | 57 ++++++++++++++++++++ src/Response.php | 60 +++++++++++++++++++++ src/ResponseHandler.php | 16 ++++++ 9 files changed, 375 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 bin/app.php create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 src/Http2.php create mode 100644 src/Request.php create mode 100644 src/Response.php create mode 100644 src/ResponseHandler.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7cf8e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/var/ +/vendor/ +/.idea/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..ba16a8d --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# `curl-http2` + +## What is this? + +A `curl` wrapper for PHP that uses `HTTP/2` to send requests over a single TCP connection. + +Provided are: +- The main class `JoopSchilder\Http2\Http2` +- A simple class for requests supporting `curl` options +- A simple class for responses + +## What is this not? + +A production-ready library. +I mean, you _are_ of course allowed to use it if you think it's useful. + +## Example + +There is some example code in `bin/app.php`. +In it's most basic form, usage of this library might look like this: +```php +use JoopSchilder\Http2\Http2; +use JoopSchilder\Http2\Response; + +$http2 = new Http2(); + +// Might also be an implementation of the ResponseHandler interface +$http2->onResponse(function(Response $response) { + var_dump($response); +}); + +// This creates a request with sensible defaults +$request = $http2->createRequest('https://www.twitter.com/'); +$request->setOptions([CURLOPT_USERAGENT => 'AppleTV6,2/11.1']); +$http2->addRequest($request); + +// Requests are not executed until this method is called +$http2->execute(); +``` +It's a good idea to use a different instance of `Http2` for every host you plan to make a request to. + +## What's next? + +- Add more control to dynamically add requests to an `Http2` instance +- Make an `Http2` instance coupled to a domain (as this is its intended use) +- Use [`PSR-7`](https://www.php-fig.org/psr/psr-7/) HTTP message interfaces +- Add a factory for requests diff --git a/bin/app.php b/bin/app.php new file mode 100755 index 0000000..e1d1f2d --- /dev/null +++ b/bin/app.php @@ -0,0 +1,37 @@ +onResponse(function (Response $response) { + $statusLine = substr($response->getHeader(), 0, strpos($response->getHeader(), "\r")); + $statusLine = str_pad($statusLine, 14, ' ', STR_PAD_RIGHT); + print("$statusLine {$response->getOriginalUrl()}\n"); +}); + +// Add urls +$urls = [ + 'https://twitter.com/survivetheark', + 'https://twitter.com/missingpeople', + 'https://twitter.com/elainecrowley', + 'https://twitter.com/2020comms', + 'https://twitter.com/goal', + 'https://twitter.com/cydarmedical', + 'https://twitter.com/cloakzy', + 'https://twitter.com/cllrandrewkelly', + 'https://twitter.com/youranonnews', + 'https://twitter.com/trickyjabs', +]; +foreach ($urls as $url) { + $http2->addRequest($http2->createRequest($url)); +} + +// Execute the requests +print("Note: it might take some time to establish the TCP connection.\n"); +print("The benefits of HTTP/2 become more clear if you send more requests to the same server.\n\n"); +$startTime = microtime(true); +$http2->execute(); +printf("Requests: %d, time(s): %.3f\n", count($urls), microtime(true) - $startTime); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..9c4cd14 --- /dev/null +++ b/composer.json @@ -0,0 +1,19 @@ +{ + "name": "joopschilder/php-http2", + "authors": [ + { + "name": "Joop Schilder", + "email": "jnmschilder@protonmail.com" + } + ], + "autoload": { + "psr-4": { + "JoopSchilder\\Http2\\": [ + "src" + ] + } + }, + "require": { + "ext-curl": "*" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..508f9b0 --- /dev/null +++ b/composer.lock @@ -0,0 +1,19 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "5b1e133742e667bcab8f73ed237d3adc", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "ext-curl": "*" + }, + "platform-dev": [] +} diff --git a/src/Http2.php b/src/Http2.php new file mode 100644 index 0000000..a7eef94 --- /dev/null +++ b/src/Http2.php @@ -0,0 +1,117 @@ +multihandle = curl_multi_init(); + curl_multi_setopt($this->multihandle, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX); + curl_multi_setopt($this->multihandle, CURLMOPT_MAX_HOST_CONNECTIONS, 1); + curl_multi_setopt($this->multihandle, CURLMOPT_MAX_TOTAL_CONNECTIONS, 1); + } + + + /** + * Http2 destructor. + */ + public function __destruct() + { + if (is_resource($this->multihandle)) { + curl_multi_close($this->multihandle); + } + } + + + /** + * @param string $url + * @param bool $priorKnowledge + * @return Request + */ + public function createRequest(string $url, bool $priorKnowledge = true): Request + { + return (new Request($url))->setOptions([ + CURLOPT_AUTOREFERER => 1, + CURLOPT_FOLLOWLOCATION => 1, + CURLOPT_HEADER => 1, + CURLOPT_HTTP_VERSION => ($priorKnowledge ? CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE : CURL_HTTP_VERSION_2), + CURLOPT_MAXREDIRS => 5, + CURLOPT_PIPEWAIT => 1, + CURLOPT_RETURNTRANSFER => 1, + CURLOPT_SSL_VERIFYPEER => 0, + CURLOPT_TIMEOUT => 10, + ]); + } + + + /** + * @param Request $request + */ + public function addRequest(Request $request): void + { + $handle = curl_init($request->getUri()); + curl_setopt_array($handle, $request->getOptions()); + curl_multi_add_handle($this->multihandle, $handle); + } + + + /** + * @param callable $responseHandler + */ + public function onResponse(callable $responseHandler): void + { + $this->responseHandler = $responseHandler; + } + + + /** + * + */ + public function execute(): void + { + $this->guardNoResponseHandler(); + do { + curl_multi_exec($this->multihandle, $stillRunning); + while (false !== ($message = curl_multi_info_read($this->multihandle))) { + $handle = $message['handle']; + $content = curl_multi_getcontent($handle); + + $header = substr($content, 0, curl_getinfo($handle, CURLINFO_HEADER_SIZE)); + $content = str_replace($header, '', $content); + $url = curl_getinfo($handle, CURLINFO_EFFECTIVE_URL); + call_user_func($this->responseHandler, new Response($url, $header, $content)); + curl_multi_remove_handle($this->multihandle, $handle); + curl_close($handle); + } + usleep(5); + } while ($stillRunning > 0); + } + + + /** + * + */ + private function guardNoResponseHandler(): void + { + if (!is_callable($this->responseHandler)) { + throw new RuntimeException('No valid response handler found'); + } + } + +} diff --git a/src/Request.php b/src/Request.php new file mode 100644 index 0000000..7998b41 --- /dev/null +++ b/src/Request.php @@ -0,0 +1,57 @@ +uri = $uri; + $this->options = []; + } + + + /** + * @return string + */ + public function getUri(): string + { + return $this->uri; + } + + + /** + * @return array + */ + public function getOptions(): array + { + return $this->options; + } + + + /** + * @param array $options + * @return Request + */ + public function setOptions(array $options) + { + $this->options = array_replace($this->options, $options); + + return $this; + } + +} diff --git a/src/Response.php b/src/Response.php new file mode 100644 index 0000000..580ec07 --- /dev/null +++ b/src/Response.php @@ -0,0 +1,60 @@ +originalUrl = trim($originalUrl); + $this->header = trim($header); + $this->content = trim($content); + } + + + /** + * @return string + */ + public function getOriginalUrl(): string + { + return $this->originalUrl; + } + + + /** + * @return string + */ + public function getHeader(): string + { + return $this->header; + } + + + /** + * @return string + */ + public function getContent(): string + { + return $this->content; + } + +} diff --git a/src/ResponseHandler.php b/src/ResponseHandler.php new file mode 100644 index 0000000..3f88a86 --- /dev/null +++ b/src/ResponseHandler.php @@ -0,0 +1,16 @@ +