Dans cette partie nous allons voir comment utiliser le module
gen_tcp, qui va nous permettre de communiquer en suivant le protocole TCP.
Les bases
Connexion et déconnexion
La première fonction du module que vous devrez utiliser lorsque vous voudrez vous connecter à un serveur est la fonction
connect, qui va établir la communication avec le correspondant souhaité.
Cette fonction s'utilise ainsi :
connect(Host, Port, Options)
Host est une variable qui peut contenir soit une chaîne de caractères, soit un atome, soit une adresse ip. En Erlang les adresses ip sont représentées sous forme de tuples, je vous invite à consulter la
documentation du module inet pour plus d'informations (
erl -man int
).
Port est une variable de type
integer, qui contient le numéro du port sur lequel vous voulez ouvrir la connexion. La variable Options est une liste d'options, qui sont quant à elles sont très nombreuses et diverses. Vous avez par exemple list et binary, qui servent à déterminer si vous voulez recevoir les données sous formes de listes (donc des chaînes de caractères) ou de binaires. Une autre option intéressante est le couple
{packet, PacketType}
, où plusieurs choix s'offrent à vous pour PacketType :
- raw (ou 0) signifie que vous ne souhaitez pas empaqueter les données ;
- 1, 2 ou encore 4, qui spécifient le nombres d'octets par paquet ;
- line, avec cette option le paquet est une ligne terminée par un retour à la ligne (\n) ;
- d'autres options qu'il n'est pas nécessaire de détailler ici, si jamais vous voulez plus d'informations référez-vous à la documentation de la fonction setopts du module inet.
Une fois la tentative de connexion effectuée la fonction va vous renvoyer un couple du type
{ok, Socket}
si la connexion a réussi,
{error, Raison}
sinon.
Quand vous aurez fini de communiquer, il vous faudra fermer la connexion si votre correspondant ne le fait pas pour vous, pour cela vous avez la fonction close/1 à votre disposition. L'argument attendu est la socket retournée par la fonction
connect.
Envoi et réception
Une fois la connexion effectuée, vous pouvez commencer à vous amuser à envoyer des messages et en recevoir.
Pour envoyer un message il faut utiliser la fonction send/2. Elle attend deux arguments, le premier est la socket renvoyée par
connect lors de l'établissement de la connexion. La seconde est un paquet, dont le type est un binaire ou une liste, en fonction des paramètres de connexion que vous avez choisis.
Pour la réception deux méthodes s'offrent à vous, la première est l'utilisation de la fonction recv/2. En premier paramètre elle attend la socket, en deuxième la longueur du paquet à recevoir. Cette longueur n'a d'importance que si vous êtes en mode raw. Si vous mettez cet argument à 0, tous les octets contenus dans le paquet seront réceptionnés, si par contre la valeur est supérieure à 0, seul un nombre limité d'octets sera reçu, celui fixé.
L'autre méthode possible est d'utiliser la structure
receive ... end
que vous avez probablement l'habitude d'utiliser lorsque vous développez des applications concurrentes. Pour distinguer les messages normaux des messages envoyés par le protocole TCP, il vous faudra filtrer vos messages en suivant le motif
{tcp, Socket, Paquet}
. L'autre type de message à repérer est si la connexion a été fermée par votre interlocuteur, vous pouvez vérifier cela grâce au format
{tcp_closed, Socket}
.
Exemple
Vous en savez désormais assez pour créer un petit client qui va envoyer une requête HTTP à une adresse donnée et recevoir la réponse. Voici le code :
Code : Erlang 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | -module(exemple1_client).
-export([get_url/1]).
get_url(Host) ->
case gen_tcp:connect(Host, 80, [list, {packet, line}]) of
{error, Reason} -> io:format("Erreur: ~w~n", [Reason]);
{ok, Socket} ->
gen_tcp:send(Socket, "GET / HTTP/1.0\r\n\r\n"),
wait_for_answer(Socket, [])
end.
wait_for_answer(Socket, Acc) ->
receive
{tcp, Socket, Packet} ->
wait_for_answer(Socket, [Packet|Acc]);
{tcp_closed, Socket} ->
io:put_chars(lists:reverse(Acc));
_ ->
wait_for_answer(Socket, Acc)
end.
|
Voici ce que ça donne quand je teste de mon côté :
Secret (cliquez pour afficher)Code : Console | Erlang (BEAM) emulator version 5.6.3 [source] [async-threads:0] [hipe]
[kernel-poll:false]
Eshell V5.6.3 (abort with ^G)
1> c(exemple1_client).
{ok,exemple1_client}
2> exemple1_client:get_url("www.google.fr").
HTTP/1.0 302 Found
Location: http://www.google.fr/
Cache-Control: private
Content-Type: text/html; charset=UTF-8
Set-Cookie:
PREF=ID=b1ad4e2e00926034:TM=1222866614:LM=1222866614:S=-zYQ1v-uiG6a1_zC;
expires=Fri, 01-Oct-2010 13:10:14 GMT; path=/; domain=.google.com
Date: Wed, 01 Oct 2008 13:10:14 GMT
Server: gws
Content-Length: 218
Connection: Close
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>302 Moved</TITLE></HEAD><BODY>
<H1>302 Moved</H1>
The document has moved
<A HREF="http://www.google.fr/">here</A>.
</BODY></HTML>
ok
3> |
Écoute
Maintenant que vous savez créer un client, intéressons-nous à la création d'un serveur. Contrairement au client, un serveur ne se charge pas d'effectuer la connexion. Tout ce qu'il a à faire c'est d'attendre que quelqu'un essaye de se connecter, et ensuite décider s'il accepte la connexion ou non.
Pour la phase d'écoute vous devrez utiliser la fonction listen/2, qui en premier argument attend le port sur lequel écouter. En deuxième argument elle attend une liste d'options semblable à celle de la fonction
connect que nous avons vu précédemment. Cette fonction renverra soit un couple
{error, Raison}
, soit
{ok, ListenSocket}
.
Une fois que listen aura fini de s'exécuter, vous utiliserez ListenSocket en association avec la fonction accept/1, qui va elle-même vous retourner un couple
{ok, Socket}
, ou bien entendu
{error, Raison}
en cas d'erreur.
Quand le temps vous manque
Il arrive parfois qu'on veuille laisser un temps limité à une action pour s'effectuer, comme par exemple : « t'as 30 secondes pour te connecter, si ça prend plus de temps laisse tomber », ou encore « si tu reçois pas de message dans les deux prochaines minutes ferme la connexion » (oui, parce que je suis persuadé que vous aussi vous parlez à vos programmes :p ).
Pour faire cela on utilise ce qu'on appelle des
timeouts. Parmi toutes les fonctions que l'on a vues précédemment, celles acceptant un timeout sont :
connect,
accept, et
recv. Pour toutes ces fonctions il suffit d'ajouter un argument lors de l'appel, la durée (en millisecondes) que vous voulez laisser à la fonction pour s'exécuter.
Si jamais vous utilisez la structure
receive ... end
, vous allez pouvoir utiliser un autre mot clé, à savoir : « after ». La structure est la suivante :
Code : Erlang1
2
3
4
5
6 | receive
{tcp, Socket, Request} -> foo;
{tcp_closed, Socket} -> bar
after Timeout ->
baz
end
|
Où Timeout est un entier.
Applications
Un serveur basique
Nous allons désormais nous servir de ce que vous venez d'apprendre pour créer un serveur basique. Basique dans le sens où il ne va gérer qu'une seule connexion à la fois.
Pour cet exercice je vais vous demander de créer un serveur qui va servir de
lien entre le client et un serveur IRC. Ça peut vous sembler bizarre, mais l'idée m'est venue quand j'ai voulu me connecter à IRC depuis mon lycée. Il est en effet apparu que tous les ports du réseau sauf quelques ports particuliers étaient bloqués. J'ai donc eu l'idée de créer un programme qui servira de passerelle entre deux ports.
L'idée c'est de créer un serveur qui attend une connexion sur un port quelconque (mettons le port 8484), et qui une fois la connexion effectuée va ouvrir une connexion avec un serveur IRC et va ensuite transférer tous les messages que lui envoie le client vers le serveur et inversement.
Cet exercice est particulièrement intéressant car il va à la fois vous faire créer un serveur, mais aussi un client. Vous mettrez ainsi en application tout ce que nous venons de voir.
Les informations dont vous avez besoin sont l'adresse du serveur IRC, mettons qu'on veuille se connecter à EpiKnet, ce sera : irc.epiknet.org. Le port associé étant le port 6667.
Voici le code :
Secret (cliquez pour afficher)Code : Erlang 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 | -module(serveur).
-export([start/0]).
start() ->
case gen_tcp:listen(8484, [list, {packet, line}]) of
{ok, Listen} ->
{ok, Socket} = gen_tcp:accept(Listen),
gen_tcp:close(Listen),
{ok, Irc} = gen_tcp:connect("irc.epiknet.org", 6667, [list, {packet, line}]),
loop(Socket, Irc);
{error, Reason} ->
io:format("Erreur: ~s~n", [Reason])
end.
loop(Socket, Irc) ->
receive
{tcp, Socket, Request} ->
gen_tcp:send(Irc, Request),
loop(Socket, Irc);
{tcp, Irc, Request} ->
gen_tcp:send(Socket, Request),
loop(Socket, Irc);
{tcp_closed, Irc} ->
io:format("Connexion à l'hôte perdue.~n"),
gen_tcp:close(Socket);
{tcp_closed, Socket} ->
io:format("Fin de la connexion.~n"),
gen_tcp:close(Irc)
end.
|
Un serveur parallèle
En testant le code précédent, vous aurez pu remarquer qu'il fonctionne très bien pour la première connexion, mais si vous souhaitez lancer une deuxième connexion pendant que la première est toujours ouverte, vous allez échouer lamentablement. Pourquoi cela ? Eh bien car vous avez créé un serveur séquentiel, c'est-à-dire qu'il va effectuer les actions les unes après les autres. Pour gérer plusieurs connexions en même temps il vous faut développer un serveur parallèle, ainsi nommé parce qu'il parallélise les actions.
Pour passer d'un serveur séquentiel, comme le précédent, à un serveur parallèle il s'agit de créer un nouveau processus dès que
accept reçoit une nouvelle connexion. Ainsi au lieu de faire :
Code : Erlang1
2
3
4
5
6
7
8 | init() ->
{ok, Listen} = gen_tcp:listen(...),
wait_for_connect(Listen).
wait_for_connect(Listen) ->
{ok, Socket} = gen_tcp:accept(Socket),
loop(Socket),
wait_for_connect(Listen).
|
Il faudra faire :
Code : Erlang1
2
3
4
5
6
7
8 | init() ->
{ok, Listen} = gen_tcp:listen(...),
spawn(fun() -> wait_for_connect(Listen) end).
wait_for_connect(Listen) ->
{ok, Socket} = gen_tcp:accept(Socket),
spawn(fun() -> wait_for_connect(Listen) end),
loop(Socket).
|
Facile non ?
Une fois le serveur parallélisé, il peut potentiellement accepter des milliers de connexions simultanées. Il serait donc bon de tenir un compteur de connexions, pour fixer une limite maximale de connexions.
Je vous laisse y réfléchir de votre côté. :p