├── .github └── FUNDING.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src └── Factory.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: clue 2 | custom: https://clue.engineering/support 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.2.0 (2024-04-05) 4 | 5 | * Feature: Full PHP 8.3 compatibility. 6 | (#22 by @yadaiio) 7 | 8 | * Feature: Simplify usage by supporting new [default loop](https://reactphp.org/event-loop/#loop). 9 | (#21 by @PaulRotmann) 10 | 11 | * Minor documentation improvements. 12 | (#23 by @yadaiio and #20 by @PaulRotmann) 13 | 14 | ## 1.1.0 (2020-12-06) 15 | 16 | * Feature: Forward compatibility with stable EventLoop 1.0 and 0.5. 17 | (#11 by @clue) 18 | 19 | * Improve documentation and add API docs (docblocks). 20 | (#10 and #12 by @clue) 21 | 22 | * Improve test suite and add `.gitattributes` to exclude dev files from export. 23 | Update to PHPUnit 9 and simplify test setup. 24 | (#8, #9, #17 and #18 by @clue and #15 by @SimonFrings) 25 | 26 | ## 1.0.0 (2016-03-07) 27 | 28 | * First stable release, now following SemVer 29 | * Improved documentation 30 | 31 | > Contains no other changes, so it's actually fully compatible with the v0.2.0 release. 32 | 33 | ## 0.2.0 (2015-03-26) 34 | 35 | * Changed to use faster stream based networking API 36 | ([#6](https://github.com/clue/php-multicast-react/pull/6)) 37 | * Reduce footprint of required dependencies 38 | * Only require `ext-sockets` for *listening* on multicast addresses 39 | 40 | ## 0.1.0 (2015-03-24) 41 | 42 | * First tagged release 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 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/reactphp-multicast 2 | 3 | [![CI status](https://github.com/clue/reactphp-multicast/actions/workflows/ci.yml/badge.svg)](https://github.com/clue/reactphp-multicast/actions) 4 | [![installs on Packagist](https://img.shields.io/packagist/dt/clue/multicast-react?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/clue/multicast-react) 5 | 6 | Simple, event-driven multicast UDP message client and server for [ReactPHP](https://reactphp.org/). 7 | 8 | Multicast UDP messages are needed for quite a few (low-level) networking protocols. 9 | Among others, multicast networking is the basis for mDNS (Multicast DNS), 10 | HTTPMU (Multicast UDP HTTP Messages), UPnP/SSDP (Universal Plug and Play / 11 | Simple Service Discovery Protocol) and others. 12 | This library exposes a simple subset of commonly needed functionality for 13 | multicast networking through an easy to use API. 14 | 15 | **Table of contents** 16 | 17 | * [Support us](#support-us) 18 | * [Quickstart example](#quickstart-example) 19 | * [Usage](#usage) 20 | * [Factory](#factory) 21 | * [createSender()](#createsender) 22 | * [createReceiver()](#createreceiver) 23 | * [SocketInterface](#socketinterface) 24 | * [Install](#install) 25 | * [Tests](#tests) 26 | * [License](#license) 27 | 28 | ## Support us 29 | 30 | We invest a lot of time developing, maintaining and updating our awesome 31 | open-source projects. You can help us sustain this high-quality of our work by 32 | [becoming a sponsor on GitHub](https://github.com/sponsors/clue). Sponsors get 33 | numerous benefits in return, see our [sponsoring page](https://github.com/sponsors/clue) 34 | for details. 35 | 36 | Let's take these projects to the next level together! 🚀 37 | 38 | ## Quickstart example 39 | 40 | Once [installed](#install), you can use the following code to create a simple 41 | echo server that listens for incoming multicast messages: 42 | 43 | ```php 44 | createReceiver('224.10.20.30:4050'); 50 | 51 | $socket->on('message', function ($data, $remote) use ($socket) { 52 | echo 'Sending back ' . strlen($data) . ' bytes to ' . $remote . PHP_EOL; 53 | $socket->send($data, $remote); 54 | }); 55 | 56 | ``` 57 | 58 | See also the [examples](examples/). 59 | 60 | ## Usage 61 | 62 | ### Factory 63 | 64 | The `Factory` is responsible for creating your [`SocketInterface`](#socketinterface) instances. 65 | 66 | ```php 67 | $factory = new Clue\React\Multicast\Factory(); 68 | ``` 69 | 70 | This class takes an optional `LoopInterface|null $loop` parameter that can be used to 71 | pass the event loop instance to use for this object. You can use a `null` value 72 | here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). 73 | This value SHOULD NOT be given unless you're sure you want to explicitly use a 74 | given event loop instance. 75 | 76 | #### createSender() 77 | 78 | The `createSender(): SocketInterface` method can be used to 79 | create a socket capable of sending outgoing multicast datagrams and receiving 80 | incoming unicast responses. It returns a [`SocketInterface`](#socketinterface) instance. 81 | 82 | ```php 83 | $socket = $factory->createSender(); 84 | 85 | // send a multicast message to everybody listening on the given address 86 | $socket->send('hello?', '224.10.20.30:4050'); 87 | 88 | // report incoming unicast replies 89 | $socket->on('message', function ($data, $address) { 90 | echo 'received ' . strlen($data) . ' bytes from ' . $address . PHP_EOL; 91 | }); 92 | ``` 93 | 94 | This method works on PHP versions as old as PHP 5.3 (and up), as its socket API has always been 95 | [level 1 multicast conformant](https://www.tldp.org/HOWTO/Multicast-HOWTO-2.html#ss2.2). 96 | 97 | #### createReceiver() 98 | 99 | The `createReceiver(string $address): SocketInterface` method can be used to 100 | create a socket capable of receiving incoming multicast datagrams and sending 101 | outgoing unicast or multicast datagrams. It returns a [`SocketInterface`](#socketinterface) instance. 102 | 103 | ```php 104 | $socket = $factory->createReceiver('224.10.20.30:4050'); 105 | 106 | // report incoming multicast messages 107 | $socket->on('message', function ($data, $remote) use ($socket) { 108 | echo 'Sending back ' . strlen($data) . ' bytes to ' . $remote . PHP_EOL; 109 | 110 | // send a unicast reply to the remote 111 | $socket->send($data, $remote); 112 | }); 113 | ``` 114 | 115 | This method requires PHP 5.4+ and `ext-sockets`. 116 | Otherwise, it will throw a `BadMethodCallException`. 117 | This is a requirement because receiving multicast datagrams requires a 118 | [level 2 multicast conformant](https://www.tldp.org/HOWTO/Multicast-HOWTO-2.html#ss2.2) 119 | socket API. 120 | The required multicast socket options and constants have been added with PHP 5.4+. 121 | These options are only available to the low level socket API (ext-sockets), not 122 | to the newer stream based networking API. 123 | 124 | Internally, this library uses a workaround to create stream based sockets 125 | and then sets the required socket options on its underlying low level socket 126 | resource. 127 | This is done because ReactPHP is built around the general purpose stream based API 128 | and has only somewhat limited support for the low level socket API. 129 | 130 | ### SocketInterface 131 | 132 | The [`Factory`](#factory) creates instances of the `React\Datagram\SocketInterface` 133 | from the [react/datagram](https://github.com/reactphp/datagram) package. 134 | This means that you can use all its normal methods like so: 135 | 136 | ```php 137 | $socket->send($message, $address); 138 | 139 | $socket->on('message', function ($message, $address) { }); 140 | $socket->on('close', function() { }); 141 | 142 | $socket->pause(); 143 | $socket->resume(); 144 | 145 | $socket->end(); 146 | $socket->close(); 147 | ``` 148 | 149 | Please refer to the [datagram documentation](https://github.com/reactphp/datagram#usage) for more details. 150 | 151 | ## Install 152 | 153 | The recommended way to install this library is [through Composer](https://getcomposer.org/). 154 | [New to Composer?](https://getcomposer.org/doc/00-intro.md) 155 | 156 | This project follows [SemVer](https://semver.org/). 157 | This will install the latest supported version: 158 | 159 | ```bash 160 | composer require clue/multicast-react:^1.2 161 | ``` 162 | 163 | See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. 164 | 165 | This project aims to run on any platform and thus does not require any PHP 166 | extensions and supports running on legacy PHP 5.3 through current PHP 8+ and 167 | HHVM. 168 | It's *highly recommended to use the latest supported PHP version* for this project. 169 | 170 | The [`createSender()`](#createsender) method works on all supported platforms 171 | without any additional requirements. However, the [`createReceiver()`](#createreceiver) 172 | method requires PHP 5.4 (or up) and `ext-sockets`. See above for more details. 173 | 174 | ## Tests 175 | 176 | To run the test suite, you first need to clone this repo and then install all 177 | dependencies [through Composer](https://getcomposer.org/): 178 | 179 | ```bash 180 | composer install 181 | ``` 182 | 183 | To run the test suite, go to the project root and run: 184 | 185 | ```bash 186 | vendor/bin/phpunit 187 | ``` 188 | 189 | ## License 190 | 191 | This project is released under the permissive [MIT license](LICENSE). 192 | 193 | > Did you know that I offer custom development services and issuing invoices for 194 | sponsorships of releases and for contributions? Contact me (@clue) for details. 195 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clue/multicast-react", 3 | "description": "Simple, event-driven multicast UDP message client and server for ReactPHP.", 4 | "keywords": ["multicast", "mcast", "udp", "ReactPHP", "async"], 5 | "homepage": "https://github.com/clue/reactphp-multicast", 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 | "react/datagram": "^1.10", 16 | "react/event-loop": "^1.2" 17 | }, 18 | "require-dev": { 19 | "clue/hexdump": "0.2.*", 20 | "phpunit/phpunit": "^9.6 ||^6.0 || ^5.7 || ^4.8.36" 21 | }, 22 | "suggest": { 23 | "ext-sockets": "Requires PHP 5.4+ and the low level socket API for listening on multicast addresses (socket options to send IGMP announcements)" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Clue\\React\\Multicast\\": "src/" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Clue\\Tests\\React\\Multicast\\": "tests/" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Factory.php: -------------------------------------------------------------------------------- 1 | loop = $loop ?: Loop::get(); 34 | } 35 | 36 | /** 37 | * Creates a socket capable of sending outgoing multicast datagrams and receiving 38 | * incoming unicast responses. It returns a [`SocketInterface`](#socketinterface) instance. 39 | * 40 | * ```php 41 | * $socket = $factory->createSender(); 42 | * 43 | * // send a multicast message to everybody listening on the given address 44 | * $socket->send('hello?', '224.10.20.30:4050'); 45 | * 46 | * // report incoming unicast replies 47 | * $socket->on('message', function ($data, $address) { 48 | * echo 'received ' . strlen($data) . ' bytes from ' . $address . PHP_EOL; 49 | * }); 50 | * ``` 51 | * 52 | * This method works on PHP versions as old as PHP 5.3 (and up), as its socket API has always been 53 | * [level 1 multicast conformant](https://www.tldp.org/HOWTO/Multicast-HOWTO-2.html#ss2.2). 54 | * 55 | * @return \React\Datagram\SocketInterface 56 | * @throws RuntimeException 57 | */ 58 | public function createSender() 59 | { 60 | $stream = @stream_socket_server('udp://0.0.0.0:0', $errno, $errstr, STREAM_SERVER_BIND); 61 | if ($stream === false) { 62 | throw new RuntimeException('Unable to create sending socket: ' . $errstr, $errno); 63 | } 64 | 65 | return new DatagramSocket($this->loop, $stream); 66 | } 67 | 68 | /** 69 | * Creates a socket capable of receiving incoming multicast datagrams and sending 70 | * outgoing unicast or multicast datagrams. It returns a [`SocketInterface`](#socketinterface) instance. 71 | * 72 | * ```php 73 | * $socket = $factory->createReceiver('224.10.20.30:4050'); 74 | * 75 | * // report incoming multicast messages 76 | * $socket->on('message', function ($data, $remote) use ($socket) { 77 | * echo 'Sending back ' . strlen($data) . ' bytes to ' . $remote . PHP_EOL; 78 | * 79 | * // send a unicast reply to the remote 80 | * $socket->send($data, $remote); 81 | * }); 82 | * ``` 83 | * 84 | * This method requires PHP 5.4+ and `ext-sockets`. 85 | * Otherwise, it will throw a `BadMethodCallException`. 86 | * This is a requirement because receiving multicast datagrams requires a 87 | * [level 2 multicast conformant](https://www.tldp.org/HOWTO/Multicast-HOWTO-2.html#ss2.2) 88 | * socket API. 89 | * The required multicast socket options and constants have been added with PHP 5.4+. 90 | * These options are only available to the low level socket API (ext-sockets), not 91 | * to the newer stream based networking API. 92 | * 93 | * Internally, this library uses a workaround to create stream based sockets 94 | * and then sets the required socket options on its underlying low level socket 95 | * resource. 96 | * This is done because ReactPHP is built around the general purpose stream based API 97 | * and has only somewhat limited support for the low level socket API. 98 | * 99 | * @param string $address 100 | * @return \React\Datagram\SocketInterface 101 | * @throws BadMethodCallException 102 | * @throws RuntimeException 103 | */ 104 | public function createReceiver($address) 105 | { 106 | if (!defined('MCAST_JOIN_GROUP')) { 107 | throw new BadMethodCallException('MCAST_JOIN_GROUP not defined (requires PHP 5.4+)'); 108 | } 109 | if (!function_exists('socket_import_stream')) { 110 | throw new BadMethodCallException('Function socket_import_stream missing (requires ext-sockets and PHP 5.4+)'); 111 | } 112 | 113 | $parts = parse_url('udp://' . $address); 114 | 115 | $stream = @stream_socket_server('udp://0.0.0.0:' . $parts['port'], $errno, $errstr, STREAM_SERVER_BIND); 116 | if ($stream === false) { 117 | throw new RuntimeException('Unable to create receiving socket: ' . $errstr, $errno); 118 | } 119 | 120 | $socket = socket_import_stream($stream); 121 | if ($stream === false) { 122 | throw new RuntimeException('Unable to access underlying socket resource'); 123 | } 124 | 125 | // allow multiple processes to bind to the same address 126 | $ret = socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1); 127 | if ($ret === false) { 128 | throw new RuntimeException('Unable to enable SO_REUSEADDR'); 129 | } 130 | 131 | // join multicast group and bind to port 132 | $ret = socket_set_option( 133 | $socket, 134 | IPPROTO_IP, 135 | MCAST_JOIN_GROUP, 136 | array('group' => $parts['host'], 'interface' => 0) 137 | ); 138 | if ($ret === false) { 139 | throw new RuntimeException('Unable to join multicast group'); 140 | } 141 | 142 | return new DatagramSocket($this->loop, $stream); 143 | } 144 | } 145 | --------------------------------------------------------------------------------