"Server send Events" (SSE) mit PHP & Javascript

Für einen performanteren Chat, war es nötig Server Send Events mit PHP und Javascript einzusetzen. Der Server sollte entlastet werden, dass nicht jede Sekunde ein AJAX Aufruf gestartet wird.

Der Vorteil von SSE ist, dass für eine Bestimmte Länge an Zeit eine Verbindung gehalten wird. In dieser Zeit, kann das PHP Script immer wieder Daten an das User Frontend schicken.

Im Kern ist es server-seitig eine PHP-Script File die in einer Dauerschleife ausgeführt wird und regelmäßig Daten zurück sendet. Im Frontend wird mit Javascript eine Event-Verbindung aufgebaut, die immer auf Daten wartet. Das praktische ist, dass nach dem beenden der Verbindung direkt eine Neue wieder aufgebaut wird.

Beispiel Code aus der MDN Dokumentation, der beim ersten mal so nicht geklappt hatte, aber gute weitere Informationen enthält.

SSE auf dem Server mit PHP

Das folgende Script wird 5 Minuten ausgeführt und gibt alle 2 Sekunden Daten einer Chat-Log Datei zurück. Die Chat-Log Datei wird per GET-Parameter erfragt. Außerdem wird eine eigene Log File optional geschrieben um den Prozess zu überwachen.

Hinweise:

  • Die Ausgabe-Daten müssen ein String sein
  • An die Ausgabe werden noch mal 400.000 Leerzeichen ran gehangen, damit die Rückgabe groß genug ist und nicht vom Server selbst gecached wird
  • Es wird mit mehreren Abfragen kontrolliert, ob der Nutzer noch verbunden ist
  • Der Code ist komplexer als die meisten Beispiele, aber so funktioniert er wenigstens
  • Der Beispiel Code kann sauberer sein, das war nur das erste was funktioniert hat
<?php
 
session_start();
session_write_close();
 
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
 
/*
	Aufruf URL: /chat-sse.php?chatlog_id=0
 */
 
 
 
// === CONFIG ===
$max_exection_time_per_session = 300; // Maximale Laufzeit, 300sek = 5min
$timeout_between_data_sends    = 2;   // Delay Timer, 1sek Pause
 
$write_logfile      = true;           // Für Debugging eine File schreiben
$logfilename        = "chat-sse.log"; // File Name für Debug ausgabe
 
$chatlog_id         = ( isset ( $_GET['chatlog_id'] ) ) ? $_GET['chatlog_id'] : "0";
$chatlog_to_read    = "$chatlog_id.json";
 
$min_string_padding = 400000;
$max_string_padding = 401000;
 
$fallback_data      = [  // Wird zurück gegeben, wenn die File noch nicht existiert
	'chatroom' => [
		'active_users' => [],
		'latest_messages' => [],
		'messages_having_attributes' => [],
	],
];
$fallback_repsonse  = json_encode( $fallback_data );
 
 
 
 
ignore_user_abort( true ); // ironischerweise hilft das besser raus zu finden ob User abbrechen oder nicht
ini_set( 'max_execution_time', $max_exection_time_per_session );
$php_execution_time    = ini_get( 'max_execution_time' );
$app_start_timestamp   = time();
$current_readable_time = date( 'Y-m-d H:i:s');
 
 
 
// Check ob es ein neuer Stream ist oder nicht - magisch irgendwie
$lastEventId = floatval(isset($_SERVER["HTTP_LAST_EVENT_ID"]) ? $_SERVER["HTTP_LAST_EVENT_ID"] : 0);
if ($lastEventId == 0) {
   $lastEventId = floatval(isset($_GET["lastEventId"]) ? $_GET["lastEventId"] : 0);
}
 
 
if( $write_logfile ){
	file_put_contents( $logfilename, "$current_readable_time $run_time :: User Connected" . PHP_EOL, FILE_APPEND );
}
 
 
 
$run_loop = true;
while ( $run_loop ) {
 
	// === AUSGABE ===
	// Wenn alles normal läuft, die statische Chatfile ausgeben
	$does_chatlog_exist   = file_exists( $chatlog_to_read );
	$chatlog_filecontent  = ( $does_chatlog_exist ) ? file_get_contents( $chatlog_to_read ) : $fallback_repsonse;
	$random_pad_generator = rand( $min_string_padding, $max_string_padding );
	$server_cache_buster  = str_pad( $message, $random_pad_generator );
	echo "data: $chatlog_filecontent" . $server_cache_buster . PHP_EOL . PHP_EOL;
 
	// Weiterer Cache Flush für Server
	ob_flush();
	flush();
 
 
 
	// === Kontrolle ob Script gestoppt werden kann ===
	//auf Wunsch werden die Gründe für Script abbrüche geloggt
	$current_timestamp = time();
	$run_time          = $current_timestamp - $app_start_timestamp;
 
	if(connection_aborted()){
		if( $write_logfile ){
			file_put_contents( $logfilename, "$current_readable_time $run_time :: ABORTED: Connection aborted." . PHP_EOL, FILE_APPEND );
		}
        break;
    }
 
	if(connection_status() != CONNECTION_NORMAL){
		if( $write_logfile ){
			file_put_contents( $logfilename, "$current_readable_time $run_time :: ABORTED: Connection Status not normal." . PHP_EOL, FILE_APPEND );
		}
        break;
    }
 
	if( ($php_execution_time + 5) < $run_time ){
		if( $write_logfile ){
			file_put_contents( $logfilename, "$current_readable_time $run_time :: ABORTED: Execution Time exceeded." . PHP_EOL, FILE_APPEND );
		}
        break;
	}
 
	// Sekunde warten bevor es wieder ausgeführt wird und Daten sendet
	sleep( $timeout_between_data_sends );
}
 
if( $write_logfile ){
	file_put_contents( $logfilename, "$current_readable_time $run_time :: RUN ENDED: While Loop was finished." . PHP_EOL, FILE_APPEND );
}
 
exit();

SSE auf im Frontend mit Javascript

Auf der Frontend Seite ist es leichter. IE-Kompatibilität ist nicht gegeben. Mehr Events und die Doku findet man im MDN dazu.

	const sse_url        = '/chat-sse.php?chatlog_id=0';
	const sse_connection = new EventSource( sse_url );
 
	sse_connection.onmessage = function( response ) {
		const response_json = JSON.parse( response.data );
		console.log( response_json );
 
	};
		sse_connection.onerror = function(err) {
		console.error("EventSource failed:", err);
	};

Page Tools