/** * Coroutine that connects to a WebSocket server. * * @param Uri $uri HTTP URI (use http / https as scheme instead of ws / wss). * @param array $options Stream context options (e.g. SSL settings) to be passed to SocketStream::connect(). * @param LoggerInterface $logger * @return WebSocketConnector */ public static function connect(Uri $uri, array $options = [], LoggerInterface $logger = NULL) : \Generator { $secure = $uri->getScheme() === 'https'; $host = $uri->getHost(); $port = $uri->getPort() ?? ($secure ? 443 : 80); $socket = (yield from SocketStream::connect($host, $port, 'tcp', 0, array_replace_recursive($options, ['socket' => ['tcp_nodelay' => true]]))); try { if ($secure) { yield from $socket->encryptClient(); } $nonce = base64_encode(random_bytes(16)); $message = sprintf("GET /%s HTTP/1.1\r\n", $uri->getPath()); $message .= "Upgrade: websocket\r\n"; $message .= "Connection: Upgrade\r\n"; $message .= "Content-Length: 0\r\n"; $message .= sprintf("Host: %s\r\n", $host); $message .= sprintf("Sec-WebSocket-Key: %s\r\n", $nonce); $message .= "Sec-WebSocket-Version: 13\r\n"; $message .= "\r\n"; yield from $socket->write($message); $m = NULL; $line = (yield from $socket->readLine()); if (!preg_match("'^HTTP/(1\\.[01])\\s+([1-5][0-9]{2})\\s*(.*)\$'i", $line, $m)) { throw new WebSocketException('Server did not respond with a valid HTTP/1 status line'); } $response = new HttpResponse(); $response = $response->withStatus((int) $m[2], trim($m[3])); $response = $response->withProtocolVersion($m[1]); while (!$socket->eof()) { $line = (yield from $socket->readLine()); if ($line === '') { break; } $header = array_map('trim', explode(':', $line, 2)); $response = $response->withAddedHeader(...$header); } if ($response->getProtocolVersion() !== '1.1') { throw new WebSocketException('WebSocket handshake requires HTTP/1.1 response'); } if ($response->getStatusCode() !== 101) { throw new WebSocketException('WebSocket protocol switching failed'); } $accept = base64_encode(sha1($nonce . self::GUID, true)); if ($accept !== $response->getHeaderLine('Sec-WebSocket-Accept')) { throw new WebSocketException('Invalid WebSocket Accept header received'); } } catch (\Throwable $e) { $socket->close(); throw $e; } $conn = new static($socket, (yield eventEmitter()), $logger); $conn->watcher = (yield runTask($conn->handleFrames(), 'WebSocket Message Handler')); $conn->watcher->setAutoShutdown(true); return $conn; }
/** * {@inheritdoc} */ public function upgradeConnection(BufferedDuplexStreamInterface $socket, HttpRequest $request, HttpResponse $response, HttpEndpoint $endpoint, callable $action) : \Generator { $response = $this->assertUpgradePossible($request, $response); if ($response instanceof HttpResponse) { return $response; } // Discard HTTP request body before switching to web socket protocol. $body = (yield from $request->getBody()->getInputStream()); try { while (!$body->eof()) { yield from $body->read(); } } finally { $body->close(); } ((yield currentTask()))->setAutoShutdown(true); try { yield from $this->sendHandshake($socket, $request); if ($this->logger) { $this->logger->debug('Upgraded HTTP/{version} connection to {protocol}', ['version' => $request->getProtocolVersion(), 'protocol' => 'WebSocket']); } $client = new Client($socket, $this->logger); $result = $this->app->open($client); if ($result instanceof \Generator) { yield from $result; } $decoder = new MessageDecoder($socket, true, $this->logger); while (!$socket->eof()) { $message = (yield from $decoder->readNextMessage($client->id)); if ($message === false) { break; } $client->time = time(); if ($message instanceof Frame) { switch ($message->opcode) { case Frame::CONNECTION_CLOSE: break 2; case Frame::PING: yield from $socket->write((new Frame(Frame::PONG, $message->data))->encode(), 10000); break; } continue; } if (\is_string($message)) { $result = $this->app->onMessage($message, $client); } elseif ($message instanceof InputStreamInterface) { $result = $this->app->onBinaryMessage($message, $client); } if ($result instanceof \Generator) { $task = (yield runTask($result)); $task->onError(function (ExecutorInterface $executor, Task $task, \Throwable $e) { $this->app->onError($e); }); } } } catch (TaskCanceledException $e) { yield from $this->sendCloseFrame($socket, $e); $client->disconnect(); $result = $this->app->close($client); if ($result instanceof \Generator) { yield from $result; } } finally { $socket->close(); } }
private function broadcastFrame(Frame $frame, array $clients, Client $sender = NULL, bool $excludeSender = true) : \Generator { foreach ($clients as $client) { if ($excludeSender && $client === $sender) { continue; } $task = (yield runTask($client->sendFrame($frame))); $task->onError(function (ExecutorInterface $executor, Task $task, \Throwable $e) { $this->onError($e); }); (yield NULL); } }