diff --git a/README.md b/README.md index d1b5889..7419758 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ # amphp/websocket-server AMPHP is a collection of event-driven libraries for PHP designed with fibers and concurrency in mind. -This library provides a [`RequestHandler`](https://amphp.org/http-server/classes/request-handler) to easily handle Websocket connections using [`amphp/http-server`](https://github.com/amphp/http-server). +This library provides a [`RequestHandler`](https://amphp.org/http-server/classes/request-handler) to easily handle WebSocket connections using [`amphp/http-server`](https://github.com/amphp/http-server). + +## Requirements + +- PHP 8.1+ ## Installation @@ -13,14 +17,51 @@ composer require amphp/websocket-server ## Documentation -The documentation for this library is currently a work in progress. Pull Requests to improve the documentation are -always welcome! +The primary component of this library is the `Websocket` class, an implementation of the `RequestHandler` interface from [`amphp/http-server`](https://github.com/amphp/http-server). Endpoints using the `Websocket` request handler will upgrade incoming requests to a WebSocket connection. -## Requirements +Creating a `Websocket` endpoint requires the user to specify a number of parameters: +- The `Amp\Http\Server\HttpServer` instance which will be used +- A [PSR-3](https://www.php-fig.org/psr/psr-3/) logger instance +- A `WebsocketAcceptor` to accept client connections +- A `WebsocketClientHandler` to handle client connections once accepted +- An optional `WebsocketCompressionContextFactory` if compression should be enabled on the server +- An optional `WebsocketClientFactory` if custom logic is needed when creating `WebsocketClient` instances -- PHP 8.1+ +### Accepting Client Connections + +Accepting client connections is performed by an instance of `WebsocketAcceptor`. This library provides two implementations: and `AllowOriginAcceptor` +- `Rfc6455Acceptor`: Accepts client connections based on [RFC6455](https://datatracker.ietf.org/doc/html/rfc6455) with no further restrictions. +- `AllowOriginAcceptor`: Requires the `"Origin"` header of the HTTP request to match one of the allowed origins provided to the constructor. Accepting the connection is then delegated to another `WebsocketAcceptor` implementation (`Rfc6455Acceptor` by default). + +### Handling Client Connections + +Once established, a WebSocket connection is handled by an implementation of `WebsocketClientHandler`. This is where your WebSocket application logic will go. + +`WebsocketClientHanler` has a single method which must be implemented, `handleClient()`. + +```php +public function handleClient( + WebsocketClient $client, + Request $request, + Response $response, +): void; +``` -## Example +After accepting a client connection, `WebsocketClientHandler::handleClient()` is invoked with the `WebsocketClient` instance, as well as the `Request` and `Response` instances which were used to establish the connection. + +This method should not return until the client connection should be closed. This method generally should not throw an exception. Any exception thrown will close the connection with an `UNEXPECTED_SERVER_ERROR` error code (1011) and forward the exception to the HTTP server logger. There is one exception to this: `WebsocketClosedException`, which is thrown when receiving or sending a message to a connection fails due to the connection being closed. If `WebsocketClosedException` is thrown from `handleClient()`, the exception is ignored. + +### Gateways + +A `WebsocketGateway` provides a means of collecting WebSocket clients into related groups to allow broadcasting a single message efficiently (and asynchronously) to multiple clients. `WebsocketClientGateway` provided by this library may be used by one or more client handlers to group clients from one or more endpoints (or multiple may be used on a single endpoint if desired). See the [example server](#example-server) below for basic usage of a gateway in a client handler. Clients added to the gateway are automatically removed when the client connection is closed. + +### Compression + +Message compression may optionally be enabled on individual WebSocket endpoints by passing an instance of `WebsocketCompressionContextFactory` to the `Websocket` constructor. Currently, the only implementation available is `Rfc7692CompressionFactory` which implements compression based on [RFC-7692](https://datatracker.ietf.org/doc/html/rfc7692). + +### Example Server + +The server below creates a simple WebSocket endpoint which broadcasts all received messages to all other connected clients. [`amphp/http-server-router`](https://github.com/amphp/http-server-router) and [`amphp/http-server-static-content`](https://github.com/amphp/http-server-static-content) are used to attach the `Websocket` handler to a specific route and to serve static files from the `/public` directory if the route is not defined in the router. ```php setFormatter(new ConsoleFormatter); +$logHandler->setFormatter(new ConsoleFormatter()); $logger = new Logger('server'); $logger->pushHandler($logHandler); @@ -61,7 +102,7 @@ $server->expose(new Socket\InternetAddress('[::1]', 1337)); $errorHandler = new DefaultErrorHandler(); -$handshakeHandler = new AllowOriginAcceptor( +$acceptor = new AllowOriginAcceptor( ['http://localhost:1337', 'http://127.0.0.1:1337', 'http://[::1]:1337'], ); @@ -73,13 +114,13 @@ $clientHandler = new class implements WebsocketClientHandler { public function handleClient( WebsocketClient $client, - Request $request, - Response $response + Request $request, + Response $response, ): void { $this->gateway->addClient($client); - while ($message = $client->receive()) { - $this->gateway->broadcastText(\sprintf( + foreach ($client as $message) { + $this->gateway->broadcastText(sprintf( '%d: %s', $client->getId(), (string) $message, @@ -88,18 +129,30 @@ $clientHandler = new class implements WebsocketClientHandler { } }; -$websocket = new Websocket($server, $logger, $handshakeHandler, $clientHandler); +$websocket = new Websocket($server, $logger, $acceptor, $clientHandler); -$router = new Router($server, new NullLogger(), $errorHandler); +$router = new Router($server, $logger, $errorHandler); $router->addRoute('GET', '/broadcast', $websocket); $router->setFallback(new DocumentRoot($server, $errorHandler, __DIR__ . '/public')); $server->start($router, $errorHandler); // Await SIGINT or SIGTERM to be received. -$signal = Amp\trapSignal([\SIGINT, \SIGTERM]); +$signal = trapSignal([SIGINT, SIGTERM]); -$logger->info(\sprintf("Received signal %d, stopping HTTP server", $signal)); +$logger->info(sprintf("Received signal %d, stopping HTTP server", $signal)); $server->stop(); ``` + +## Versioning + +`amphp/websocket-server` follows the [semver](http://semver.org/) semantic versioning specification like all other `amphp` packages. + +## Security + +If you discover any security related issues, please use the private security issue reporter instead of using the public issue tracker. + +## License + +The MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information.