Différences entre versions de « Php socket »

De The Linux Craftsman
Aller à la navigation Aller à la recherche
Ligne 222 : Ligne 222 :
 
</source>
 
</source>
  
 +
On peut maintenant relancer le script sans problème !
 
== Traitement multi-client ==
 
== Traitement multi-client ==

Version du 21 mars 2017 à 11:53

Introduction

Dans le contexte des logiciels, on peut le traduire par « connecteur réseau » ou « interface de connexion ».

Apparu dans les systèmes UNIX, un socket est un élément logiciel qui est aujourd’hui répandu dans la plupart des systèmes d’exploitation. Il s’agit d’une interface logicielle avec les services du système d’exploitation, grâce à laquelle un développeur exploitera facilement et de manière uniforme les services d’un protocole réseau.

[...]

Il s’agit d’un modèle permettant la communication inter processus (IPC - Inter Process Communication) afin de permettre à divers processus de communiquer aussi bien sur une même machine qu’à travers un réseau TCP/IP.

[...]

On distingue ainsi deux modes de communication :

  • Le mode connecté [...] utilisant le protocole TCP. [...]
  • Le mode non connecté [...] utilisant le protocole UDP.

[...]

Les sockets se situent entre la couche transport et les couches applicatives du modèle OSI (protocoles UDP ou TCP utilisant IP / ARP).

Wikipedia

Mise en œuvre

Les socket vont nous permettre de faire communiquer deux processus entre eux grâce à un protocole, généralement un processus appelé serveur et l'autre processus appelé client. C'est pourquoi on parle souvent de protocole client / serveur.

Les fonctions

Tout est détaillé sur le site php.net mais il est important de s'attarder sur certaines fonctions très utiles.

Établissement d'une connexion

Pour créer une socket, il y a deux façons, soit on utilise :

La première méthode va permettre de choisir l'adresse sur laquelle on va placer la socket en écoute ainsi que le type de socket. La deuxième méthode permet uniquement de créer des socket AF_INET (IPv4) de type SOCK_STREAM (TCP).

Si l'objectif est de créer un démon qui écoute sur toutes les interfaces en TCP, la deuxième méthode est plus appropriée.
Au contraire, si l'objectif est de faire de l'IPC, une socket UDP ou même brute (OSI 3), il vaut mieux opter pour la première méthode.

Voici un exemple de socket TCP avec la première méthode.
Ce socket écoute sur le port 1234 de l'interface 127.0.0.1 en TCP :

Voici un exemple de socket TCP avec la deuxième méthode.
Ce socket écoute sur le port 1234 de toutes les interfaces en TCP :

<?php
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($socket, "127.0.0.1", 1234);
socket_listen($socket);
while($c = socket_accept($socket)) {
	/* Traiter la requête entrante */
}
socket_close($socket);
?>
<?php
$socket = socket_create_listen(1234);
while($c = socket_accept($socket)) {
	/* Traiter la requête entrante */
}
socket_close($socket);
?>

Que se passe t-il si le port est déjà utilisé, si les droits ne permettent pas de binder la socket, si le type de socket demandée n'existe pas ? Un erreur survient et regardons maintenant comment l'attraper !

Gestion des erreurs

Pour attraper les erreurs, il faut tester le retour des fonctions énoncées précédemment. Si celui-ci est à FALSE une erreur est survenue et on peut récupérer son code et même une explication textuelle.

Voici un exemple avec la première méthode.

Voici un exemple avec la deuxième méthode.

<?php
if (($socket = socket_create ( AF_INET, SOCK_STREAM, SOL_TCP )) == FALSE) {
	echo "socket_create_listen() a échoué : " . socket_strerror ( socket_last_error () ) . "\n";
	exit ( 1 );
}
if(socket_bind ( $socket, "127.0.0.1", 1234 )==FALSE){
	echo "socket_bind() a échoué : " . socket_strerror ( socket_last_error () ) . "\n";
	exit ( 1 );
}
if(socket_listen ( $socket )==FALSE){
	echo "socket_listen() a échoué : " . socket_strerror ( socket_last_error () ) . "\n";
	exit ( 1 );
}
while ( $c = socket_accept ( $socket ) ) {
	/* Traiter la requête entrante */
}
socket_close ( $socket );
?>
<?php
if (($socket = socket_create_listen ( 1234 )) == FALSE) {
	echo "socket_create_listen() a échoué : " . socket_strerror ( socket_last_error () ) . "\n";
	exit ( 1 );
}
while ( $c = socket_accept ( $socket ) ) {
	/* Traiter la requête entrante */
}
socket_close ( $socket );
?>

Cela donnera le résultat suivant :

PHP Warning:  socket_create_listen(): unable to bind to given address [98]: Address already in use in /root/workspace/Sockets/src/socket.php on line 2
socket_create() a échoué : raison : Address already in use

Pour enlever le PHP warning il suffit d'ajouter un '@' devant la fonction qui génère le warning

$socket = socket_create_listen ( 1234 )

devient

$socket = @socket_create_listen ( 1234 )

Traitement des connexions entrantes

On va maintenant s'intéresser à la récupération des connexions entrante. Pour cela, nous allons créer un petit serveur echo.

<?php
if (($socket = socket_create_listen ( 1234 )) == FALSE) {
	echo "socket_create_listen() a échoué : " . socket_strerror ( socket_last_error () ) . "\n";
	exit ( 1 );
}
while ( $c = socket_accept ( $socket ) ) {
	while ( $c !== FALSE ) {
		if ($buf = socket_read ( $c, 2048 )) {
			socket_write ( $c, "You said : ".$buf );
		}
	}
}
socket_close ( $socket );
?>

Ce qui nous donne :

côté serveur :

coté client, en utilisant la commande telnet :

# php -f socket.php

# telnet 127.0.01 1234
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
coucou
You said : coucou

Lorsque l'on termine le processus serveur en envoyant un SIGINT (ctrl + c), le client reçoit le message Connection closed by foreign host. et le serveur laisse la socket en TIME_WAIT. C'est un comportement tout à fait normale car la connexion, à l'initiative du client, et terminée par le serveur et le système attends un certain temps (d’où le nom de TIME_WAIT) pour laisser aux derniers segments TCP le temps d'arriver.

Tout cela nous importe peu, sauf que lors du prochain lancement du script :

# php -f test.php
PHP Warning:  socket_create_listen(): unable to bind to given address [98]: Address already in use in /root/workspace/Sockets/src/socket.php on line 2

Effectivement, lorsque l'on regarde qui occupe le port 1234 :

# netstat -atnp | grep 1234
tcp        0      0 127.0.0.1:1234              127.0.0.1:47408             TIME_WAIT   -  

C'est bien le système qui laisse la socket ouverte un certain temps, même si personne n'écoute (- à la place du nom du processus).

Nous allons maintenant modifier les options de la socket pour éviter cet effet.

Modification des options de socket

Pour régler le problème précédent, deux choix s'offre à nous :

  • SO_REUSEADDR : option qui permet, lorsqu'elle est mise à 1, de binder de nouveau une socket à un port en TIME_WAIT en assumant qu'il n'y ai aucun paquet en transit ;
  • SO_LINGER : option qui, lorsqu'elle est mise à 0, initie une fermeture anormale de la socket. Ce phénomène s'appelle en anglais slamming the connection shut ou en français raccrocher brutalement la connexion.

Nous allons préférer la première option qui est plus propre. Cependant, l'exemple précédent qui utilise socket_create_listen ne permet pas de modifier l'option avant l'étape de bind puisque les trois étapes sont regroupées. Nous allons donc basculer sur la première méthode...

if (($socket = socket_create ( AF_INET, SOCK_STREAM, SOL_TCP )) == FALSE) {
	echo "socket_create_listen() a échoué : " . socket_strerror ( socket_last_error () ) . "\n";
	exit ( 1 );
}
// Modification de l'option SO_REUSEADDR à la valeur 1 !
if (!socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1)) {
	echo 'Impossible de définir l\'option du socket : '. socket_strerror(socket_last_error()) . "\n";
	exit ( 1 );
}
if(socket_bind ( $socket, "127.0.0.1", 1234 )==FALSE){
	echo "socket_bind() a échoué : " . socket_strerror ( socket_last_error () ) . "\n";
	exit ( 1 );
}
if(socket_listen ( $socket )==FALSE){
	echo "socket_listen() a échoué : " . socket_strerror ( socket_last_error () ) . "\n";
	exit ( 1 );
}
while ( $c = socket_accept ( $socket ) ) {
	while ( $c !== FALSE ) {
		if ($buf = socket_read ( $c, 2048 )) {
			socket_write ( $c, "You said : " . $buf );
		}
	}
}
socket_close ( $socket );

On peut maintenant relancer le script sans problème !

Traitement multi-client