Last active
February 15, 2019 03:24
-
-
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
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 | |
// 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