Mit CodeIgniter zum eigenen Online-Chat

Jeder kennt ihn oder hat ihn zumindest schon mal gesehen – die Rede ist vom Online-Chat, wie er auf vielen Websites, wie beispielsweise Facebook oder Google Plus, vorkommt.

Doch wie die Technik dahinter aussieht, und wie so ein Chat prinzipiell funktioniert möchte ich euch im Folgenden erklären.

Im Anschluss daran gibt’s auch ein Tutorial, wie ihr euch euren eigenen Chat mit Hilfe des CodeIgniter-Frameworks bauen könnt.

 

Das Prinzip dahinter

Grundsätzlich unterscheidet man beim Online-Chat zwischen drei Formen:

Beim IRC kommt die klassische Form der Client-Server-Architektur ins Spiel. Hierbei werden spezielle Chat-Server benötigt, welche untereinander vernetzt sind. Mit einer geeigneten Client-Software, welche sich entweder direkt auf den PCs der jeweiligen Chatteilnehmer befindet bzw. über deren Browser ausgeführt wird, können untereinander Nachrichten ausgetauscht werden.

Bei einfachen Webchat, wie er beispielsweise von Live-Support-Systemen verwendet wird, ist keine zusätzliche Software notwendig. Meistens ist die Kommunikation auch auf die jeweilige Website beschränkt.

Ganz anders sieht es allerdings beim Instant Messaging aus. Hier findet die Kommunikation in der Regel über keinen öffentlichen Chatraum statt, sondern ausschließlich zwischen den jeweiligen Chatteilnehmern, welche sich über eine entsprechende Software identifizieren.

IRC und Instant Messaging bieten meistens noch weitere Funktionen, wie beispielsweise Gesprächsprotokolle oder die Übertragung von Dateien.

 

Der eigene Chat mit CodeIgniter

 

Grundstruktur vorbereiten

Um zu beginnen, laden wir uns zuerst die aktuellste Version des CodeIgniter-Frameworks von dessen Website (http://www.codeigniter.com) herunter. Dies ist in diesem Fall Version 3.0.2.

Nun kopieren wir alle Dateien am besten in einen Unterordner „ci-chat“ auf den Webserver (z.B. XAMPP, LAMP, etc.) und rufen dessen Rootverzeichnis im Browser auf, z.B. http://localhost/ci-chat/.

Wenn alle Anforderungen des Frameworks erfüllt sind, sollte hier nun die Willkommensseite von CodeIgniter sichtbar sein.

In der Autoloader-Konfiguration unter /application/config/autoload.php fügen wir als nächstes den URL-Helper sowie die Datenbank-Library hinzu, da wir diese Komponenten später brauchen werden.

$autoload['libraries'] = array('database');
$autoload['helper'] = array('url');

In der Routenkonfiguration unter /application/config/routes.php ändern wir als nächstes den Standard-Controller von „welcome“ auf „chat“, da wir später möchten, dass standardmäßig unsere Chat-Applikation aufgerufen wird.

$route['default_controller'] = 'chat';

 

Die eigentliche Applikation

Momentan würde die Routenkonfiguration nur einen 404-Fehler verursachen, da wir bislang noch keinen Chat-Controller definiert haben.

Aus diesem Grund erstellen wir unter /application/controllers einen neuen namens Chat.php und schreiben hier folgendes hinein:

<?php
defined('BASEPATH') OR exit('No direct script access allowed');

class Chat extends CI_Controller {


	public function index()
	{
		$this->load->view('chat');
	}
}

Dieser Controller wirkt nicht nur primitiv, er ist es auch. Seine einzige Aufgabe liegt darin, die entsprechende View zu laden. Für die eigentliche Funktion kommt ein separater API-Controller ins Spiel.

Selbstverständlich könnte man die gesamte Logik auch in den zuvor erstellten Controller unterbringen. Die Aufteilung in mehrere Controller macht vor allem bei größeren Projekten Sinn, da dies die Lesbarkeit und die damit in Verbindung stehende Wartbarkeit wesentlich erhöht.

 

Um die einzelnen Nachrichten zu speichern, um diese später auch den anderen Chatteilnehmern auszuliefern, brauchen wir eine Datenbank sowie ein geeignetes Model.

CREATE TABLE IF NOT EXISTS `messages` (
`id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`message` text NOT NULL,
`nickname` varchar(50) NOT NULL,
`guid` varchar(100) NOT NULL,
`timestamp` int(11) NOT NULL
);

 

Nun erstellen wir unter /application/models ein neues Model namens Chat_model.php und fügen folgenden Inhalt ein:

<?php
defined('BASEPATH') OR exit('No direct script access allowed');

class Chat_model extends CI_Model {  
  
	function add_message($message, $nickname, $guid)
	{
		$data = array(
			'message'	=> (string) $message,
			'nickname'	=> (string) $nickname,
			'guid'		=> (string)	$guid,
			'timestamp'	=> time(),
		);
		  
		$this->db->insert('messages', $data);
	}

	function get_messages($timestamp)
	{
		$this->db->where('timestamp >', $timestamp);
		$this->db->order_by('timestamp', 'DESC');
		$this->db->limit(10); 
		$query = $this->db->get('messages');
		
		return array_reverse($query->result_array());
	}

}

Die Methode add_message() ermöglicht es uns, neue Nachrichten hinzuzufügen und erwartet als Parameter die eigentliche Nachricht und den Nickname des Chatteilnehmers, von welchem die Nachricht stammt. Um Konflikte zu vermeiden, wenn mehrere Chatteilnehmer den gleichen Nickname verwenden sollten, werden wir zusätzlich eine GUID mitspeichern. Dies ist eine eindeutige Kennung, welche später nur von der Applikation selbst ausgewertet werden wird.

Mittels get_messages() bekommen wir alle Nachrichten, welche von einem bestimmten Zeitpunkt ausgehend geschrieben wurden. Diese Methode erwartet hierfür den jeweiligen UNIX-Zeitstempel.

 

Um nun diese beiden Methoden nach außen hin verfügbar zu machen, legen wir unter /application/controllers einen neuen Controller Api.php an und schreiben hier folgendes hinein:

<?php
defined('BASEPATH') OR exit('No direct script access allowed');

class Api extends CI_Controller {

	
	public function __construct()
	{
		parent::__construct();
		
		$this->load->model('Chat_model');
	}
	
		
	public function send_message()
	{
		$message = $this->input->get('message', null);
		$nickname = $this->input->get('nickname', '');
		$guid = $this->input->get('guid', '');
		
		$this->Chat_model->add_message($message, $nickname, $guid);
		
		$this->_setOutput($message);
	}
	
	
	public function get_messages()
	{
		$timestamp = $this->input->get('timestamp', null);
		
		$messages = $this->Chat_model->get_messages($timestamp);
		
		$this->_setOutput($messages);
	}
	
	
	private function _setOutput($data)
	{
		header('Cache-Control: no-cache, must-revalidate');
		header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
		header('Content-type: application/json');
		
		echo json_encode($data);
	}
}

Im Konstruktur wird nun das zuvor erstellte Chat-Model geladen. Des Weiteren wird via _setOutput() ein zentraler Output-Handler bereitgestellt, welchem wir einfach die notwendigen Daten übergeben und diese dann entsprechend aufbereitet werden (in diesem Fall JSON).

 

Die grafische Oberfläche

Unter /application/views erstellen wir eine neue View chat.php (welche wir zuvor im Chat-Controller bereits verlinkt haben).

In dieser bauen wir uns das HTML-Grundgerüst unseres Chats auf. Basierend auf jQuery und Bootstrap.

<?php
defined('BASEPATH') OR exit('No direct script access allowed');
?><!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="utf-8">
	<title>Chat-Example | CodeIgniter</title>
	
	<script type="text/javascript" src="https://code.jquery.com/jquery-2.1.4.min.js"></script>

		
	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" />
	<script type="text/javascript" src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
	
	<!-- http://bootsnipp.com/snippets/4jXW -->
	<link rel="stylesheet" href="<?php echo base_url(); ?>assets/css/chat.css" />
	
	
	<script type="text/javascript">	  
		$( document ).ready ( function () {
			
			$('#nickname').keyup(function() {
				var nickname = $(this).val();
				
				if(nickname == ''){
					$('#msg_block').hide();
				}else{
					$('#msg_block').show();
				}
			});
			
			// initial nickname check
			$('#nickname').trigger('keyup');
		});
		
		
	</script>

</head>
<body>



<div class="container">
    <div class="row">
		<div class="panel panel-primary">
			<div class="panel-heading">
				<span class="glyphicon glyphicon-comment"></span> Chat
			</div>
			<div class="panel-body">
				<ul class="chat" id="received">
					
				</ul>
			</div>
			<div class="panel-footer">
				<div class="clearfix">
					<div class="col-md-3">
						<div class="input-group">
							<span class="input-group-addon">
								Nickname:
							</span>
							<input id="nickname" type="text" class="form-control input-sm" placeholder="Nickname..." />
						</div>
					</div>
					<div class="col-md-9" id="msg_block">
						<div class="input-group">
							<input id="message" type="text" class="form-control input-sm" placeholder="Type your message here..." />
							<span class="input-group-btn">
								<button class="btn btn-warning btn-sm" id="submit">Send</button>
							</span>
						</div>
					</div>
				</div>
			</div>
		</div>
    </div>
</div>



</body>
</html>

Im Javascript-Block befindet sich ein Check, sodass das Eingabefeld für die Chatnachrichten erst dann sichtbar ist, wenn der Benutzer einen Nickname eingegeben hat.

Damit das ganze nun auch grafisch entsprechend gut aussieht, legen wir noch eine chat.css mit folgendem Inhalt an:

.chat
{
    list-style: none;
    margin: 0;
    padding: 0;
}

.chat li
{
    margin-bottom: 10px;
    padding-bottom: 5px;
    border-bottom: 1px dotted #B3A9A9;
}

.chat li.left .chat-body
{
    margin-left: 60px;
}

.chat li.right .chat-body
{
    margin-right: 60px;
}


.chat li .chat-body p
{
    margin: 0;
    color: #777777;
}

.panel .slidedown .glyphicon, .chat .glyphicon
{
    margin-right: 5px;
}

.panel-body
{
    overflow-y: scroll;
    height: 250px;
}

::-webkit-scrollbar-track
{
    -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
    background-color: #F5F5F5;
}

::-webkit-scrollbar
{
    width: 12px;
    background-color: #F5F5F5;
}

::-webkit-scrollbar-thumb
{
    -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
    background-color: #555;
}

 

Im nächsten Schritt definieren wir hier noch zusätzliche Hilfsfunktionen und speichern hier auch die GUID des Chatteilnehmers in Form eines Cookies. Der Einfachheit halber wird diese Kennung nur lokal im Browser generiert. In einer produktiven Umgebung könnte diese beispielweise in einer User-Tabelle direkt in der Datenbank gespeichert werden.

var request_timestamp = 0;

var setCookie = function(key, value) {
	var expires = new Date();
	expires.setTime(expires.getTime() + (5 * 60 * 1000));
	document.cookie = key + '=' + value + ';expires=' + expires.toUTCString();
}

var getCookie = function(key) {
	var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
	return keyValue ? keyValue[2] : null;
}

var guid = function() {
	function s4() {
		return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
	}
	return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
}

if(getCookie('user_guid') == null || typeof(getCookie('user_guid')) == 'undefined'){
	var user_guid = guid();
	setCookie('user_guid', user_guid);
}


// https://gist.github.com/kmaida/6045266
var parseTimestamp = function(timestamp) {
	var d = new Date( timestamp * 1000 ), // milliseconds
		yyyy = d.getFullYear(),
		mm = ('0' + (d.getMonth() + 1)).slice(-2),	// Months are zero based. Add leading 0.
		dd = ('0' + d.getDate()).slice(-2),			// Add leading 0.
		hh = d.getHours(),
		h = hh,
		min = ('0' + d.getMinutes()).slice(-2),		// Add leading 0.
		ampm = 'AM',
		timeString;
			
	if (hh > 12) {
		h = hh - 12;
		ampm = 'PM';
	} else if (hh === 12) {
		h = 12;
		ampm = 'PM';
	} else if (hh == 0) {
		h = 12;
	}

	timeString = yyyy + '-' + mm + '-' + dd + ', ' + h + ':' + min + ' ' + ampm;
		
	return timeString;
}

 

Nun definieren wir die eigentlichen Funktionen in Form von Closures für das Hinzufügen von Nachrichten und dem Auslesen dieser.

var sendChat = function (message, callback) {
	$.getJSON('<?php echo base_url(); ?>api/send_message?message=' + message + '&nickname=' + $('#nickname').val() + '&guid=' + getCookie('user_guid'), function (data){
		callback();
	});
}

var append_chat_data = function (chat_data) {
	chat_data.forEach(function (data) {
		var is_me = data.guid == getCookie('user_guid');
		
		if(is_me){
			var html = '<li class="right clearfix">';
			html += '	<span class="chat-img pull-right">';
			html += '		<img src="http://placehold.it/50/FA6F57/fff&text=' + data.nickname.slice(0,2) + '" alt="User Avatar" class="img-circle" />';
			html += '	</span>';
			html += '	<div class="chat-body clearfix">';
			html += '		<div class="header">';
			html += '			<small class="text-muted"><span class="glyphicon glyphicon-time"></span>' + parseTimestamp(data.timestamp) + '</small>';
			html += '			<strong class="pull-right primary-font">' + data.nickname + '</strong>';
			html += '		</div>';
			html += '		<p>' + data.message + '</p>';
			html += '	</div>';
			html += '</li>';
		}else{
		  
			var html = '<li class="left clearfix">';
			html += '	<span class="chat-img pull-left">';
			html += '		<img src="http://placehold.it/50/55C1E7/fff&text=' + data.nickname.slice(0,2) + '" alt="User Avatar" class="img-circle" />';
			html += '	</span>';
			html += '	<div class="chat-body clearfix">';
			html += '		<div class="header">';
			html += '			<strong class="primary-font">' + data.nickname + '</strong>';
			html += '			<small class="pull-right text-muted"><span class="glyphicon glyphicon-time"></span>' + parseTimestamp(data.timestamp) + '</small>';
			
			html += '		</div>';
			html += '		<p>' + data.message + '</p>';
			html += '	</div>';
			html += '</li>';
		}
		$("#received").html( $("#received").html() + html);
	});
  
	$('#received').animate({ scrollTop: $('#received').height()}, 1000);
}

var update_chats = function () {
	if(typeof(request_timestamp) == 'undefined' || request_timestamp == 0){
		var offset = 60*15; // 15min
		request_timestamp = parseInt( Date.now() / 1000 - offset );
	}
	$.getJSON('<?php echo base_url(); ?>api/get_messages?timestamp=' + request_timestamp, function (data){
		append_chat_data(data);	

		var newIndex = data.length-1;
		if(typeof(data[newIndex]) != 'undefined'){
			request_timestamp = data[newIndex].timestamp;
		}
	});      
}

Der Einfachheit halber wird in append_chat_data() das HTML-Markup direkt zusammengebaut. Die Daten selbst sollten am besten entweder an eigene Render-Funktionen übergeben werden, oder gleich eine richtige Javascript-Template-Engine verwendet werden.

Dies würde jedoch den Rahmen für dieses Beispiel hier bei weitem sprengen, daher diese primitive Lösung.

 

Nun definieren wir das Verhalten der GUI selbst.

$('#submit').click(function (e) {
	e.preventDefault();
	
	var $field = $('#message');
	var data = $field.val();

	$field.addClass('disabled').attr('disabled', 'disabled');
	sendChat(data, function (){
		$field.val('').removeClass('disabled').removeAttr('disabled');
	});
});

$('#message').keyup(function (e) {
	if (e.which == 13) {
		$('#submit').trigger('click');
	}
});

setInterval(function (){
	update_chats();
}, 1500);

Dieser Code ermöglicht es, die eingegebenen Nachrichten mit einem Klick auf die Enter-Taste direkt abzusenden, und sperrt das Feld so lange, bis die Nachricht vom Server verarbeitet wurde. Im Callback von sendChat() wird dieses Feld danach wieder freigegeben.

Um stets alle Nachrichten nahezu in Echtzeit zu erhalten, wird die Funktion update_chats() im Intervall von 1,5 Sekunden aufgerufen. Diese prüft anhand des jeweiligen Zeitstempels, ob es neue Nachrichten gibt oder nicht und fügt diese dann dem Chatfenster hinzu.

 

Fazit

Die in diesem Beispiel gezeigte Form einer Chat-Integration verursacht durch die zahlreichen Abfragen eine relativ hohe Last auf dem Datenbankserver.

Für größere Websites wäre es jedoch ratsam auf eine entsprechende Infrastruktur zu setzen, d.h. ein geeignetes Chat-Protokoll (z.B. XMPP) und danach eine geeignete Integrationsmöglichkeit auszuwählen.

Dieses Beispiel sollte mehr als Lernbeispiel dienen, kann aber auf kleineren weniger stark frequentierten Websites auch produktiv genutzt werden.

 

Hinweis: Dieses Beispiel setzt ein Grundwissen im Umgang mit dem CodeIgniter-Framework voraus (siehe Schnelleinstieg ins CodeIgniter-Framework) und nutzt teilweise quelloffene Codeschnipsel von Bootswatch.com.