├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── Exception.php ├── Factory.php └── Socket.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: clue 2 | custom: https://clue.engineering/support 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | PHPUnit: 9 | name: PHPUnit (PHP ${{ matrix.php }} on ${{ matrix.os }}) 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | os: 14 | - ubuntu-24.04 15 | - windows-2022 16 | php: 17 | - 8.4 18 | - 8.3 19 | - 8.2 20 | - 8.1 21 | - 8.0 22 | - 7.4 23 | - 7.3 24 | - 7.2 25 | - 7.1 26 | - 7.0 27 | - 5.6 28 | - 5.5 29 | - 5.4 30 | - 5.3 31 | steps: 32 | - uses: actions/checkout@v4 33 | - uses: shivammathur/setup-php@v2 34 | with: 35 | php-version: ${{ matrix.php }} 36 | extensions: sockets 37 | coverage: xdebug 38 | - run: composer config secure-http false && composer config repo.packagist composer http://packagist.org && composer config preferred-install source 39 | if: ${{ matrix.php < 5.5 && matrix.os == 'windows-2022' }} # legacy PHP on Windows is allowed to use insecure downloads until it will be removed again 40 | - run: composer install 41 | - run: vendor/bin/phpunit --coverage-text 42 | if: ${{ matrix.php >= 7.3 }} 43 | - run: vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy 44 | if: ${{ matrix.php < 7.3 }} 45 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.6.0 (2022-04-14) 4 | 5 | * Feature: Forward compatibility with PHP 8.1 release. 6 | (#67 and #68 by @clue) 7 | 8 | * Fix: Fix reporting refused connections on Windows. 9 | (#69 by @clue) 10 | 11 | * Improve CI setup and documentation. 12 | (#70 and #65 by @clue, #64 by @szepeviktor and #66 by @PaulRotmann) 13 | 14 | ## 1.5.0 (2020-11-27) 15 | 16 | * Feature: Support PHP 8 and drop legacy HHVM support. 17 | (#60 and #61 by @clue) 18 | 19 | * Improve test suite and add `.gitattributes` to exclude dev files from export. 20 | Update to PHPUnit 9 and simplify test matrix. 21 | (#50, #51, #58 and #63 by @clue and #57 by @SimonFrings) 22 | 23 | ## 1.4.1 (2019-10-28) 24 | 25 | * Fix: Fix error reporting when invoking methods on closed socket instance. 26 | (#48 by @clue) 27 | 28 | * Improve test suite to run tests on Windows via Travis CI. 29 | (#49 by @clue) 30 | 31 | ## 1.4.0 (2019-01-22) 32 | 33 | * Feature: Improve Windows support (async connections and Unix domain sockets). 34 | (#43 by @clue) 35 | 36 | * Improve test suite by adding forward compatibility with PHPUnit 7 and PHPUnit 6. 37 | (#42 by @clue) 38 | 39 | ## 1.3.0 (2018-06-10) 40 | 41 | * Feature: Add `$timeout` parameter for `Factory::createClient()` 42 | (#39 by @Elbandi and @clue) 43 | 44 | ```php 45 | // connect to Google, but wait no longer than 2.5s for connection 46 | $socket = $factory->createClient('www.google.com:80', 2.5); 47 | ``` 48 | 49 | * Improve test suite by adding PHPUnit to require-dev, 50 | update test suite to test against legacy PHP 5.3 through PHP 7.2 and 51 | optionally skip functional integration tests requiring internet. 52 | (#26 by @ascii-soup, #28, #29, #37 and #38 by @clue) 53 | 54 | ## 1.2.0 (2015-03-18) 55 | 56 | * Feature: Expose optional `$type` parameter for `Socket::read()` 57 | ([#16](https://github.com/clue/php-socket-raw/pull/16) by @Elbandi) 58 | 59 | ## 1.1.0 (2014-10-24) 60 | 61 | * Feature: Accept float timeouts like `0.5` for `Socket::selectRead()` and `Socket::selectWrite()`. 62 | ([#8](https://github.com/clue/php-socket-raw/issues/8)) 63 | 64 | * Feature: Add new `Socket::connectTimeout()` method. 65 | ([#11](https://github.com/clue/php-socket-raw/pull/11)) 66 | 67 | * Fix: Close invalid socket resource when `Factory` fails to create a `Socket`. 68 | ([#12](https://github.com/clue/php-socket-raw/pull/12)) 69 | 70 | * Fix: Calling `accept()` on an idle server socket emits right error code and message. 71 | ([#14](https://github.com/clue/php-socket-raw/pull/14)) 72 | 73 | ## 1.0.0 (2014-05-10) 74 | 75 | * Feature: Improved errors reporting through dedicated `Exception` 76 | ([#6](https://github.com/clue/socket-raw/pull/6)) 77 | * Feature: Support HHVM 78 | ([#5](https://github.com/clue/socket-raw/pull/5)) 79 | * Use PSR-4 layout 80 | ([#3](https://github.com/clue/socket-raw/pull/3)) 81 | * Continuous integration via Travis CI 82 | 83 | ## 0.1.2 (2013-05-09) 84 | 85 | * Fix: The `Factory::createUdg()` now returns the right socket type. 86 | * Fix: Fix ICMPv6 addressing to not require square brackets because it does not 87 | use ports. 88 | * Extended test suite. 89 | 90 | ## 0.1.1 (2013-04-18) 91 | 92 | * Fix: Raw sockets now correctly report no port instead of a `0` port. 93 | 94 | ## 0.1.0 (2013-04-10) 95 | 96 | * First tagged release 97 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Christian Lück 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is furnished 10 | to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clue/socket-raw 2 | 3 | [![CI status](https://github.com/clue/socket-raw/actions/workflows/ci.yml/badge.svg)](https://github.com/clue/socket-raw/actions) 4 | [![installs on Packagist](https://img.shields.io/packagist/dt/clue/socket-raw?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/clue/socket-raw) 5 | 6 | Simple and lightweight OOP wrapper for PHP's low-level sockets extension (ext-sockets). 7 | 8 | PHP offers two networking APIs, the newer [streams API](https://www.php.net/manual/en/book.stream.php) and the older [socket API](https://www.php.net/manual/en/ref.sockets.php). 9 | While the former has been a huge step forward in generalizing various streaming resources, 10 | it lacks some of the advanced features of the original and much more low-level socket API. 11 | This lightweight library exposes this socket API in a modern way by providing a thin wrapper around the underlying API. 12 | 13 | * **Full socket API** - 14 | It exposes the whole [socket API](https://www.php.net/manual/en/ref.sockets.php) through a *sane* object-oriented interface. 15 | Provides convenience methods for common operations as well as exposing all underlying methods and options. 16 | * **Fluent interface** - 17 | Uses a fluent interface so you can easily chain method calls. 18 | Error conditions will be signalled using `Exception`s instead of relying on cumbersome return codes. 19 | * **Lightweight, SOLID design** - 20 | Provides a thin abstraction that is [*just good enough*](https://en.wikipedia.org/wiki/Principle_of_good_enough) 21 | and does not get in your way. 22 | This library is merely a very thin wrapper and has no other external dependencies. 23 | * **Good test coverage** - 24 | Comes with an automated test suite and is regularly tested in the *real world*. 25 | 26 | **Table of contents** 27 | 28 | * [Support us](#support-us) 29 | * [Quickstart example](#quickstart-example) 30 | * [Usage](#usage) 31 | * [Factory](#factory) 32 | * [createClient()](#createclient) 33 | * [createServer()](#createserver) 34 | * [create*()](#create) 35 | * [Socket](#socket) 36 | * [Methods](#methods) 37 | * [Data I/O](#data-io) 38 | * [Unconnected I/O](#unconnected-io) 39 | * [Non-blocking (async) I/O](#non-blocking-async-io) 40 | * [Connection handling](#connection-handling) 41 | * [Install](#install) 42 | * [Tests](#tests) 43 | * [License](#license) 44 | 45 | ## Support us 46 | 47 | We invest a lot of time developing, maintaining and updating our awesome 48 | open-source projects. You can help us sustain this high-quality of our work by 49 | [becoming a sponsor on GitHub](https://github.com/sponsors/clue). Sponsors get 50 | numerous benefits in return, see our [sponsoring page](https://github.com/sponsors/clue) 51 | for details. 52 | 53 | Let's take these projects to the next level together! 🚀 54 | 55 | ## Quickstart example 56 | 57 | Once [installed](#install), you can use the following example to send and receive HTTP messages: 58 | 59 | ```php 60 | $factory = new \Socket\Raw\Factory(); 61 | 62 | $socket = $factory->createClient('www.google.com:80'); 63 | echo 'Connected to ' . $socket->getPeerName() . PHP_EOL; 64 | 65 | // send simple HTTP request to remote side 66 | $socket->write("GET / HTTP/1.1\r\n\Host: www.google.com\r\n\r\n"); 67 | 68 | // receive and dump HTTP response 69 | var_dump($socket->read(8192)); 70 | 71 | $socket->close(); 72 | ``` 73 | 74 | See also the [examples](examples). 75 | 76 | ## Usage 77 | 78 | ### Factory 79 | 80 | As shown in the [quickstart example](#quickstart-example), this library uses a `Factory` pattern 81 | as a simple API to [`socket_create()`](https://www.php.net/manual/en/function.socket-create.php). 82 | It provides simple access to creating TCP, UDP, UNIX, UDG and ICMP protocol sockets and supports both IPv4 and IPv6 addressing. 83 | 84 | ```php 85 | $factory = new \Socket\Raw\Factory(); 86 | ``` 87 | 88 | #### createClient() 89 | 90 | The `createClient(string $address, null|float $timeout): Socket` method is 91 | the most convenient method for creating connected client sockets 92 | (similar to how [`fsockopen()`](https://www.php.net/manual/en/function.fsockopen.php) or 93 | [`stream_socket_client()`](https://www.php.net/manual/en/function.stream-socket-client.php) work). 94 | 95 | ```php 96 | // establish a TCP/IP stream connection socket to www.google.com on port 80 97 | $socket = $factory->createClient('tcp://www.google.com:80'); 98 | 99 | // same as above, as scheme defaults to TCP 100 | $socket = $factory->createClient('www.google.com:80'); 101 | 102 | // same as above, but wait no longer than 2.5s for connection 103 | $socket = $factory->createClient('www.google.com:80', 2.5); 104 | 105 | // create connectionless UDP/IP datagram socket connected to google's DNS 106 | $socket = $factory->createClient('udp://8.8.8.8:53'); 107 | 108 | // establish TCP/IPv6 stream connection socket to localhost on port 1337 109 | $socket = $factory->createClient('tcp://[::1]:1337'); 110 | 111 | // connect to local Unix stream socket path 112 | $socket = $factory->createClient('unix:///tmp/daemon.sock'); 113 | 114 | // create Unix datagram socket 115 | $socket = $factory->createClient('udg:///tmp/udg.socket'); 116 | 117 | // create a raw low-level ICMP socket (requires root!) 118 | $socket = $factory->createClient('icmp://192.168.0.1'); 119 | ``` 120 | 121 | #### createServer() 122 | 123 | The `createServer($address)` method can be used to create a server side (listening) socket bound to specific address/path 124 | (similar to how [`stream_socket_server()`](https://www.php.net/manual/en/function.stream-socket-server.php) works). 125 | It accepts the same addressing scheme as the [`createClient()`](#createclient) method. 126 | 127 | ```php 128 | // create a TCP/IP stream connection socket server on port 1337 129 | $socket = $factory->createServer('tcp://localhost:1337'); 130 | 131 | // create a UDP/IPv6 datagram socket server on port 1337 132 | $socket = $factory->createServer('udp://[::1]:1337'); 133 | ``` 134 | 135 | #### create*() 136 | 137 | Less commonly used, the `Factory` provides access to creating (unconnected) sockets for various socket types: 138 | 139 | ```php 140 | $socket = $factory->createTcp4(); 141 | $socket = $factory->createTcp6(); 142 | 143 | $socket = $factory->createUdp4(); 144 | $socket = $factory->createUdp6(); 145 | 146 | $socket = $factory->createUnix(); 147 | $socket = $factory->createUdg(); 148 | 149 | $socket = $factory->createIcmp4(); 150 | $socket = $factory->createIcmp6(); 151 | ``` 152 | 153 | You can also create arbitrary socket protocol types through the underlying mechanism: 154 | 155 | ```php 156 | $factory->create($family, $type, $protocol); 157 | ``` 158 | 159 | ### Socket 160 | 161 | As discussed above, the `Socket` class is merely an object-oriented wrapper around a socket resource. As such, it helps if you're familar with socket programming in general. 162 | 163 | The recommended way to create a `Socket` instance is via the above [`Factory`](#factory). 164 | 165 | #### Methods 166 | 167 | All low-level socket operations are available as methods on the `Socket` class. 168 | 169 | You can refer to PHP's fairly good [socket API documentation](https://www.php.net/manual/en/ref.sockets.php) or the docblock comments in the [`Socket` class](src/Socket.php) to get you started. 170 | 171 | ##### Data I/O: 172 | 173 | ``` 174 | $socket->write('data'); 175 | $data = $socket->read(8192); 176 | ``` 177 | 178 | ##### Unconnected I/O: 179 | 180 | ``` 181 | $socket->sendTo('data', $flags, $remote); 182 | $data = $socket->rcvFrom(8192, $flags, $remote); 183 | ``` 184 | 185 | ##### Non-blocking (async) I/O: 186 | 187 | ``` 188 | $socket->setBlocking(false); 189 | $socket->selectRead(); 190 | $socket->selectWrite(); 191 | ``` 192 | 193 | ##### Connection handling: 194 | 195 | ```php 196 | $client = $socket->accept(); 197 | $socket->bind($address); 198 | $socket->connect($address); 199 | $socket->shutdown(); 200 | $socket->close(); 201 | ``` 202 | 203 | ## Install 204 | 205 | The recommended way to install this library is [through Composer](https://getcomposer.org/). 206 | [New to Composer?](https://getcomposer.org/doc/00-intro.md) 207 | 208 | This project follows [SemVer](https://semver.org/). 209 | This will install the latest supported version: 210 | 211 | ```bash 212 | composer require clue/socket-raw:^1.6 213 | ``` 214 | 215 | See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. 216 | 217 | This project aims to run on any platform and thus does not require any PHP 218 | extensions besides `ext-sockets` and supports running on legacy PHP 5.3 through 219 | current PHP 8+. 220 | It's *highly recommended to use the latest supported PHP version* for this project. 221 | 222 | ## Tests 223 | 224 | To run the test suite, you first need to clone this repo and then install all 225 | dependencies [through Composer](https://getcomposer.org/): 226 | 227 | ```bash 228 | composer install 229 | ``` 230 | 231 | To run the test suite, go to the project root and run: 232 | 233 | ```bash 234 | vendor/bin/phpunit 235 | ``` 236 | 237 | Note that the test suite contains tests for ICMP sockets which require root 238 | access on Unix/Linux systems. Therefor some tests will be skipped unless you run 239 | the following command to execute the full test suite: 240 | 241 | ```bash 242 | sudo vendor/bin/phpunit 243 | ``` 244 | 245 | The test suite also contains a number of functional integration tests that rely 246 | on a stable internet connection. 247 | If you do not want to run these, they can simply be skipped like this: 248 | 249 | ```bash 250 | vendor/bin/phpunit --exclude-group internet 251 | ``` 252 | 253 | ## License 254 | 255 | This project is released under the permissive [MIT license](LICENSE). 256 | 257 | > Did you know that I offer custom development services and issuing invoices for 258 | sponsorships of releases and for contributions? Contact me (@clue) for details. 259 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clue/socket-raw", 3 | "description": "Simple and lightweight OOP wrapper for PHP's low-level sockets extension (ext-sockets).", 4 | "keywords": ["socket", "stream", "datagram", "dgram", "client", "server", "ipv6", "tcp", "udp", "icmp", "unix", "udg"], 5 | "homepage": "https://github.com/clue/socket-raw", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Christian Lück", 10 | "email": "christian@clue.engineering" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.3", 15 | "ext-sockets": "*" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Socket\\Raw\\": "src/" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Exception.php: -------------------------------------------------------------------------------- 1 | = 80000) { 22 | try { 23 | $code = socket_last_error($resource); 24 | } catch (\Error $e) { 25 | $code = SOCKET_ENOTSOCK; 26 | } 27 | } elseif (is_resource($resource)) { 28 | $code = socket_last_error($resource); 29 | socket_clear_error($resource); 30 | } else { 31 | // socket already closed, return fixed error code instead of operating on invalid handle 32 | $code = SOCKET_ENOTSOCK; 33 | } 34 | 35 | return self::createFromCode($code, $messagePrefix); 36 | } 37 | 38 | /** 39 | * Create an Exception after a global socket operation failed (like socket creation) 40 | * 41 | * @param string $messagePrefix 42 | * @return self 43 | * @uses socket_last_error() to get last global error code 44 | * @uses socket_clear_error() to clear global error code 45 | * @uses self::createFromCode() to automatically construct exception with full error message 46 | */ 47 | public static function createFromGlobalSocketOperation($messagePrefix = 'Socket operation failed') 48 | { 49 | $code = socket_last_error(); 50 | socket_clear_error(); 51 | 52 | return self::createFromCode($code, $messagePrefix); 53 | } 54 | 55 | /** 56 | * Create an Exception for given error $code 57 | * 58 | * @param int $code 59 | * @param string $messagePrefix 60 | * @return self 61 | * @throws Exception if given $val is boolean false 62 | * @uses self::getErrorMessage() to translate error code to error message 63 | */ 64 | public static function createFromCode($code, $messagePrefix = 'Socket error') 65 | { 66 | return new self($messagePrefix . ': ' . self::getErrorMessage($code), $code); 67 | } 68 | 69 | /** 70 | * get error message for given error code 71 | * 72 | * @param int $code error code 73 | * @return string 74 | * @uses socket_strerror() to translate error code to error message 75 | * @uses get_defined_constants() to check for related error constant 76 | */ 77 | protected static function getErrorMessage($code) 78 | { 79 | $string = socket_strerror($code); 80 | 81 | // search constant starting with SOCKET_ for this error code 82 | foreach (get_defined_constants() as $key => $value) { 83 | if($value === $code && strpos($key, 'SOCKET_') === 0) { 84 | $string .= ' (' . $key . ')'; 85 | break; 86 | } 87 | } 88 | 89 | return $string; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Factory.php: -------------------------------------------------------------------------------- 1 | createFromString($address, $scheme); 24 | 25 | try { 26 | if ($timeout === null) { 27 | $socket->connect($address); 28 | } else { 29 | // connectTimeout enables non-blocking mode, so turn blocking on again 30 | $socket->connectTimeout($address, $timeout); 31 | $socket->setBlocking(true); 32 | } 33 | } catch (Exception $e) { 34 | $socket->close(); 35 | throw $e; 36 | } 37 | 38 | return $socket; 39 | } 40 | 41 | /** 42 | * create server socket bound to given address (and start listening for streaming clients to connect to this stream socket) 43 | * 44 | * @param string $address address to bind socket to 45 | * @return \Socket\Raw\Socket 46 | * @throws Exception on error 47 | * @uses self::createFromString() 48 | * @uses Socket::bind() 49 | * @uses Socket::listen() only for stream sockets (TCP/UNIX) 50 | */ 51 | public function createServer($address) 52 | { 53 | $socket = $this->createFromString($address, $scheme); 54 | 55 | try { 56 | $socket->bind($address); 57 | 58 | if ($socket->getType() === SOCK_STREAM) { 59 | $socket->listen(); 60 | } 61 | } catch (Exception $e) { 62 | $socket->close(); 63 | throw $e; 64 | } 65 | 66 | return $socket; 67 | } 68 | 69 | /** 70 | * create TCP/IPv4 stream socket 71 | * 72 | * @return \Socket\Raw\Socket 73 | * @throws Exception on error 74 | * @uses self::create() 75 | */ 76 | public function createTcp4() 77 | { 78 | return $this->create(AF_INET, SOCK_STREAM, SOL_TCP); 79 | } 80 | 81 | /** 82 | * create TCP/IPv6 stream socket 83 | * 84 | * @return \Socket\Raw\Socket 85 | * @throws Exception on error 86 | * @uses self::create() 87 | */ 88 | public function createTcp6() 89 | { 90 | return $this->create(AF_INET6, SOCK_STREAM, SOL_TCP); 91 | } 92 | 93 | /** 94 | * create UDP/IPv4 datagram socket 95 | * 96 | * @return \Socket\Raw\Socket 97 | * @throws Exception on error 98 | * @uses self::create() 99 | */ 100 | public function createUdp4() 101 | { 102 | return $this->create(AF_INET, SOCK_DGRAM, SOL_UDP); 103 | } 104 | 105 | /** 106 | * create UDP/IPv6 datagram socket 107 | * 108 | * @return \Socket\Raw\Socket 109 | * @throws Exception on error 110 | * @uses self::create() 111 | */ 112 | public function createUdp6() 113 | { 114 | return $this->create(AF_INET6, SOCK_DGRAM, SOL_UDP); 115 | } 116 | 117 | /** 118 | * create local UNIX stream socket 119 | * 120 | * @return \Socket\Raw\Socket 121 | * @throws Exception on error 122 | * @uses self::create() 123 | */ 124 | public function createUnix() 125 | { 126 | return $this->create(AF_UNIX, SOCK_STREAM, 0); 127 | } 128 | 129 | /** 130 | * create local UNIX datagram socket (UDG) 131 | * 132 | * @return \Socket\Raw\Socket 133 | * @throws Exception on error 134 | * @uses self::create() 135 | */ 136 | public function createUdg() 137 | { 138 | return $this->create(AF_UNIX, SOCK_DGRAM, 0); 139 | } 140 | 141 | /** 142 | * create raw ICMP/IPv4 datagram socket (requires root!) 143 | * 144 | * @return \Socket\Raw\Socket 145 | * @throws Exception on error 146 | * @uses self::create() 147 | */ 148 | public function createIcmp4() 149 | { 150 | return $this->create(AF_INET, SOCK_RAW, getprotobyname('icmp')); 151 | } 152 | 153 | /** 154 | * create raw ICMPv6 (IPv6) datagram socket (requires root!) 155 | * 156 | * @return \Socket\Raw\Socket 157 | * @throws Exception on error 158 | * @uses self::create() 159 | */ 160 | public function createIcmp6() 161 | { 162 | return $this->create(AF_INET6, SOCK_RAW, 58 /*getprotobyname('icmp')*/); 163 | } 164 | 165 | /** 166 | * create low level socket with given arguments 167 | * 168 | * @param int $domain 169 | * @param int $type 170 | * @param int $protocol 171 | * @return \Socket\Raw\Socket 172 | * @throws Exception if creating socket fails 173 | * @throws \Error PHP 8 only: throws \Error when arguments are invalid 174 | * @uses socket_create() 175 | */ 176 | public function create($domain, $type, $protocol) 177 | { 178 | $sock = @socket_create($domain, $type, $protocol); 179 | if ($sock === false) { 180 | throw Exception::createFromGlobalSocketOperation('Unable to create socket'); 181 | } 182 | return new Socket($sock); 183 | } 184 | 185 | /** 186 | * create a pair of indistinguishable sockets (commonly used in IPC) 187 | * 188 | * @param int $domain 189 | * @param int $type 190 | * @param int $protocol 191 | * @return \Socket\Raw\Socket[] 192 | * @throws Exception if creating pair of sockets fails 193 | * @throws \Error PHP 8 only: throws \Error when arguments are invalid 194 | * @uses socket_create_pair() 195 | */ 196 | public function createPair($domain, $type, $protocol) 197 | { 198 | $ret = @socket_create_pair($domain, $type, $protocol, $pair); 199 | if ($ret === false) { 200 | throw Exception::createFromGlobalSocketOperation('Unable to create pair of sockets'); 201 | } 202 | return array(new Socket($pair[0]), new Socket($pair[1])); 203 | } 204 | 205 | /** 206 | * create TCP/IPv4 stream socket and listen for new connections 207 | * 208 | * @param int $port 209 | * @param int $backlog 210 | * @return \Socket\Raw\Socket 211 | * @throws Exception if creating listening socket fails 212 | * @throws \Error PHP 8 only: throws \Error when arguments are invalid 213 | * @uses socket_create_listen() 214 | * @see self::createServer() as an alternative to bind to specific IP, IPv6, UDP, UNIX, UGP 215 | */ 216 | public function createListen($port, $backlog = 128) 217 | { 218 | $sock = @socket_create_listen($port, $backlog); 219 | if ($sock === false) { 220 | throw Exception::createFromGlobalSocketOperation('Unable to create listening socket'); 221 | } 222 | return new Socket($sock); 223 | } 224 | 225 | /** 226 | * create socket for given address 227 | * 228 | * @param string $address (passed by reference in order to remove scheme, if present) 229 | * @param string|null $scheme default scheme to use, defaults to TCP (passed by reference in order to update with actual scheme used) 230 | * @return \Socket\Raw\Socket 231 | * @throws InvalidArgumentException if given address is invalid 232 | * @throws Exception in case creating socket failed 233 | * @uses self::createTcp4() etc. 234 | */ 235 | public function createFromString(&$address, &$scheme) 236 | { 237 | if ($scheme === null) { 238 | $scheme = 'tcp'; 239 | } 240 | 241 | $hasScheme = false; 242 | 243 | $pos = strpos($address, '://'); 244 | if ($pos !== false) { 245 | $scheme = substr($address, 0, $pos); 246 | $address = substr($address, $pos + 3); 247 | $hasScheme = true; 248 | } 249 | 250 | if (strpos($address, ':') !== strrpos($address, ':') && in_array($scheme, array('tcp', 'udp', 'icmp'))) { 251 | // TCP/UDP/ICMP address with several colons => must be IPv6 252 | $scheme .= '6'; 253 | } 254 | 255 | if ($scheme === 'tcp') { 256 | $socket = $this->createTcp4(); 257 | } elseif ($scheme === 'udp') { 258 | $socket = $this->createUdp4(); 259 | } elseif ($scheme === 'tcp6') { 260 | $socket = $this->createTcp6(); 261 | } elseif ($scheme === 'udp6') { 262 | $socket = $this->createUdp6(); 263 | } elseif ($scheme === 'unix') { 264 | $socket = $this->createUnix(); 265 | } elseif ($scheme === 'udg') { 266 | $socket = $this->createUdg(); 267 | } elseif ($scheme === 'icmp') { 268 | $socket = $this->createIcmp4(); 269 | } elseif ($scheme === 'icmp6') { 270 | $socket = $this->createIcmp6(); 271 | if ($hasScheme) { 272 | // scheme was stripped from address, resulting IPv6 must not 273 | // have a port (due to ICMP) and thus must not be enclosed in 274 | // square brackets 275 | $address = trim($address, '[]'); 276 | } 277 | } else { 278 | throw new InvalidArgumentException('Invalid address scheme given'); 279 | } 280 | return $socket; 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/Socket.php: -------------------------------------------------------------------------------- 1 | resource = $resource; 31 | } 32 | 33 | /** 34 | * get actual socket resource 35 | * 36 | * @return \Socket|resource returns the socket resource (a `Socket` object as of PHP 8) 37 | */ 38 | public function getResource() 39 | { 40 | return $this->resource; 41 | } 42 | 43 | /** 44 | * accept an incomming connection on this listening socket 45 | * 46 | * @return \Socket\Raw\Socket new connected socket used for communication 47 | * @throws Exception on error, if this is not a listening socket or there's no connection pending 48 | * @throws \Error PHP 8 only: throws \Error when socket is invalid 49 | * @see self::selectRead() to check if this listening socket can accept() 50 | * @see Factory::createServer() to create a listening socket 51 | * @see self::listen() has to be called first 52 | * @uses socket_accept() 53 | */ 54 | public function accept() 55 | { 56 | $resource = @socket_accept($this->resource); 57 | if ($resource === false) { 58 | throw Exception::createFromGlobalSocketOperation(); 59 | } 60 | return new Socket($resource); 61 | } 62 | 63 | /** 64 | * binds a name/address/path to this socket 65 | * 66 | * has to be called before issuing connect() or listen() 67 | * 68 | * @param string $address either of IPv4:port, hostname:port, [IPv6]:port, unix-path 69 | * @return self $this (chainable) 70 | * @throws Exception on error 71 | * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid 72 | * @uses socket_bind() 73 | */ 74 | public function bind($address) 75 | { 76 | $ret = @socket_bind($this->resource, $this->unformatAddress($address, $port), $port); 77 | if ($ret === false) { 78 | throw Exception::createFromSocketResource($this->resource); 79 | } 80 | return $this; 81 | } 82 | 83 | /** 84 | * close this socket 85 | * 86 | * ATTENTION: make sure to NOT re-use this socket instance after closing it! 87 | * its socket resource remains closed and most further operations will fail! 88 | * 89 | * @return self $this (chainable) 90 | * @throws \Error PHP 8 only: throws \Error when socket is invalid 91 | * @see self::shutdown() should be called before closing socket 92 | * @uses socket_close() 93 | */ 94 | public function close() 95 | { 96 | socket_close($this->resource); 97 | return $this; 98 | } 99 | 100 | /** 101 | * initiate a connection to given address 102 | * 103 | * @param string $address either of IPv4:port, hostname:port, [IPv6]:port, unix-path 104 | * @return self $this (chainable) 105 | * @throws Exception on error 106 | * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid 107 | * @uses socket_connect() 108 | */ 109 | public function connect($address) 110 | { 111 | $ret = @socket_connect($this->resource, $this->unformatAddress($address, $port), $port); 112 | if ($ret === false) { 113 | throw Exception::createFromSocketResource($this->resource); 114 | } 115 | return $this; 116 | } 117 | 118 | /** 119 | * Initiates a new connection to given address, wait for up to $timeout seconds 120 | * 121 | * The given $timeout parameter is an upper bound, a maximum time to wait 122 | * for the connection to be either accepted or rejected. 123 | * 124 | * The resulting socket resource will be set to non-blocking mode, 125 | * regardless of its previous state and whether this method succedes or 126 | * if it fails. Make sure to reset with `setBlocking(true)` if you want to 127 | * continue using blocking calls. 128 | * 129 | * @param string $address either of IPv4:port, hostname:port, [IPv6]:port, unix-path 130 | * @param float $timeout maximum time to wait (in seconds) 131 | * @return self $this (chainable) 132 | * @throws Exception on error 133 | * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid 134 | * @uses self::setBlocking() to enable non-blocking mode 135 | * @uses self::connect() to initiate the connection 136 | * @uses self::selectWrite() to wait for the connection to complete 137 | * @uses self::assertAlive() to check connection state 138 | */ 139 | public function connectTimeout($address, $timeout) 140 | { 141 | $this->setBlocking(false); 142 | 143 | try { 144 | // socket is non-blocking, so connect should emit EINPROGRESS 145 | $this->connect($address); 146 | 147 | // socket is already connected immediately? 148 | return $this; 149 | } catch (Exception $e) { 150 | // non-blocking connect() should be EINPROGRESS (or EWOULDBLOCK on Windows) => otherwise re-throw 151 | if ($e->getCode() !== SOCKET_EINPROGRESS && $e->getCode() !== SOCKET_EWOULDBLOCK) { 152 | throw $e; 153 | } 154 | 155 | // connection should be completed (or rejected) within timeout: socket becomes writable on success or error 156 | // Windows requires special care because it uses exceptfds for socket errors: https://github.com/reactphp/event-loop/issues/206 157 | $r = null; 158 | $w = array($this->resource); 159 | $e = DIRECTORY_SEPARATOR === '\\' ? $w : null; 160 | $ret = @socket_select($r, $w, $e, $timeout === null ? null : (int) $timeout, (int) (($timeout - floor($timeout)) * 1000000)); 161 | 162 | if ($ret === false) { 163 | throw Exception::createFromGlobalSocketOperation('Failed to select socket for writing'); 164 | } elseif ($ret === 0) { 165 | throw new Exception('Timed out while waiting for connection', SOCKET_ETIMEDOUT); 166 | } 167 | 168 | // confirm connection success (or fail if connected has been rejected) 169 | $this->assertAlive(); 170 | 171 | return $this; 172 | } 173 | } 174 | 175 | /** 176 | * get socket option 177 | * 178 | * @param int $level 179 | * @param int $optname 180 | * @return mixed 181 | * @throws Exception on error 182 | * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid 183 | * @uses socket_get_option() 184 | */ 185 | public function getOption($level, $optname) 186 | { 187 | $value = @socket_get_option($this->resource, $level, $optname); 188 | if ($value === false) { 189 | throw Exception::createFromSocketResource($this->resource); 190 | } 191 | return $value; 192 | } 193 | 194 | /** 195 | * get remote side's address/path 196 | * 197 | * @return string 198 | * @throws Exception on error 199 | * @throws \Error PHP 8 only: throws \Error when socket is invalid 200 | * @uses socket_getpeername() 201 | */ 202 | public function getPeerName() 203 | { 204 | $ret = @socket_getpeername($this->resource, $address, $port); 205 | if ($ret === false) { 206 | throw Exception::createFromSocketResource($this->resource); 207 | } 208 | return $this->formatAddress($address, $port); 209 | } 210 | 211 | /** 212 | * get local side's address/path 213 | * 214 | * @return string 215 | * @throws Exception on error 216 | * @throws \Error PHP 8 only: throws \Error when socket is invalid 217 | * @uses socket_getsockname() 218 | */ 219 | public function getSockName() 220 | { 221 | $ret = @socket_getsockname($this->resource, $address, $port); 222 | if ($ret === false) { 223 | throw Exception::createFromSocketResource($this->resource); 224 | } 225 | return $this->formatAddress($address, $port); 226 | } 227 | 228 | /** 229 | * start listen for incoming connections 230 | * 231 | * @param int $backlog maximum number of incoming connections to be queued 232 | * @return self $this (chainable) 233 | * @throws Exception on error 234 | * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid 235 | * @see self::bind() has to be called first to bind name to socket 236 | * @uses socket_listen() 237 | */ 238 | public function listen($backlog = 0) 239 | { 240 | $ret = @socket_listen($this->resource, $backlog); 241 | if ($ret === false) { 242 | throw Exception::createFromSocketResource($this->resource); 243 | } 244 | return $this; 245 | } 246 | 247 | /** 248 | * read up to $length bytes from connect()ed / accept()ed socket 249 | * 250 | * The $type parameter specifies if this should use either binary safe reading 251 | * (PHP_BINARY_READ, the default) or stop at CR or LF characters (PHP_NORMAL_READ) 252 | * 253 | * @param int $length maximum length to read 254 | * @param int $type either of PHP_BINARY_READ (the default) or PHP_NORMAL_READ 255 | * @return string 256 | * @throws Exception on error 257 | * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid 258 | * @see self::recv() if you need to pass flags 259 | * @uses socket_read() 260 | */ 261 | public function read($length, $type = PHP_BINARY_READ) 262 | { 263 | $data = @socket_read($this->resource, $length, $type); 264 | if ($data === false) { 265 | throw Exception::createFromSocketResource($this->resource); 266 | } 267 | return $data; 268 | } 269 | 270 | /** 271 | * receive up to $length bytes from connect()ed / accept()ed socket 272 | * 273 | * @param int $length maximum length to read 274 | * @param int $flags 275 | * @return string 276 | * @throws Exception on error 277 | * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid 278 | * @see self::read() if you do not need to pass $flags 279 | * @see self::recvFrom() if your socket is not connect()ed 280 | * @uses socket_recv() 281 | */ 282 | public function recv($length, $flags) 283 | { 284 | $ret = @socket_recv($this->resource, $buffer, $length, $flags); 285 | if ($ret === false) { 286 | throw Exception::createFromSocketResource($this->resource); 287 | } 288 | return $buffer; 289 | } 290 | 291 | /** 292 | * receive up to $length bytes from socket 293 | * 294 | * @param int $length maximum length to read 295 | * @param int $flags 296 | * @param string $remote reference will be filled with remote/peer address/path 297 | * @return string 298 | * @throws Exception on error 299 | * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid 300 | * @see self::recv() if your socket is connect()ed 301 | * @uses socket_recvfrom() 302 | */ 303 | public function recvFrom($length, $flags, &$remote) 304 | { 305 | $ret = @socket_recvfrom($this->resource, $buffer, $length, $flags, $address, $port); 306 | if ($ret === false) { 307 | throw Exception::createFromSocketResource($this->resource); 308 | } 309 | $remote = $this->formatAddress($address, $port); 310 | return $buffer; 311 | } 312 | 313 | /** 314 | * check socket to see if a read/recv/revFrom will not block 315 | * 316 | * @param float|null $sec maximum time to wait (in seconds), 0 = immediate polling, null = no limit 317 | * @return boolean true = socket ready (read will not block), false = timeout expired, socket is not ready 318 | * @throws Exception on error 319 | * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid 320 | * @uses socket_select() 321 | */ 322 | public function selectRead($sec = 0) 323 | { 324 | $usec = $sec === null ? 0 : (int) (($sec - floor($sec)) * 1000000); 325 | $r = array($this->resource); 326 | $n = null; 327 | $ret = @socket_select($r, $n, $n, $sec === null ? null : (int) $sec, $usec); 328 | if ($ret === false) { 329 | throw Exception::createFromGlobalSocketOperation('Failed to select socket for reading'); 330 | } 331 | return !!$ret; 332 | } 333 | 334 | /** 335 | * check socket to see if a write/send/sendTo will not block 336 | * 337 | * @param float|null $sec maximum time to wait (in seconds), 0 = immediate polling, null = no limit 338 | * @return boolean true = socket ready (write will not block), false = timeout expired, socket is not ready 339 | * @throws Exception on error 340 | * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid 341 | * @uses socket_select() 342 | */ 343 | public function selectWrite($sec = 0) 344 | { 345 | $usec = $sec === null ? 0 : (int) (($sec - floor($sec)) * 1000000); 346 | $w = array($this->resource); 347 | $n = null; 348 | $ret = @socket_select($n, $w, $n, $sec === null ? null : (int) $sec, $usec); 349 | if ($ret === false) { 350 | throw Exception::createFromGlobalSocketOperation('Failed to select socket for writing'); 351 | } 352 | return !!$ret; 353 | } 354 | 355 | /** 356 | * send given $buffer to connect()ed / accept()ed socket 357 | * 358 | * @param string $buffer 359 | * @param int $flags 360 | * @return int number of bytes actually written (make sure to check against given buffer length!) 361 | * @throws Exception on error 362 | * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid 363 | * @see self::write() if you do not need to pass $flags 364 | * @see self::sendTo() if your socket is not connect()ed 365 | * @uses socket_send() 366 | */ 367 | public function send($buffer, $flags) 368 | { 369 | $ret = @socket_send($this->resource, $buffer, strlen($buffer), $flags); 370 | if ($ret === false) { 371 | throw Exception::createFromSocketResource($this->resource); 372 | } 373 | return $ret; 374 | } 375 | 376 | /** 377 | * send given $buffer to socket 378 | * 379 | * @param string $buffer 380 | * @param int $flags 381 | * @param string $remote remote/peer address/path 382 | * @return int number of bytes actually written 383 | * @throws Exception on error 384 | * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid 385 | * @see self::send() if your socket is connect()ed 386 | * @uses socket_sendto() 387 | */ 388 | public function sendTo($buffer, $flags, $remote) 389 | { 390 | $ret = @socket_sendto($this->resource, $buffer, strlen($buffer), $flags, $this->unformatAddress($remote, $port), $port); 391 | if ($ret === false) { 392 | throw Exception::createFromSocketResource($this->resource); 393 | } 394 | return $ret; 395 | } 396 | 397 | /** 398 | * enable/disable blocking/nonblocking mode (O_NONBLOCK flag) 399 | * 400 | * @param boolean $toggle 401 | * @return self $this (chainable) 402 | * @throws Exception on error 403 | * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid 404 | * @uses socket_set_block() 405 | * @uses socket_set_nonblock() 406 | */ 407 | public function setBlocking($toggle = true) 408 | { 409 | $ret = $toggle ? @socket_set_block($this->resource) : @socket_set_nonblock($this->resource); 410 | if ($ret === false) { 411 | throw Exception::createFromSocketResource($this->resource); 412 | } 413 | return $this; 414 | } 415 | 416 | /** 417 | * set socket option 418 | * 419 | * @param int $level 420 | * @param int $optname 421 | * @param mixed $optval 422 | * @return self $this (chainable) 423 | * @throws Exception on error 424 | * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid 425 | * @see self::getOption() 426 | * @uses socket_set_option() 427 | */ 428 | public function setOption($level, $optname, $optval) 429 | { 430 | $ret = @socket_set_option($this->resource, $level, $optname, $optval); 431 | if ($ret === false) { 432 | throw Exception::createFromSocketResource($this->resource); 433 | } 434 | return $this; 435 | } 436 | 437 | /** 438 | * shuts down socket for receiving, sending or both 439 | * 440 | * @param int $how 0 = shutdown reading, 1 = shutdown writing, 2 = shutdown reading and writing 441 | * @return self $this (chainable) 442 | * @throws Exception on error 443 | * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid 444 | * @see self::close() 445 | * @uses socket_shutdown() 446 | */ 447 | public function shutdown($how = 2) 448 | { 449 | $ret = @socket_shutdown($this->resource, $how); 450 | if ($ret === false) { 451 | throw Exception::createFromSocketResource($this->resource); 452 | } 453 | return $this; 454 | } 455 | 456 | /** 457 | * write $buffer to connect()ed / accept()ed socket 458 | * 459 | * @param string $buffer 460 | * @return int number of bytes actually written 461 | * @throws Exception on error 462 | * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid 463 | * @see self::send() if you need to pass flags 464 | * @uses socket_write() 465 | */ 466 | public function write($buffer) 467 | { 468 | $ret = @socket_write($this->resource, $buffer); 469 | if ($ret === false) { 470 | throw Exception::createFromSocketResource($this->resource); 471 | } 472 | return $ret; 473 | } 474 | 475 | /** 476 | * get socket type as passed to socket_create() 477 | * 478 | * @return int usually either SOCK_STREAM or SOCK_DGRAM 479 | * @throws Exception on error 480 | * @throws \Error PHP 8 only: throws \Error when socket is invalid 481 | * @uses self::getOption() 482 | */ 483 | public function getType() 484 | { 485 | return $this->getOption(SOL_SOCKET, SO_TYPE); 486 | } 487 | 488 | /** 489 | * assert that this socket is alive and its error code is 0 490 | * 491 | * This will fetch and reset the current socket error code from the 492 | * socket and options and will throw an Exception along with error 493 | * message and code if the code is not 0, i.e. if it does indicate 494 | * an error situation. 495 | * 496 | * Calling this method should not be needed in most cases and is 497 | * likely to not throw an Exception. Each socket operation like 498 | * connect(), send(), etc. will throw a dedicated Exception in case 499 | * of an error anyway. 500 | * 501 | * @return self $this (chainable) 502 | * @throws Exception if error code is not 0 503 | * @throws \Error PHP 8 only: throws \Error when socket is invalid 504 | * @uses self::getOption() to retrieve and clear current error code 505 | * @uses self::getErrorMessage() to translate error code to 506 | */ 507 | public function assertAlive() 508 | { 509 | $code = $this->getOption(SOL_SOCKET, SO_ERROR); 510 | if ($code !== 0) { 511 | throw Exception::createFromCode($code, 'Socket error'); 512 | } 513 | return $this; 514 | } 515 | 516 | /** 517 | * format given address/host/path and port 518 | * 519 | * @param string $address 520 | * @param int $port 521 | * @return string 522 | */ 523 | protected function formatAddress($address, $port) 524 | { 525 | if ($port !== 0) { 526 | if (strpos($address, ':') !== false) { 527 | $address = '[' . $address . ']'; 528 | } 529 | $address .= ':' . $port; 530 | } 531 | return $address; 532 | } 533 | 534 | /** 535 | * format given address by splitting it into returned address and port set by reference 536 | * 537 | * @param string $address 538 | * @param int $port 539 | * @return string address with port removed 540 | */ 541 | protected function unformatAddress($address, &$port) 542 | { 543 | // [::1]:2 => ::1 2 544 | // test:2 => test 2 545 | // ::1 => ::1 546 | // test => test 547 | 548 | $colon = strrpos($address, ':'); 549 | 550 | // there is a colon and this is the only colon or there's a closing IPv6 bracket right before it 551 | if ($colon !== false && (strpos($address, ':') === $colon || strpos($address, ']') === ($colon - 1))) { 552 | $port = (int)substr($address, $colon + 1); 553 | $address = substr($address, 0, $colon); 554 | 555 | // remove IPv6 square brackets 556 | if (substr($address, 0, 1) === '[') { 557 | $address = substr($address, 1, -1); 558 | } 559 | } 560 | return $address; 561 | } 562 | } 563 | --------------------------------------------------------------------------------