/**
  * 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);
     }
 }