Last active
October 5, 2024 09:56
-
-
Save thekid/7f11a62e0a57d18588694f058ebcc38a to your computer and use it in GitHub Desktop.
WebSocket chat based on Redis queues
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"/> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <meta name="msapplication-config" content="none"/> | |
| <meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
| <title>WebSocket test</title> | |
| <link rel="stylesheet" type="text/css" href="//cdn.jsdelivr.net/npm/[email protected]/dist/semantic.min.css"> | |
| </head> | |
| <body class="pushable"> | |
| <div class="pusher"> | |
| <div id="status" class="ui inverted clearing segment">Disconnected</div> | |
| <div class="ui vertical segment"> | |
| <div class="ui form"> | |
| <div class="field"> | |
| <label>Channel</label> | |
| <select id="channel" class="ui search dropdown" onchange="join(this)"> | |
| <option value="announcements">Announcements</option> | |
| <option value="karlsruhe">#Karlsruhe</option> | |
| <option value="agile">#Agile</option> | |
| </select> | |
| </div> | |
| <div class="field"> | |
| <textarea id="entry" cols="76" rows="3" onkeydown="return send(this);" disabled></textarea> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="ui segment"> | |
| <div class="event" id="event-template" style="display: none"> | |
| <div class="content"> | |
| <div class="summary"> | |
| <span class="type">{{type}}</span> | |
| <div class="date">{{date}}</div> | |
| </div> | |
| <div class="extra text" style="white-space: pre">{{content}}</div> | |
| </div> | |
| </div> | |
| <div class="ui large feed" id="response"> | |
| <!-- Nodes will be inserted here --> | |
| </div> | |
| </div> | |
| </div> | |
| <script src="//cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script> | |
| <script src="//cdn.jsdelivr.net/npm/[email protected]/dist/semantic.min.js"></script> | |
| <script type="text/javascript"> | |
| var conn = null; | |
| var channel = null; | |
| function add(type, text) { | |
| var $response = document.getElementById('response'); | |
| var $node = document.getElementById('event-template').cloneNode(true); | |
| $node.querySelector('.content .type').innerText = type; | |
| $node.querySelector('.content .date').innerText = new Date().toString(); | |
| $node.querySelector('.content .text').innerText = text; | |
| $node.style.display = 'block'; | |
| $response.insertBefore($node, $response.childNodes[0] || null); | |
| } | |
| function disconnect() { | |
| var $status = document.getElementById('status'); | |
| if (conn) { | |
| clearInterval(conn.interval); | |
| conn.socket.close(3000); | |
| conn = null; | |
| } | |
| } | |
| function connect() { | |
| var $status = document.getElementById('status'); | |
| var $entry = document.getElementById('entry'); | |
| disconnect(); | |
| $status.classList.remove('red'); | |
| $status.classList.remove('green'); | |
| $status.innerText = 'Connecting...'; | |
| var ws = new WebSocket("ws://localhost:8081/chat"); | |
| ws.onerror = function(error) { | |
| console.log(error); | |
| $status.classList.add('red'); | |
| $status.innerHTML = '<button class="ui tiny right floated button" onclick="connect()">Try again</button>'; | |
| }; | |
| ws.onopen = function() { | |
| add('Connected', 'Start typing, hit Ctrl+Enter to send!'); | |
| $entry.disabled = false; | |
| $entry.focus(); | |
| $status.classList.add('green'); | |
| $status.innerHTML = '<button class="ui tiny right floated button" onclick="disconnect()">Disconnect</button>'; | |
| // Join selected channel | |
| join(document.getElementById('channel')); | |
| }; | |
| ws.onmessage = function(event) { | |
| var message = JSON.parse(event.data); | |
| switch (message.kind) { | |
| case 'ACCEPTED': case 'PONG': break; // Noop | |
| case 'JOINED': add('Channel', 'You are now on ' + message.channel); channel = message.channel; break; | |
| case 'MESSAGE': add('Message', message.value); break; | |
| case 'ERROR': add('Error', message.value); break; | |
| } | |
| }; | |
| ws.onclose = function(event) { | |
| if (event.code > 1000 && event.code < 3000) { | |
| add('Disconnected', 'Timeout reached, it seems (code #' + event.code + ')'); | |
| $status.classList.remove('green'); | |
| $status.classList.add('red'); | |
| } else { | |
| add('Disconnected', 'Connection closed'); | |
| $status.classList.remove('red'); | |
| $status.classList.remove('green'); | |
| } | |
| $status.innerHTML = '<button class="ui tiny right floated button" onclick="connect()">Reconnect</button>'; | |
| $entry.disabled = true; | |
| }; | |
| conn = { | |
| socket : ws, | |
| interval : setInterval(function() { ws.send('{"kind":"PING"}'); }, 9 * 60 * 1000) | |
| }; | |
| } | |
| function send(entry) { | |
| if ((window.event.keyCode == 10 || window.event.keyCode == 13) && window.event.ctrlKey) { | |
| try { | |
| conn.socket.send(JSON.stringify({kind: 'MESSAGE', channel: channel, message: entry.value})); | |
| entry.value = ''; | |
| } catch (error) { | |
| console.log(error); | |
| } | |
| return false; | |
| } | |
| } | |
| function join(select) { | |
| conn.socket.send(JSON.stringify({kind: 'SUBSCRIBE', channel: select.value})); | |
| } | |
| // Establish connection | |
| connect(); | |
| // Dropdown menu initialization | |
| $('#channel').dropdown(); | |
| </script> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| use io\redis\RedisProtocol; | |
| use websocket\Listeners; | |
| class Chat extends Listeners { | |
| /** @return [:var] */ | |
| public function serve($events) { | |
| $dsn= $this->environment->arguments()[0] ?? 'redis://localhost'; | |
| $subscriptions= []; | |
| // Subscribe to Redis queues | |
| $pub= new RedisProtocol($dsn)->connect(); | |
| $sub= new RedisProtocol($dsn)->connect(); | |
| $events->add($sub->socket(), function() use($pub, $sub, &$subscriptions) { | |
| [$type, $channel, $message]= $sub->receive(); | |
| foreach ($subscriptions[$channel] ?? [] as $index => $connection) { | |
| try { | |
| $connection->send(json_encode(['kind' => 'MESSAGE', 'value' => $message])); | |
| } catch ($e) { | |
| unset($subscriptions[$channel][$index]); // Client disconnected | |
| } | |
| } | |
| }); | |
| // Handle websocket messages | |
| return [ | |
| '/chat' => function($connection, $message) use($pub, $sub, &$subscriptions) { | |
| $value= json_decode($message, true); | |
| $return= match ($value['kind']) { | |
| 'PING' => ['kind' => 'PONG'], | |
| 'SEND' => { | |
| $pub->command('PUBLISH', $value['channel'], $value['message']); | |
| return ['kind' => 'ACCEPTED']; | |
| }, | |
| 'JOIN' => { | |
| foreach ($subscriptions as $channel => &$subscribed) { | |
| unset($subscribed[$connection->id()]); | |
| empty($subscribed) && $sub->command('UNSUBSCRIBE', $channel); | |
| } | |
| $channel= $value['channel']; | |
| if (!isset($subscriptions[$channel])) { | |
| $sub->command('SUBSCRIBE', $channel); | |
| $subscriptions[$channel]= []; | |
| } | |
| $subscriptions[$channel][$connection->id()]= $connection; | |
| return ['kind' => 'JOINED', 'channel' => $channel]; | |
| }, | |
| default => ['kind' => 'ERROR', 'value' => 'Unknown '.$value['kind']], | |
| }; | |
| $connection->send(json_encode($return)); | |
| return $return['kind']; | |
| } | |
| ]; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment