Skip to content

Instantly share code, notes, and snippets.

@ariankordi
Last active February 15, 2019 03:24
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ariankordi/b193285edc3af8492579657e27a94a25 to your computer and use it in GitHub Desktop.
Save ariankordi/b193285edc3af8492579657e27a94a25 to your computer and use it in GitHub Desktop.
A REAL-TIME (no AJAX polling), database-less, no-setup chat room in PHP, using SSE and shmop
<?php
// This chat thing uses nothing but shmop, but it requires SSE (Server-Sent Events) to work on your server.
// If this chat just does nothing, i.e. you post a message and literally nothing else happens, SSE probably doesn't work.
// Note that this file continuously reads a shared memory block and detects a difference, in order to get live messages.
// This might not be all that efficient, and can be intensive on your server. Don't leave this up for a long period of time.
// This will also not work with PHP's built-in server, due to it being single-threaded. Oh yeah, since this uses SSE, this will use a separate thread for every client that is connected.
// Have fun. This should work right away with no config.
// set the size of these shared memory blocks, "message buffers" here
// smaller is probably better, but this size is also the byte length that any message can be
// it is 20k by default, so therefore you can post 20k bytes but 20k shared memory buffers are being thrown around all the time
const max_message_size = 20000;
// handle posting
if($_SERVER['REQUEST_METHOD'] === 'POST') {
// send the message, it's contained in the post request so get that
$input = file_get_contents('php://input');
// see if input is large, if it isn't then return a bad request because you can't do that
if(strlen($input) > max_message_size) {
http_response_code(400);
exit('please make your message shorter, maximum size is ' . max_message_size);
}
// open a shared memory block for creation, rw (you can't open for just w)
$shm = shmop_open(69421, 'c', 0644, max_message_size);
// write our message to the shared memory block at offset 0
// add a null byte at the end of our string, because writing to shared memory ONLY overwrites what's currently in it
shmop_write($shm, $input . "\0", 0);
// done, close shared memory
shmop_close($shm);
// exit if there's a post request so that the html below won't be returned
exit();
}
// handle live streaming using sse, only if there's the message_update_stream get parameter
if(isset($_GET['message_update_stream'])) {
// set sse output headers, disabling cache may be important
header('Content-Type: text/event-stream');
// the query string may avoid cache too but let's still add it for good measure
header('Cache-Control: no-cache');
// also close the session right now, because we don't know how long this response will last
session_write_close();
// open another shared memory block for creation or rw (you can't open just for r)
$shm = shmop_open(69421, 'c', 0644, max_message_size);
// initialize $data and $data_new, $data_new will have newer data than $data but $data will eventually become the value of $data_new
$data = '';
$data_new = '';
$is_first_message = true;
// loop reading memory forever until the script shuts down
// keep track of the time of the connection start, so we can time out
//$connect_time = time();
while(true) {
// this actually doesn't really work
/*if((time() - $connect_time) > 30) {
break;
}*/
// read new data into $data_new
$data_new = shmop_read($shm, 0, max_message_size);
// if the data is actually new data...
if($data_new !== $data) {
// update $data with $data_new
$data = $data_new;
// make sure to skip the first message, since it'll be the one that's already there and not a new one
if($is_first_message) {
$is_first_message = false;
continue;
}
// new data, get all data before any null byte
// position of first null byte, or it's false if there is no null byte
$null_pos = strpos($data_new, "\0");
if($null_pos !== false) {
// use substr to get data before null_pos
$data_str = substr($data_new, 0, $null_pos);
}
// now $data_str is $data_new but in a proper string
// output the data in sse format
echo 'data: ' . $data_str . "\n\n";
ob_flush();
flush();
//break;
}
// sleep for 80ms (a long time) so that this while loop isn't cpu-intensive
// change this to a lower number if you want, but 80ms probably works fine
usleep(80000);
}
// close the shmop if the request times out
shmop_close($shm);
// exit if there's the message_update_stream get parameter, so that the html below won't be returned
exit();
}
?><!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
button {
display: block;
font-size: 28px;
margin-bottom: 10px;
margin-top: 10px;
}
textarea {
display: block;
width: 800px;
height: 300px;
}
input {
width: 800px;
margin-top: 10px;
}
#name {
margin-top: -10px;
margin-bottom: 10px;
}
#error {
white-space: pre-wrap;
word-wrap: break-word;
color: red;
}
@media screen and (max-width: 800px) {
button {
font-size: 14px;
margin-bottom: 5px;
}
textarea {
width: 98.5%;
}
input {
width: 98.5%;
}
}
</style>
<title>cedar chat 3 (REAL TIME, PHP ONLY)</title>
</head>
<body>
<div id="error" style="display: none;"></div>
<input id="name" placeholder="enter your name here, it will be prepended to every message you send" type="text">
<textarea placeholder="message log" disabled></textarea>
<form>
<input id="message" placeholder="type a message" type="text">
<button>send</button>
</form>
<script>
// @license magnet:?xt=urn:btih:e95b018ef3580986a04669f1b5879592219e2a7a&dn=public-domain.txt
// variables for all of the elements that are going to be edited soon
var textarea = document.getElementsByTagName('textarea')[0],
nameInput = document.getElementById('name'),
message = document.getElementById('message'),
form = document.getElementsByTagName('form')[0],
button = document.getElementsByTagName('button')[0],
error = document.getElementById('error'),
// an xhr for posting chat messages and one for receiving chat updates
req = new XMLHttpRequest();
// this will be the amount of bytes seen in the xhr update response, so we can use substr
let seen = 0;
// connect to the event source, make it updates
var updates = new EventSource('?message_update_stream');
// when a message comes in...
updates.addEventListener('message', event => {
// update the textarea with the new message, add a line feed though
textarea.textContent += event.data + '\n';
// also scroll the textarea all the way down
textarea.scrollTop = textarea.scrollHeight;
// focus the message input? this may help
message.focus();
});
// send the message via req when form is submitted
form.addEventListener('submit', event => {
event.preventDefault();
// un-disable the button, hide error and success
button.setAttribute('disabled', '');
error.setAttribute('style', 'display: none;');
// ask the user to enter a MESSAGE please
if(message.value === '') {
button.removeAttribute('disabled');
error.textContent = 'enter a message';
error.removeAttribute('style');
return;
}
// open req xhr
req.open('POST', '');
// when the xhr is finished, reset input
req.addEventListener('load', () => {
// un-disable the button
button.removeAttribute('disabled');
if(req.status !== 200) {
// if there's an error, set error text and un-hide it
error.textContent = 'error posting ' + req.responseURL + ': ' + req.status + ' ' + req.statusText + ', ' + req.response;;
error.removeAttribute('style');
} else {
// clear input
message.value = '';
}
});
// all of the event listeners have been assigned, so now send the posting xhr
// with the contents of input as the post data
// if there's a name, send it with the name prepended to it
if(nameInput.value.length) {
req.send('[' + nameInput.value + '] ' + message.value);
} else {
req.send(message.value);
}
});
// @license-end
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment