Вот мой сокет сервер
<?php

abstract class WebSocketServer
{
    protected $socket_tick = 1000000;
    protected $userClass = 'WebSocketUser';
    protected $maxBufferSize;
    protected $master;
    protected $sockets = [];
    protected $usersBySocket = [];
    protected $users = [];
    protected $heldMessages = [];
    protected $interactive = false;
    protected $headerOriginRequired = false;
    protected $headerSecWebSocketProtocolRequired = false;
    protected $headerSecWebSocketExtensionsRequired = false;

    public function __construct($addr, $port, $bufferLength, $socket_tick)
    {
        $this->socket_tick = $socket_tick;
        $this->maxBufferSize = $bufferLength;
        $this->master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or die('Failed: socket_create()');
        socket_set_option($this->master, SOL_SOCKET, SO_REUSEADDR, 1) or die('Failed: socket_option()');
        socket_bind($this->master, $addr, $port) or die('Failed: socket_bind()');
        socket_listen($this->master, 1024) or die('Failed: socket_listen()'); // увеличен backlog
        $this->sockets['m'] = $this->master;
    }

    abstract protected function process($user, $message);

    abstract protected function connected($user);

    abstract protected function closed($user);

    protected function checkHost($hostName)
    {
        return true;  // Override and return false if the host is not one that you would expect.
        // Ex: You only want to accept hosts from the my-domain.com domain,
        // but you receive a host from malicious-site.com instead.
    }

    protected function checkOrigin($origin)
    {
        return true;  // Override and return false if the origin is not one that you would expect.
    }

    protected function checkWebsocProtocol($protocol)
    {
        return true;  // Override and return false if a protocol is not found that you would expect.
    }

    protected function checkWebsocExtensions($extensions)
    {
        return true;  // Override and return false if an extension is not found that you would expect.
    }

    protected function processProtocol($protocol)
    {
        return '';  // return either "Sec-WebSocket-Protocol: SelectedProtocolFromClientList\r\n" or return an empty string.
        // The carriage return/newline combo must appear at the end of a non-empty string, and must not
        // appear at the beginning of the string nor in an otherwise empty string, or it will be considered part of
        // the response body, which will trigger an error in the client as it will not be formatted correctly.
    }

    protected function processExtensions($extensions)
    {
        return '';  // return either "Sec-WebSocket-Extensions: SelectedExtensions\r\n" or return an empty string.
    }

    protected function connecting($user) {}

    protected function send($user, $message)
    {
        if ($user->handshake) {
            $data = $this->frame($message, $user);
            @socket_write($user->socket, $data, strlen($data));
        } else {
            $this->heldMessages[] = ['user' => $user, 'message' => $message];
        }
    }

    protected function tick() {}

    protected function _tick()
    {
        $newHeldMessages = [];
        foreach ($this->heldMessages as $hm) {
            $user = $hm['user'];
            $message = $hm['message'];
            $currentUser = $this->usersBySocket[(int)$user->socket] ?? null;
            if ($currentUser === null) {
                continue;
            }
            if (!$currentUser->handshake) {
                $newHeldMessages[] = $hm;
                continue;
            }
            $this->send($currentUser, $message);
        }
        $this->heldMessages = $newHeldMessages;
    }

    public function run()
    {
        if (!isset($this->sockets['m'])) {
            $this->sockets['m'] = $this->master;
        }

        while (true) {
            $read = $this->sockets;
            $write = $except = null;

            $this->_tick();
            $this->tick();

            $numChanged = @socket_select($read, $write, $except, 0, $this->socket_tick);

            if ($numChanged === false) {
                continue;
            }

            if ($numChanged === 0) {
                continue;
            }

            foreach ($read as $socket) {
                if ($socket === $this->master) {
                    $client = @socket_accept($socket);
                    if ($client === false) {
                        continue;
                    }
                    $this->connect($client);
                } else {
                    $numBytes = @socket_recv($socket, $buffer, $this->maxBufferSize, 0);
                    if ($numBytes === false) {
                        $sockErrNo = socket_last_error($socket);
                        switch ($sockErrNo) {
                            case 102:
                            case 103:
                            case 104:
                            case 108:
                            case 110:
                            case 111:
                            case 112:
                            case 113:
                            case 121:
                            case 125:
                                $this->disconnect($socket, true, $sockErrNo);
                                break;
                        }
                    } elseif ($numBytes === 0) {
                        $this->disconnect($socket);
                    } else {
                        $user = $this->getUserBySocket($socket);
                        if ($user === null) {
                            continue;
                        }
                        if (!$user->handshake) {
                            if (strpos($buffer, "\r\n\r\n") === false) {
                                continue;
                            }
                            $this->doHandshake($user, $buffer);
                        } else {
                            $payload = substr($buffer, 0, $numBytes);
                            $this->split_packet($numBytes, $payload, $user);
                        }
                    }
                }
            }
        }
    }

    protected function connect($socket)
    {
        $user = new $this->userClass(uniqid('u'), $socket);
        $this->users[$user->id] = $user;
        $this->sockets[$user->id] = $socket;
        $this->usersBySocket[(int)$socket] = $user;
        $this->connecting($user);
    }

    protected function disconnect($socket, $triggerClosed = true, $sockErrNo = null)
    {
        $user = $this->usersBySocket[(int)$socket] ?? null;
        if ($user === null) {
            return;
        }
        unset($this->users[$user->id], $this->sockets[$user->id], $this->usersBySocket[(int)$socket]);
        if ($sockErrNo !== null) {
            socket_clear_error($socket);
        }
        if ($triggerClosed) {
            $this->closed($user);
            socket_close($user->socket);
        } else {
            $msg = $this->frame('', $user, 'close');
            @socket_write($user->socket, $msg, strlen($msg));
        }
    }

    protected function doHandshake($user, $buffer)
    {
        $magicGUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
        $headers = [];

        $lines = preg_split('/\r\n|\r|\n/', $buffer);
        foreach ($lines as $line) {
            if (strpos($line, ':') !== false) {
                list($name, $value) = explode(':', $line, 2);
                $headers[strtolower(trim($name))] = trim($value);
            } elseif (preg_match('/^GET\s+(.*?)\s+HTTP\/\d\.\d$/i', $line, $matches)) {
                $headers['get'] = $matches[1];
            }
        }

        if (!isset($headers['get'])) {
            $this->sendHttpResponse($user->socket, "HTTP/1.1 405 Method Not Allowed\r\n\r\n");
            $this->disconnect($user->socket);
            return;
        }

        $user->requestedResource = $headers['get'];

        if (!isset($headers['host']) || !$this->checkHost($headers['host'])) {
            $this->sendHttpResponse($user->socket, "HTTP/1.1 400 Bad Request\r\n\r\n");
            $this->disconnect($user->socket);
            return;
        }
        if (!isset($headers['upgrade']) || strtolower($headers['upgrade']) !== 'websocket') {
            $this->sendHttpResponse($user->socket, "HTTP/1.1 400 Bad Request\r\n\r\n");
            $this->disconnect($user->socket);
            return;
        }
        if (!isset($headers['connection']) || stripos($headers['connection'], 'upgrade') === false) {
            $this->sendHttpResponse($user->socket, "HTTP/1.1 400 Bad Request\r\n\r\n");
            $this->disconnect($user->socket);
            return;
        }
        if (!isset($headers['sec-websocket-key'])) {
            $this->sendHttpResponse($user->socket, "HTTP/1.1 400 Bad Request\r\n\r\n");
            $this->disconnect($user->socket);
            return;
        }
        if (!isset($headers['sec-websocket-version']) || trim($headers['sec-websocket-version']) !== '13') {
            $this->sendHttpResponse($user->socket, "HTTP/1.1 426 Upgrade Required\r\nSec-WebSocket-Version: 13\r\n\r\n");
            $this->disconnect($user->socket);
            return;
        }
        if ($this->headerOriginRequired && (!isset($headers['origin']) || !$this->checkOrigin($headers['origin']))) {
            $this->sendHttpResponse($user->socket, "HTTP/1.1 403 Forbidden\r\n\r\n");
            $this->disconnect($user->socket);
            return;
        }
        if ($this->headerSecWebSocketProtocolRequired && (!isset($headers['sec-websocket-protocol']) || !$this->checkWebsocProtocol($headers['sec-websocket-protocol']))) {
            $this->sendHttpResponse($user->socket, "HTTP/1.1 400 Bad Request\r\n\r\n");
            $this->disconnect($user->socket);
            return;
        }
        if ($this->headerSecWebSocketExtensionsRequired && (!isset($headers['sec-websocket-extensions']) || !$this->checkWebsocExtensions($headers['sec-websocket-extensions']))) {
            $this->sendHttpResponse($user->socket, "HTTP/1.1 400 Bad Request\r\n\r\n");
            $this->disconnect($user->socket);
            return;
        }

        $acceptKey = base64_encode(sha1($headers['sec-websocket-key'] . $magicGUID, true));
        $user->headers = $headers;
        $user->handshake = true;

        $subProtocol = isset($headers['sec-websocket-protocol']) ? $this->processProtocol($headers['sec-websocket-protocol']) : '';
        $extensions = isset($headers['sec-websocket-extensions']) ? $this->processExtensions($headers['sec-websocket-extensions']) : '';

        $response = "HTTP/1.1 101 Switching Protocols\r\n";
        $response .= "Upgrade: websocket\r\n";
        $response .= "Connection: Upgrade\r\n";
        $response .= "Sec-WebSocket-Accept: $acceptKey\r\n";
        if ($subProtocol !== '') {
            $response .= $subProtocol;
        }
        if ($extensions !== '') {
            $response .= $extensions;
        }
        $response .= "\r\n";

        socket_write($user->socket, $response);

        $this->connected($user);
    }

    protected function sendHttpResponse($socket, string $response): void
    {
        @socket_write($socket, $response, strlen($response));
    }

    protected function getUserBySocket($socket)
    {
        return $this->usersBySocket[(int)$socket] ?? null;
    }

    protected function frame($message, $user, $messageType = 'text', $messageContinues = false)
    {
        switch ($messageType) {
            case 'continuous':
                $b1 = 0;
                break;
            case 'text':
                $b1 = ($user->sendingContinuous) ? 0 : 1;
                break;
            case 'binary':
                $b1 = ($user->sendingContinuous) ? 0 : 2;
                break;
            case 'close':
                $b1 = 8;
                break;
            case 'ping':
                $b1 = 9;
                break;
            case 'pong':
                $b1 = 10;
                break;
            default:
                $b1 = 1;
                break;
        }
        if ($messageContinues) {
            $user->sendingContinuous = true;
        } else {
            $b1 |= 0x80;
            $user->sendingContinuous = false;
        }

        $length = strlen($message);
        $lengthField = '';

        if ($length < 126) {
            $b2 = $length;
        } elseif ($length <= 0xFFFF) {
            $b2 = 126;
            $lengthField = pack('n', $length);
        } else {
            $b2 = 127;
            $high = ($length & 0xFFFFFFFF00000000) >> 32;
            $low = $length & 0xFFFFFFFF;
            $lengthField = pack('NN', $high, $low);
        }

        return chr($b1) . chr($b2) . $lengthField . $message;
    }

    protected function split_packet($length, $packet, $user)
    {
        if ($user->handlingPartialPacket) {
            $packet = $user->partialBuffer . $packet;
            $user->handlingPartialPacket = false;
            $length = strlen($packet);
        }
        $frame_pos = 0;
        while ($frame_pos < $length) {
            $headers = $this->extractHeaders(substr($packet, $frame_pos));
            if ($headers === null || !isset($headers['length'])) {
                $user->handlingPartialPacket = true;
                $user->partialBuffer = substr($packet, $frame_pos);
                break;
            }
            $headers_size = $this->calcoffset($headers);
            $framesize = $headers['length'] + $headers_size;
            if ($frame_pos + $framesize > $length) {
                $user->handlingPartialPacket = true;
                $user->partialBuffer = substr($packet, $frame_pos);
                break;
            }
            $frame = substr($packet, $frame_pos, $framesize);
            $message = $this->deframe($frame, $user, $headers);
            if ($message !== false) {
                if ($user->hasSentClose) {
                    $this->disconnect($user->socket);
                    break;
                } else {
                    if (preg_match('//u', $message) || $headers['opcode'] == 2) {
                        $this->process($user, $message);
                    }
                }
            }
            $frame_pos += $framesize;
        }
    }

    protected function calcoffset($headers)
    {
        $offset = 2;
        if ($headers['hasmask']) {
            $offset += 4;
        }
        if ($headers['length'] > 65535) {
            $offset += 8;
        } elseif ($headers['length'] > 125) {
            $offset += 2;
        }
        return $offset;
    }

    protected function deframe($message, &$user)
    {
        $headers = $this->extractHeaders($message);
        $pongReply = false;
        $willClose = false;
        switch ($headers['opcode']) {
            case 0:
            case 1:
            case 2:
                break;
            case 8:
                $user->hasSentClose = true;
                return '';
            case 9:
                $pongReply = true;
            case 10:
                break;
            default:
                $willClose = true;
                break;
        }
        if ($this->checkRSVBits($headers, $user)) {
            return false;
        }
        if ($willClose) {
            return false;
        }
        $payload = $user->partialMessage . $this->extractPayload($message, $headers);
        if ($pongReply) {
            $reply = $this->frame($payload, $user, 'pong');
            socket_write($user->socket, $reply, strlen($reply));
            return false;
        }
        if ($headers['length'] > strlen($this->applyMask($headers, $payload))) {
            $user->handlingPartialPacket = true;
            $user->partialBuffer = $message;
            return false;
        }
        $payload = $this->applyMask($headers, $payload);
        if ($headers['fin']) {
            $user->partialMessage = '';
            return $payload;
        }
        $user->partialMessage = $payload;
        return false;
    }

    protected function extractHeaders($message)
    {
        if (strlen($message) < 2) {
            return null;
        }
        $byte0 = ord($message[0]);
        $byte1 = ord($message[1]);
        $header = [
            'fin' => ($byte0 & 0x80) !== 0,
            'rsv1' => ($byte0 & 0x40) !== 0,
            'rsv2' => ($byte0 & 0x20) !== 0,
            'rsv3' => ($byte0 & 0x10) !== 0,
            'opcode' => $byte0 & 0x0f,
            'hasmask' => ($byte1 & 0x80) !== 0,
            'length' => 0,
            'mask' => ''
        ];
        $payloadLen = $byte1 & 0x7F;
        $offset = 2;
        if ($payloadLen <= 125) {
            $header['length'] = $payloadLen;
        } elseif ($payloadLen === 126) {
            if (strlen($message) < 4) {
                return null;
            }
            $header['length'] = (ord($message[2]) << 8) | ord($message[3]);
            $offset += 2;
        } elseif ($payloadLen === 127) {
            if (strlen($message) < 10) {
                return null;
            }
            $lengthBytes = substr($message, 2, 8);
            $header['length'] = 0;
            for ($i = 0; $i < 8; $i++) {
                $header['length'] <<= 8;
                $header['length'] |= ord($lengthBytes[$i]);
            }
            $offset += 8;
        }
        if ($header['hasmask']) {
            if (strlen($message) < $offset + 4) {
                return null;
            }
            $header['mask'] = substr($message, $offset, 4);
            $offset += 4;
        }
        $header['header_size'] = $offset;
        return $header;
    }

    protected function extractPayload($message, $headers)
    {
        $offset = 2;
        if ($headers['length'] == 126) {
            $offset += 2;
        } elseif ($headers['length'] == 127) {
            $offset += 8;
        }
        if ($headers['hasmask']) {
            $offset += 4;
        }
        return substr($message, $offset);
    }

    protected function applyMask($headers, $payload)
    {
        if (!$headers['hasmask']) {
            return $payload;
        }
        $mask = $headers['mask'];
        $maskBytes = [
            ord($mask[0]),
            ord($mask[1]),
            ord($mask[2]),
            ord($mask[3]),
        ];
        $len = strlen($payload);
        $result = $payload;
        for ($i = 0; $i < $len; $i++) {
            $result[$i] = chr(ord($payload[$i]) ^ $maskBytes[$i % 4]);
        }
        return $result;
    }

    protected function checkRSVBits($headers, $user)
    {
        if ($headers['rsv1'] || $headers['rsv2'] || $headers['rsv3']) {
            return true;
        }
        return false;
    }

    protected function strtohex($str)
    {
        $strout = '';
        $len = strlen($str);
        $colonPositions = [7, 15, 23, 31];
        for ($i = 0; $i < $len; $i++) {
            $byte = ord($str[$i]);
            $strout .= ($byte < 16) ? '0' . dechex($byte) : dechex($byte);
            $strout .= ' ';
            if (in_array($i % 32, $colonPositions, true)) {
                $strout .= ': ';
            }
            if ($i % 32 === 31) {
                $strout .= "\n";
            }
        }
        if ($len % 32 !== 0) {
            $strout .= "\n";
        }
        return $strout;
    }

    protected function printHeaders($headers)
    {
        echo "Array(";
        foreach ($headers as $key => $value) {
            if ($key == 'length' || $key == 'opcode') {
                echo "[$key] => $value";
            } else {
                echo "[$key] => " . $this->strtohex($value) . "";
            }
        }
        echo ")";
    }
}

а вот так я с нима работаю - тут все же ок или можно улучшить производительность

class echoServer extends WebSocketServer
{
  protected $my_max_index_auto = 999999;
  protected $my_last_index_auto = 1;
  private $my_ws_auto = [];

  protected $my_max_index_user = 999999;
  protected $my_last_index_user = 1;
  private $my_ws_users = [];

  private $users_debug = [];
  public $db = null;

  private $timeout = 0;

  function __construct($addr, $port, $bufferLength = 4096, $tick = 1000000)
  {
    parent::__construct($addr, $port, $bufferLength, $tick);
    $this->userClass = 'MyUser';
    $this->db = DB::init();
    $this->db->mem_server();
    $this->db->Conn(1);
    ini_set('date.timezone', fun_conf_param($this->db, 'SITE_TIMEZONE'));


    if ($tick >= 1000000) {
      $this->timeout = ($tick / 1000000) + 5;
    } else {
      $this->timeout = 5;
    }

    $this->db->mem_replace('work-websocket', 1, $this->timeout);

    $this->db->mem_replace('ws-contr-0', 1, 0);
    $this->db->mem_replace('ws-client-0', 1, 0);
  }

  protected function closeserver()
  {
    $this->db->Close();
    $this->db = null;
  }

  // отправляем на контроллер
  private function tick_auto()
  {
    $index = $this->db->mem_eget('ws-contr-0');

    if ($index != false) {
      while ($this->my_last_index_auto != $index) {
        echo ("tick_auto " . $index . '|' . $this->my_last_index_auto) . "\r\n";

        // 1 ... 999
        $r = $this->db->mem_get('ws-contr-' . $this->my_last_index_auto);

        echo "** GET ***\r\n";
        print_r($r);
        echo "\r\n** GET CONTROLLER ***\r\n";


        if ($r != false) {

          if (isset($this->my_ws_auto[$r['contr']])) {

            $socket = $this->my_ws_auto[$r['contr']];

            echo "** SEND CONTROLLER ***\r\n";
            print_r($r);
            echo "\r\n** SEND CONTROLLER ***\r\n";

            $this->send($socket, $r['json']);
          } else {
            unset($this->my_ws_auto[$r['contr']]);
          }
        }

        $this->my_last_index_auto++;
        if ($this->my_last_index_auto == $this->my_max_index_auto) {
          $this->my_last_index_auto = 1;
        }
      }
    } else {
      $this->db->mem_replace('ws-contr-0', 1, 0);
      $this->my_last_index_auto = 1;
    }
  }

  private function tick_user()
  {
    // ws-client-1 .... 999 is last index
    $index = $this->db->mem_eget('ws-client-0');


    if ($index != false) {
      // 5 != 15 ...    12 != 3

      while ($this->my_last_index_user != $index) {
        echo ("user " . $index . '|' . $this->my_last_index_user) . "\r\n";

        // 1 ... 9999
        $r = $this->db->mem_get('ws-client-' . $this->my_last_index_user);

        if ($r != false) {
          $json = json_encode($r);

          echo "** tick_user ***\r\n";
          print_r($json);
          echo "\r\n** assess ***\r\n";


          if ($r['p'] == 'debug') {
            foreach ($this->users_debug as $u => $user) {
              $this->send($user, $json);
            }
          } else {
            foreach ($this->my_ws_users as $u => $chn) {
              $this->send($chn, $json);
            }
          }
          unset($r);
        }

        $this->my_last_index_user++;
        if ($this->my_last_index_user == $this->my_max_index_user) {
          $this->my_last_index_user = 1;
        }
      }
    } else {
      $this->db->mem_replace('ws-client-0', 1, 0);
      $this->my_last_index_user = 1;
    }
  }
  protected function tick()
  {
    $this->tick_auto();
    $this->tick_user();
    $this->db->mem_replace('work-websocket', 1, $this->timeout);
  }

  // прислал контроллер
  // обработка через event/fun
  private function _find_contr($json)
  {
    return fun_event_websocket($this->db, $json['e'], $json['c']);
  }

  protected function process($user, $m)
  {
    $j = json_decode($m, true);

    echo "resv  \r\n";
    print_r($j);
    echo "\r\n";

    // find autonumber - шлет РАСПОЗНОВАЛКА В СОКЕТ
    if (isset($j['n'])) {

      auto_find_socket($this->db, $j);
    } else if (isset($j['p'])) {

      // Авторизация контроллера
      if ($j['p'] == 'ok' && isset($j['c']) && strlen($j['c']) == 16) {

        $controller_id = $j['c'];

        $contr = fun_event_status($this->db, $controller_id, 'controller_uid');
        if ($contr) {

          $user->controller_uid = $j['c'];

          if (isset($this->my_ws_auto[$j['c']])) {

            $old_user = $this->my_ws_auto[$j['c']];

            $this->disconnect($old_user->socket);
          }

          $this->my_ws_auto[$j['c']] = $user;

          $this->send($user, json_encode(['r' => 1]));

          // update time status
          event_time($this->db, $j, $contr['id']);
        }
      }

      if ($j['p'] == 'debug') {
        if (!isset($this->users_debug[$user->id])) {
          $this->users_debug[$user->id] = $user;
        }
      }
      if ($j['p'] == 'u') {
        //    print_r($user->header['host']);

        if (!isset($this->my_ws_users[$user->id])) {
          $this->my_ws_users[$user->id] = $user;

          /*
          * [
          *  'rele' => 0,
          *  'card' => 0,
          *  'ping' => 0,
          *  'auto' => 0,
          *  'pin' => 0,
          *  'contrs' => 0,
          *
          *  'modem' => 0,
          *
          *  'code' => 0,
          *
          *  'open' => 0,
          *  'close' => 0,
          *  'error' => 0,
          *  'network' => 0,
          *  'dispenser' => 0
          *
          * ];
          */
        }
      }
    } else  if (isset($j['c'])) {
      // контроллер шлет
      $data = $this->_find_contr($j);
      if ($data) {
        $this->send($user, json_encode(['r' => 1]));
      }
    } else {
      echo "ERRROR REQUEST\r\n";
      print_r($m);
    }
  }

  protected function connected($user) {}

  protected function closed($user)
  {
    if (isset($this->my_ws_auto[$user->controller_uid])) {
      unset($this->my_ws_auto[$user->controller_uid]);
    }
    if (isset($this->my_ws_users[$user->id])) {
      unset($this->my_ws_users[$user->id]);
    }
    if (isset($this->users_debug[$user->id])) {
      unset($this->users_debug[$user->id]);
    }
  }
}