Un serveur d'objets en PHP 5 - 1ère partie
Par Charlie, lundi 21 avril 2008 à 11:54 :: PHP :: #49 :: rss
Code source
Le code source de cet article est disponible sur Google Code à l'adresse : http://code.google.com/p/charlie-tutoriels/
Imaginez les cas suivants (liste non limitative) :
- Nous voulons soulager la charge de notre serveur Web en déléguant un traitement complexe à un autre serveur.
- Notre serveur Web ne dispose pas d'une application spécialisée et propriétaire, mais présente sur un autre serveur.
- Nous avons plusieurs serveurs Web désirant mettre en commun des données et des traitements.
- Nous voulons séparer la gestion de contenu et la gestion des données sur deux serveurs séparés.
En résumé nous voulons exploiter des ressources qui ne se trouvent pas toutes sur le serveur Web. C'est ce que l'on appelle une architecture distribuée.
Le schéma suivant montre un exemple d'interactions entre des serveurs

L'interaction utilisateur-serveur Web, nous la connaissons bien : Ce sont les requêtes HTTP que vous exécutez au travers de votre navigateur Web. Quant à l'interaction de serveur à serveur, vous l'utilisez très couramment lorsque vous interrogez votre base de données.
En effet, même si la plupart du temps, c'est une communication de votre serveur avec lui-même, cela reste une communication de machine à machine. Lorsque vous établissez une connexion avec MySQL par exemple, vous devez indiquer l'adresse (le hostname) de votre serveur de données, qui la plupart du temps est "localhost", c'est-à-dire vous-même. Mais il est parfaitement envisageable d'entrer en contact avec un serveur de données placé sur une autre machine, pour peut que celle-ci soit configurée pour accepter des connexions extérieures.
Le code si dessous est un exemple de script PHP fonctionnant dans une architecture distribuée, où le serveur de données est un moteur MySQL placé sur un autre serveur que le serveur Web : "sql.serveur-donnees.com".
<?php
$pdo = new PDO( 'mysql:host=sql.serveur-donnees.com;dbname=test', 'utilisateur',
'mot_de_passe' );
echo '<ul>';
foreach( $pdo->query('SELECT nom, prenom FROM membres ORDER BY nom, prenom ') as $row )
{
echo '<li>', $row['nom'], ' ', $row['prenom'], '</li>';
}
echo '</ul>';
?>
Remarque : Ce code utilise PDO pour interroger MySQL, car il n'est pas recommandé d'utiliser les fonctions "mysql_xxxx" pour ce genre de manipulation.
Maintenant, supposons que notre serveur Web "www.serveur-web.com" n'ait pas les droits pour accéder à notre serveur MySQL "sql.serveur-donnees.com", mais qu'un autre serveur "www.autre-serveur.com" dispose de cette possibilité. Il serait intéressant de pouvoir interroger ce serveur pour qu'il puisse faire le travail à notre place et retourner la liste des membres.
Pour mettre en place ce dialogue entre les serveurs, nous allons donc utiliser une possibilité offerte par PHP : l'utilisation d'une URL comme nom de fichier.
Effectivement, lorsque l'option "allow_url_fopen" est activée dans le php.ini, il est possible de lire la réponse d'une requête HTTP comme si nous accédions à un fichier sur le disque dur.
<?php
$membres = file_get_contents('http://www.autre-serveur.com/liste-membres.php');
echo $membres;
?>
Le script "liste-membres.php" contenant le même code d'accès aux données, que nous avons présenté précédemment.
Remarque : PHP ne se limite pas aux requêtes HTTP, mais peut effectuer des requêtes HTTPS, FTP, etc. Jetez un oeil dans la documentation sur la liste des protocoles supportés.
Cette technique fonctionne bien, mais elle présente deux inconvénients majeurs :
- L'accès à la liste n'est pas sécurisé. Si "
www.autre-serveur.com" est connecté à Internet, tout le monde peut accéder à ces informations. - Le deuxième serveur doit gérer la mise en forme, ce qui n'est normalement pas son travail.
Pour résoudre le problème de sécurité, nous pouvons passer en paramètre, une clef d'authentification qui serait une suite aléatoire de chiffres et de lettres, d'une longueur assez importante pour ne pas être devinable :
<?php
// On définit la clef d'authentification (peut être beaucoup plus complexe, plus longue
// avec minuscules, majuscules, caractères spéciaux, etc.)
define('REQUEST_AUTHENTICATE', '657c080af2561edfea164e9f0c6af7bde673a3a310d78');
$url = 'http://www.autre-serveur.com/liste-membres.php';
// On ajoute la clef d'authentification dans les paramètres de l'URL
$url .= '?auth=' . REQUEST_AUTHENTICATE;
$membres = file_get_contents($url);
echo $membres;
?>
Dans le code de "liste-membres.php", on teste alors la clef d'authentification avant de renvoyer le résultat :
<?php
// On définit la clef d'authentification qui doit être identique à celle
// présente sur le serveur Web.
define('REQUEST_AUTHENTICATE', '657c080af2561edfea164e9f0c6af7bde673a3a310d78');
if( empty( $_GET['auth']) or $_GET['auth'] != REQUEST_AUTHENTICATE ) {
// Pas de clef d'authentification ou clef incorrecte.
echo '</p>Vous n\'êtes pas autorisés à effectuer cette opération !</p>'
exit;
}
$pdo = new PDO( 'mysql:host=sql.serveur-donnees.com;dbname=test', 'utilisateur',
'mot_de_passe' );
echo '<ul>';
foreach( $pdo->query('SELECT nom, prenom from membres') as $row ) {
echo '<li>', $row['nom'], ' ', $row['prenom'], '</li>';
}
echo '</ul>';
?>
Seulement, cette nouvelle version ne règle pas le problème de présentation. De plus, le serveur Web n'est pas en mesure de faire la différence entre une erreur sur le serveur distant et une réponse correcte.
Pour résoudre ces deux difficultés à la fois, nous pouvons construire notre réponse sous la forme d'un tableau PHP linéarisé, qui sera délinéarisé sur le serveur Web. Ce tableau sera constitué de deux index "return" et "exception", ce qui donne :
<?php
// On défini la clef d'authentification
define('REQUEST_AUTHENTICATE', '657c080af2561edfea164e9f0c6af7bde673a3a310d78');
$url = 'http://www.autre-serveur.com/liste-membres.php';
// On ajoute la clef d'authentification dans les paramètres de l'URL
$url .= '?auth=' . REQUEST_AUTHENTICATE;
// On récupère le résultat en masquant les erreurs de connexion
$content = @file_get_contents($url);
if( false === $content ) {
// Si le contenu retourné contient "false" cela indique un problème de
// connexion au serveur
$result = array( 'exception' => 'Problème d\'accès au serveur distant');
} else {
// On délinéarise le contenu retourné
$result = unserialize($content);
if( false === $result ) {
// Problème de délinéarisation du résultat (ou résultat contenant
// "false"). Cela arrive quand le script distant affiche une erreur.
// On ajoute le contenu retourné pour identifier les erreurs sur le
// script distant.
$result = array( 'exception' => 'Problème de délinéarisation du ' .
'résultat.<br />' . $content);
}elseif( ! is_array( $result) or
(empty($result['exception']) and empty($result['return']) ) ) {
// Si le contenu n'est pas un tableau, ou qu'il n'y a ni exception ni
// résultat, cela indique que le résultat linéarisé n'est pas conforme
// aux attentes (tableau contenant "exception" et "return")
$result = array( 'exception' => 'Résultat retourné non conforme');
}
}
if( ! empty( $result['exception'])) {
// Il y a une exception, alors on la lève
throw new Exception( $result['exception'] );
} else {
if( ! is_array( $result['return']) ) {
// Le résultat n'est pas un tableau
throw new Exception( 'Le résultat n\'est pas un tableau' );
} else {
// Le tableau est vide, il n'y a donc pas de membre inscrit
if( empty($result['return'])) echo '<p>Aucun membre inscrit</p>';
else {
// On construit la liste des membres.
// Il serait possible de tester si le tableau contient bien des
// Non + Prénom, mais ça commence à faire beaucoup de code :-)
echo '<ul>';
foreach( $result['return'] as $membre ) {
printf('<li>%s %s</li>',
// Ne pas oublier de protéger les caractères spéciaux
htmlspecialchars($membre['nom']),
htmlspecialchars($membre['prenom'])
);
}
echo '</ul>';
}
}
}
?>
Cette fois-ci, le code est beaucoup plus complet et permet de traiter les erreurs retournées par le serveur. Nous vous laissons le soin de lire les commentaires pour comprendre son fonctionnement.
Et voici le code du script "liste-membres.php" sur le serveur distant :
<?php
// On définit la clef d'authentification qui doit être identique à celle
// présente sur le serveur Web.
define('REQUEST_AUTHENTICATE', '657c080af2561edfea164e9f0c6af7bde673a3a310d78');
// on définit le tableau qui sera linéarisé à la fin du script
$result = array();
if( empty( $_GET['auth']) or $_GET['auth'] != REQUEST_AUTHENTICATE) {
// Pas de clef d'authentification ou clef incorrecte.
$result['exception'] = 'L\'authentification a échoué';
} else {
// On attrape les exceptions
try {
// On se connecte au serveur MySQL et on s'authentifie
$pdo = new PDO( 'mysql:host=sql.serveur-donnees.com;dbname=test',
'utilisateur', 'mot_de_passe' );
// Indique à PDO de lever une exception en cas d'erreur
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// On définit la requête SQL en classant par nom, puis prénom.
$sql = 'SELECT nom, prenom FROM membres ORDER BY nom, prenom';
// On exécute la requête
$st = $pdo->query($sql);
// On place tous les enregistrements sous forme de tableau dans la
// variable de résultat
$result['return'] = $st->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
// Si on attrape une exception PDO, on enregistre ça dans le résultat
$result['exception'] = $e->getMessage();
}
}
// On linéarise le tableau de résultat puis on l'affiche.
// Cet affichage sera repris par le serveur Web qui va le délinéariser
echo serialize( $result );
?>
Comme vous pouvez le voir, le script a été enrichi pour traiter les erreurs de PDO.
L'utilisation de la linéarisation PHP n'est qu'une solution parmi tant d'autres. On aurait très bien pu générer du XML et sur le serveur Web, nous aurions fait appel à SimpleXML pour traiter et interpréter le résultat. Ce type de solution a le mérite d'être utilisable par d'autres technologies. En effet, si le serveur distant s'appuie sur une technologie comme Java (J2EE) par exemple, il est plus simple pour lui de générer du XML qu'une donnée linéarisée dans un format compris de PHP uniquement.
De même, on pourrait remplacer le format des données échangées par un format standard comme SOAP, ce qui permettrait une portabilité des échanges dans une architecture hétérogène (présence de différente technologies), mais ce n'est pas l'objectif de cette suite d'articles, qui permettent de démontrer des concepts et se veulent le plus simple possible, même si les derniers scripts commencent à se complexifier.
Dans la deuxième partie de cet article, nous allons voir les points suivants :
- Création de fonctions réutilisables pour le dialogue entre les serveurs
- Passage de paramètres au script distant sous la forme d'une requête POST
- Création d'un système de répartition de charge
Remarque :Si vous désirez effectuer des tests sur ce genre d'architecture mais que vous ne disposez pas de plusieurs machines en réseau, vous pouvez bien évidemment utiliser l'adresse "localhost", mais vous pouvez également créer plusieurs "hostname" pointant tous sur votre machine locale.
Sur Windows, allez dans le répertoire C:\windows\system32\drivers\etc et éditez le fichier "hosts". Par exemple:
127.0.0.1 localhost 127.0.0.1 www.site-web.com 127.0.0.1 serveur-donnees.com 127.0.0.1 www.autre-serveur.com 127.0.0.1 serveur-donnees-1.com 127.0.0.1 serveur-donnees-2.com 127.0.0.1 www.site-1.com 127.0.0.1 www.site-2.com 127.0.0.1 sql.serveur-donnees.com
Sur un environnement Unix, éditez de la même manière le fichier /etc/hosts.


Commentaires
1. Le vendredi 25 avril 2008 à 16:16, par Jérôme Charron
2. Le dimanche 27 avril 2008 à 14:16, par Kiruban
Ajouter un commentaire