Sockets Client Serveur TCP Indy - John COLIBRI. |
- résumé : Comment utiliser les composants tIdTcpClient et tIdTcpServer ou tIdCmdTcpServer : architecture, exemple avec un protocole simple, échange de texte, transfert de données binaires et transfert de fichier
- mots clé : TCP/IP - Indy - tIdTcpClient - tIdTcpServer et tIdCmdTcpServer - tIdCommandHandler - programmation sockets
- logiciel utilisé : Windows XP personnel, Delphi 2006 et Delphi 2009
- matériel utilisé : Pentium 2.800 Mhz, 512 Meg de mémoire, 250 Giga disque dur
- champ d'application : Delphi Delphi 2006 à 2010
- niveau : développeur Delphi
- plan :
1 - Les Sockets TCP/IP avec Indy Le jeu de composants Indy nous permet de développer toute la gamme d'applications de communications entre machines en utilisant la pile TCP/IP. Nous pouvons utiliser :
- Icmp pour tester si une machine répond (Ping)
- les sockets Udp (envoi non connecté, sans garantie de réception ni d'absence d'erreur)
- les sockets Tcp, qui fonctionnent en "mode connecté", qui garantit la
transmission, dans l'ordre et sans erreur
Nous nous intéresserons aux sockets TCP. Fondamentalement les sockets ont pour but de communiquer comme avec un fichier: - ouverture
- lecture / écriture
- fermeture
2 - Envoi de fichier par tIdTcpServer et tIdTcpClient 2.1 - Protocole sur mesure simple
Pour que les PC qui communiquent entre eux se comprennent, il faut convenir du contenu et de la signification des données échangées, en bref le protocole. Il existe des dizaines de protocoles normalisés pour lire et écrire des mails
(POP3 et SMTP), lire des pages web (HTTP), transmettre des fichiers (FTP et SFTP) etc. Mais pour nos applications, nous pouvons opter pour un protocole "sur mesure". C'est ce que nous ferons ici.
Pour prendre un exemple concret, pour transférer un fichier, nous pouvons - démarrer le Serveur
- le Client se connecte
- le Serveur retourne un message d'accueil
- le Client envoie le nom d'une fichier
- le Serveur envoie la taille du fichier, suivi du contenu du fichier
- le Client lit ces informations, puis se déconnecte
2.2 - Le Client tIdTcpClient Indy 9 Voyons tout d'abord le Client. Indy nous propose un composant tIdTcpClient, qui a la structure suivante :
et pour les traitements : - nous initialisons Host, l'adresse du serveur. Dans notre cas 127.0.0.1
- nous initialisons Port, qui indique quel type de protocole nous souhaitons
utiliser. Ici, pour notre protocole sur mesure, nous prendrons 5678
- sur un événement OnClick
- nous appelons Connect
- nous lisons le texte de bienvenue par Readln
- nous envoyons le nom du fichier par Writeln
- nous lisons le fichier
- nous déconnnectons par Disconnect
Voici le code |
créez une applications Win32 | | de l'onglet Indy, sélectionnez un tIdAntiFreeze et posez le sur la Form1 |
| de l'onglet Indy, sélectionnez un tIdTcpClient et posez-le sur la Form1 |
| posez un tButton, nommez le "connect_" et tapez le code suivant qui connecte puis lit la String de bienvenue:
Procedure TForm1.connect_Click(Sender: TObject);
Var l_greeting: String; Begin
With IdTcpClient1 Do Begin
Host:= '127.0.0.1'; Port:= 5678;
Connect; l_greeting:= Readln;
display(l_greeting); End; // with IdTcpClient1
End; // connect_Click | | |
posez un tButton, nommez le "get_file_" et envoyez le nom du fichier, lisez la taille du fichier puis ses données:
Procedure TForm1.get_file_Click(Sender: TObject);
Var l_size: Integer;
l_c_file_stream: tFileStream; Begin
With IdTcpClient1 Do Begin
WriteLn('aha.txt'); l_size:= ReadInteger;
l_c_file_stream:= tFileStream.Create('resu.txt', fmCreate);
ReadStream(l_c_file_stream, l_size, False);
l_c_file_stream.Free; End; // with IdTcpClient1
display('< get_file'); End; // get_file_Click |
| | posez un tButton, nommez le "disconnect_" et déconnectez le Client:
Procedure TForm1.disconnect_Click(Sender: TObject);
Begin IdTcpClient1.Disconnect;
End; // disconnect_Click | |
2.3 - Le Serveur tIdTcpServer Indy 9
Le Serveur utilise les Classes suivantes: Il est utilisé de la façon suivante
- le basculement tIdTcpServer.Active à True met le Serveur en mode écoute
- nous créons l'événement tIdTcpServer.OnExecute qui sera appelé chaque fois
que des données seront reçues du Client. C'est dans cet événement que nous effectuons les lectures des requêtes Client et renvoyons éventuellement les réponses.
Pour cela, le paramètre tIdPeerThread a un attribut Connection qui permet d'effectuer les lectures / écritures Nous pouvons aussi déconnecter ce Client en appelant tIdPeerThread.Connection.Disconnect
- éventuellement nous fermons le Serveur eu basculant Active sur False
Voici le code: | créez une applications Win32
| | de l'onglet Indy, sélectionnez un tIdTcpServer et posez-le sur la Form1 |
| posez un tButton, nommez le "start_" et dans son OnClic initialisez le DefaultPort, et basculez Active à True
Procedure TForm1.start_Click(Sender: TObject);
Begin IdTcpServer1.DefaultPort:= 5678;
IdTcpServer1.Active:= True;
End; // start_Click | | |
créez l'événement IdTcpServer.OnExecute, et lisez le nom du fichier et retournez sa taille et son contenu :
Procedure TForm1.IdTCPServer1Execute(AThread: TIdPeerThread);
Var l_request_command: String;
l_c_file_stream: tFileStream; Begin
With AThread.Connection Do
Begin l_request_command := ReadLn;
l_c_file_stream:= tFileStream.Create(l_request_command, fmOpenRead);
WriteInteger(l_c_file_stream.Size);
WriteStream(l_c_file_stream);
l_c_file_stream.Free; // Disconnect;
End; // with AThread.Connection End; // IdTCPServer1Execute
| | | posez un tButton, nommez le "stop_" et arrêtez le Serveur:
Procedure TForm1.stop_Click(Sender: TObject);
Begin IdTcpServer1.Active:= True;
End; // stop_Click | |
2.4 - Exécution Voici le Serveur
et voici le Client
2.5 - Commentaires sur le mode OnExecute Notez que: - cet exemple n'a pas pour but de décrire en détail le fonctionnement de tIdTcpClient et tIdTcpServer.
- nous ne gérons pas
- les erreurs (fichier absent, adresse du serveur incorrecte etc)
- fin du dialogue, fermeture inattendue du PC distant
- l'affichage côté Serveur est incorrect dans notre code. Comme
tItTcpServer.OnExecute se déroule dans le contexte du tIdPeerThread, nous devrions utiliser une quelconque technique de synchronisation. Pour cet exemple manuel, et comme notre Serveur ne gère qu'un seul Client dans
notre démo et qu'il ne fait rien d'autre, c'est tolérable. Mais il est évident que pour la programmation socket Indy, il est impératif de bien comprendre le fonctionnement des tThreads
- pour des projets industriels, nous utilisons aussi un log sous section critique, qui affiche les ThreadId, ainsi que les adresses IP et ports, surtout côté Serveur
- le tIdAntiFreeze a pour but d'éviter le gel du client (lecture bloquante des données)
- nous avons choisi un lecture d'un nombre d'octets fixes, mais une lecture jusqu'à disconnection du Serveur aurait été une option
- nous avons volontairement séparé les actions Client en 3, pour mieux comprendre les différentes étapes
- les diagrammes de classes sont aussi largement simplifié. Nous n'avons pas
représenté les IoHandlers, les Intercept, les Bindings etc. Ces détail sont expliqués durant les 2 jours de nos formations sockets Tcp/IP de 2 jours où le temps nous est moins compté
- le projet que vous pourrez télécharger du .ZIP contient aussi d'autres événement des sockets Indy (OnConnected, OnStatus etc) et de nombreux messages signalant le début et la fin des
procédures
Pour la partie qui nous intéresse dans cet article, soulignons que: - après le lancement du Serveur, le serveur se met à m'écoute des Clients
- lorsqu'un nouveau Client se connecte
- le Serveur crée un tIdPeerThread pour gérer le dialogue avec ce Client. Ce tThread est ajouté à la liste tIdTcpServer.Threads
- le Serveur appelle alors automatiquement OnExecute, qui sera rappelé après chaque réception de données du Client. Et le paramètre tIdPeerThread permet les lectures / écritures
- le Serveur est plus compliqué à présenter, car il doit gérer plusieurs Clients, en utilisant la mécanique Accept que nous avons maintes et maintes fois présentée dans nos articles. Quoiqu'il en soit, ceci explique
pourquoi nous avons présenté le Client en premier. L'écriture du Client en premier permet aussi de mieux suivre le dialogue, car c'est le Client qui a
l'initiative, le Serveur ne faisant que répondre aux requêtes des Clients
3 - Le mode Commande des tIdTcpServer Indy 9 3.1 - Protocole textuel complexe
Notre protocole était des plus simple | envoi d'un nom de fichier | |
retour de la taille et du contenu | De nombreux protocoles standard (mail, news etc) ont un schéma très similaire |
le Client peut envoyer un certain nombre de commandes ayant le format
Par exemple LIST DELETE 123 GET index.html |
| | le Serveur analyse le verbe envoyé en premier, éventuellement les paramètres. Si le verbe et ses paramètres sont corrects
- il envoie un code de réponse (par exemple 200 pour signifier l'accord, 404 pour signaler une page non présente etc), avec éventuellement des paramètres
- puis, en fonction du protocole envoie d'autres informations (une page
Web, un fichier etc)
|
Voici, par exemple, une dialogue imaginaire pour un envoi de courrier (SMTP) | un client se connecte
| | 220 indy.smtp.server.9. ready at Mon, 21 mar 2010 | |
MAIL FROM:felix@felix-colibri.com | | 250 felix@felix-colibri.com... Sender OK |
| RCPT TO lily@dev.com | | 250lily@dev.com... Recipient OK |
| DATA | | 350 Enter mail, end with "." on a line by itself |
| Subject: specification de l'application de facturation // ici le contenu textuel du mail . | |
250 UB12YT Message accepted for delivery | | QUIT | |
221 indy.smtp.server.9 closing connection | Dans notre cas leServeur recevra les commandes MAIL, RCPT, DATA et QUIT.
Il serait tout à fait possible de gérer ce type de protocole en utilisant
tIdTcpServer.OnExecute.
Procedure TForm1.IdTCPServer1Execute(AThread: TIdPeerThread);
Var l_request_command: String; Begin
With AThread.Connection Do
Begin l_request_command := ReadLn;
If COPY(l_request_command, 1, 4)= 'MAIL'
Then Begin
// -- analyze FROM source
// -- send response: 250 or error
End Else
If COPY(l_request_command, 1, 4)= 'RCPT'
Then Begin
// -- analyze TO destination
// -- send response: 250 or error
End Else
... End; // with AThread.Connection
End; // IdTCPServer1Execute | Le code comporterait une cascade de If pour traiter chaque commande. Et le
code de POP3 ou NNTP suivrait exactement le même schéma.
3.2 - Le mode Commande Indy D'ou l'idée de faciliter le traitement de chaque partie d'un protocole. Pour cela:
- pour chaque élément du protocole (MAIL, RCPT, DATA, QUIT), le dévelopeur crée une commande tIdTcpCommand
- lorsque le Serveur reçoit un texte
- il compare le début de ce texte aux commandes prévues (MAIL etc)
- si la commande envoyée correspond à une commande prévue, il crée un objet tIdCommand qui, pour ce Client, va analyser les paramètres et construire la réponse
En résumé:
- le développeur créé autant d'objets tIdCommandHandler que de commandes prévues
- il place son code (analyse des paramètres, construction de la réponse) dans l'événement tIdCommandHandler.OnCommand
Voici le code du dispatcher de tIdTcpServer (la méthode qui est appelée lorsque des données arrivent):
Function TIdTCPServer.DoExecute(AThread: TIdPeerThread): boolean;
Var l_command_handler_count: integer;
l_command_handler_index: Integer;
l_client_line: String; Begin
l_command_handler_count:= CommandHandlers.Count- 1;
If CommandHandlersEnabled And (l_command_handler_count>= 0)
Then Begin
// -- CommandHandler mode
Result:= TRUE;
If AThread.Connection.Connected
Then Begin
l_client_line:= AThread.Connection.ReadLn;
If l_client_line<> ''
Then Begin
DoBeforeCommandHandler(AThread, l_client_line);
Try
l_command_handler_index:= 0;
While l_command_handler_index<= l_command_handler_count Do
Begin
With CommandHandlers.Items[l_command_handler_index] Do
If Enabled And Check(l_client_line, AThread)
Then Break;
inc(l_command_handler_index);
End; //while
If l_command_handler_index> l_command_handler_count
Then DoOnNoCommandHandler(l_client_line, AThread);
Finally
DoAfterCommandHandler(AThread);
End;
End; // if non empty line
End; End
Else Begin // -- OnExecute mode
Result:= Assigned(OnExecute);
If Result
Then OnExecute(AThread);
End; End; // DoExecute |
qui peut se lire ainsi: - après quelques vérification (nous sommes en mode Commande et nous sommes toujours connecté) tIdTcpServer appelle la méthode Check de chaque tIdCommandHandler
- Check vérifie le premier mot, et si un mot correspond, appelle OnCommand
Le code (très simplifié ici) de Check, qui est le noeud du traitement, est le suivant:
Function TIdCommandHandler.Check(Const p_received_string: String;
p_c_id_peer_thread: TIdPeerThread): boolean; Begin
// -- compare la chaîne reçue à la commade
Result:= AnsiSameText(p_received_string, Command);
If Result Then
// -- ici le verbe correspond.
// -- créé un objet tIdCommand
With TIdCommand.Create Do
Begin // -- appelle notre OnCommand
DoCommand; Free;
End; // with TIdCommand
End; // Check |
3.3 - Le premier exemple
Commençons par un Client qui envoie "A" et attend une réponse "aaa" | créez une application Client qui se contente d'envoyer "A" et lit la
réponse "aaa". Nous n'avons besoin que d'un seul bouton qui connecte, envoie et reçoit, et déconnecte est le suivant
Procedure TForm1.send_A_receive_aaaClick(Sender: TObject);
Var l_response: String; Begin
With IdTcpClient1 Do Begin
Port:= 5678; Connect; Try
// -- write to the server WriteLn('A');
l_response:= ReadLn;
display(l_response); Finally
Disconnect; End; // try ... finally
End; // with IdTcpClient1 End; // get_date_Click |
| Et pour le Serveur
3.4 - Les autre possibilités A ce stade, tout est idyllique : à chaque commande correspond un tIdCommand,
et dans l'événement OnCommand, nous lisons et écrivons les données spécifiées par notre protocole. Hélas, là où cela se gâte, c'est que les développeurs Indy ont ajoutés de
nombreuses possibilités pour automatiser la réception et l'envoi de valeurs par défaut. Le syndrome "look ma, no line of code". Si la spécification est que le Serveur accueille toujours la connexion d'un
nouveau Client par "200 Bienvenue", nous pouvons utiliser l'événement tIdTcpServer.OnConnect (appel à chaque fois qu'un nouveau Client se connecte) et écrire :
Procedure TForm1.IdTCPServer1Connect(AThread: TIdPeerThread);
Begin AThread.Connection.Writeln('200 Bienvenue');
End; // IdTCPServer1Connect | On comprend aisément qu'une autre façon de procéder est de placer cette chaîne
dans tIdTcpConnection.Greeting, et tIdTcpServer se chargera d'envoyer cette chaîne automatiquement au moment de la connection. Un événement de moins, une propriété de plus. Bon.
En fait, la technique a été poussée au maximum, et nous pouvons par exemple, dans l'Inspecteur d'Objet préparer un message pour les commandes inconnues, une réponse préparée avec son code et son text, un autre texte en cas d'exception
rencontrés dans OnCommand. Nous pouvons récupérer les paramètres dans un tableau de String, ajouter automatiquement un "." isolé après certains textes etc. Le principal problème est que ces possibilités se combinent, et il devient
difficile, sans investissement en temps important, de comprendre les interactions entre toutes ces possibilités. Une plongée dans le source Indy, et des essais.
Nous allons nous efforcer de présenter ces différentes possibilités
4 - Exemple tIdCmdTcpServer détaillé - Indy 9 4.1 - Le protocole Comme toujours lorsque nous souhaitons implémenter un protocole sur mesure,
nous commençons par définir les textes envoyés et reçus et leur sémantique. Pour un exemple un peu plus complet, voici notre protocole
L'accueil - Greeting Pour demander à tIdTcpServer d'envoyer le message de bienvenue, nous pouvons
initialisant la propriété tIdTcpServer.Greeting. Cette propriété est de type tIdReply, qui contient - le code de status (sous forme d'Integer ou de String
- un tStrings pour le texte
Par conséquent: | créez un projet Client | |
dans un tButton "connect_" placez la connection, et les 3 Readln pour lire les 3 lignes qui seront envoyées par le Serveur
Procedure TForm1.connect_Click(Sender: TObject);
Var l_response: String; Begin
With IdTcpClient1 Do Begin
Port:= 5678; Connect;
l_response:= Readln; display(l_response);
l_response:= Readln; display(l_response);
l_response:= Readln; display(l_response);
End; // with ItTcpClient1 End; // connect_Click |
| Côté Serveur
Tout est très naturel. Sauf que le code 200 est reproduit devant les deux premières lignes, avec un - pour la première.
Pourquoi ? Parce que dans IdRfcReply.Pas nous trouvons:
Function TIdRFCReply.GenerateReply: String;
Var l_row_index: Integer; Begin
Result:= ''; If NumericCode> 0
Then Begin
Result:= '';
If FText.Count> 0
Then
For l_row_index:= 0 To FText.Count- 1 Do
If l_row_index< FText.Count- 1
Then Result:= Result+ IntToStr(NumericCode)
+ '-'+ FText[l_row_index]+ EOL;
Else Result:= Result+ IntToStr(NumericCode)
+ ' '+ FText[l_row_index]+ EOL;
Else Result:= Result+ IntToStr(NumericCode)+ EOL;
End Else
If FText.Count> 0
Then Result:= FText.Text;
End; // GenerateReply | ce que nous pouvons formuler
SI il un code numérique
ALORS
SI il y du texte
ALORS
la premièe ligne avec ce code et "-",
les suivantes le code avec " ",
puis le texte sont envoyés
SINON le code est envoyé
SINON tout le texte est envoyé
|
Une petite note sur "RFC". Cette abréviation correspond à Request For Comment. Lorsqu'internet s'est créé, différent chercheurs et hobbyistes on
proposé des protocoles. Comme leur définition pouvait comporter des erreurs ou omissions, ils demandaient l'avis des autres, pour tomber d'accord sur une spécification. Par exemple, POP3 correspond à RFC-1939, et SMTP à RFC-821. Vous
trouverez aisément ces spécification en utilisant Google. Le nom tIdRFCReply laisse entendre que le mode commande de Indy correspond à une spécification (internationale, ou, au mieux, définie par Indy). Il n'en est
rien. Tout ce que ce terme indique est que le mode commande a été créé pour pouvoir plus facilement écrire des composants qui correspondent aux véritables spécifications RFC, en facilitant le schéma
| commande et paramètre | | status et autre textes |
4.2 - Les tIdCommandHandlers Nous allons maintenant utiliser tIdCommandHandler.OnCommand |
côté Client, ajoutez un bouton "A" qui envoie cette commande et attend 2 chaînes:
Procedure TForm1.send_A_Click(Sender: TObject);
Var l_response: String; Begin
With IdTcpClient1 Do Begin
WriteLn('A'); l_response:= ReadLn;
display(l_response); l_response:= ReadLn;
display(l_response); End; // with IdTcpClient1
End; // send_A_Click | | |
côté Serveur, créez un tIdCommandHandler A, créez son événement OnCommand qui retournera la date et une autre chaîne:
Procedure TForm1.comand_handler_A_Command(ASender: TIdCommand);
Begin
ASender.Thread.Connection.WriteLn(DateToStr(Now)+ ' OnCommand');
ASender.Thread.Connection.WriteLn('after_date OnCommand');
End; // comand_handler_A_Command | | |
ajoutez aussi les événements tIdTcpServer.OnBeforeCommandHandler et tIdTcpServer.OnAfterCommandHandler | | voici le résultat:
|
4.3 - Utilisation de tIdCommandHandler.Response
Chaque tIdCommandHandler a une propriété Response, de type tStrings, qui enverra automatiquement ces chaînes. Deux précisions: - après les chaînes que nous avons placées dans Response, le Serveur enverra
en plus un "." isolé (comme dans le protocole SMTP décrit plus haut)
En effet, dans tIdCommandHandler.Check, la réponse est envoyée par
p_c_id_peer_thread.Connection.WriteRFCStrings(CommandHandler.Response); |
et WriteRfcStrings fonctionne ainsi :
Procedure TIdTCPConnection.WriteRFCStrings(AStrings: TStrings);
Var i: Integer; Begin
For i:= 0 To AStrings.Count- 1 Do
If AStrings[i]= '.'
Then WriteLn('..');
Else WriteLn(AStrings[i]);
WriteLn('.'); End; // WriteRFCStrings |
Le code montre aussi que si nos chaînes de Response contiennent un "." isolé sur une ligne, Indy dédoublera de point en ".." - et les chaînes de Response seront envoyées APRES les chaînes que nous
envoyons, si nous le souhaitons, dans tIdCommandHandler.OnCommand. Nous le savons après avoir examiné le texte de Check, que nous présenterons ci-dessous.
Pour le démontrer, nous allons utiliser ces deux types d'envoi au Client | côté Client, ajoutez un bouton "B" qui envoie cette commande et attend 5
chaînes (deux Writeln de OnCommand, 2 response et le point):
Procedure TForm1.send_B_Click(Sender: TObject);
Var l_response: String; Begin
With IdTcpClient1 Do Begin
WriteLn('B'); l_response:= ReadLn;
display(l_response); l_response:= ReadLn;
display(l_response); l_response:= ReadLn;
display(l_response); l_response:= ReadLn;
display(l_response); l_response:= ReadLn;
display(l_response); End; // with IdTcpClient1
End; // send_B_Click | | |
côté Serveur, créez un tIdCommandHandler B, créez son événement OnCommand qui retournera la date et une autre chaîne:
Procedure TForm1.id_command_handler_B_Command(ASender: TIdCommand);
Begin
ASender.Thread.Connection.WriteLn(DateToStr(Now)+ ' OnCommand');
ASender.Thread.Connection.WriteLn('after_date OnCommand');
End; // id_command_handler_B_Command | | |
dans l'Inspecteur d'Objet, sélectionnez le tIdCommandHandler B (éventuellement ouvrez l'éditeur en cliquant sur l'ellipse de IdTcpServer1.CommandHandlers)
Sélectionnez Response, et en cliquant sur Response, ajoutez les deux lignes "reponse_1" et "reponse_2" | | voici le résultat:
|
4.4 - tIdCommandHandler.ReplyNormal
Pour le moment, les commandes ne retournaient pas de code de status. Nous aurions pu en renvoyer en les plaçant dans les Writeln de l'événement OnCommand.
Mais Indy a prévu une automatisation par la propriété Reply. Reply est de type tIdRfcReply, et donc la mécanique de gestion du status est celle
présentée plus haut pour Greeting (premier status suivi de "-", status suivant suivi de " ") Voici un exemple |
côté Client, ajoutez un bouton "C" qui envoie cette commande et attend 7 chaînes (2 Writeln de OnCommand, 2 reply, 2 response et le point):
Procedure TForm1.send_C_Click(Sender: TObject);
Var l_response: String; Begin
With IdTcpClient1 Do Begin
WriteLn('C'); l_response:= ReadLn;
display(l_response); l_response:= ReadLn;
display(l_response); l_response:= ReadLn;
display(l_response); l_response:= ReadLn;
display(l_response); l_response:= ReadLn;
display(l_response); l_response:= ReadLn;
display(l_response); l_response:= ReadLn;
display(l_response); End; // with IdTcpClient1
End; // send_C_Click | | |
côté Serveur, créez un tIdCommandHandler C, créez son événement OnCommand qui retournera la date et une autre chaîne:
Procedure TForm1.command_hander_C_command(ASender: TIdCommand);
Begin
ASender.Thread.Connection.WriteLn(DateToStr(Now)+ ' OnCommand');
ASender.Thread.Connection.WriteLn('after_date OnCommand');
End; // command_hander_C_command | | |
dans l'Inspecteur d'Objet, sélectionnez le tIdCommandHandler C sélectionnez Response, et en cliquant sur Response, ajoutez les deux lignes "reponse_1" et "reponse_2" |
| sélectionnez ReplyNormal, et - entrez le code numérique 222
- entrez dans Text les deux chaînes "reply_1" et "reply_2"
|
| voici le résultat: |
Envoi de données binaires
Nous avons essentiellement échangé des données textuelles. Pour transférer des données binaires, nous utilisons les primitives SendBuffer, SendStream, ToBytes etc. Dans ce cas, le mode commande permet juste d'aiguiller la
commande vers le bon tIdCommandHandler, et nous devons utiliser les primitives de base, avec ou sans Reply et Response, en fonction de ce que le protocole a défini. Dans notre cas
| dans la commande DATE, le Client envoie "LA_DATE", les 8 octets de Now, lit les 8 octets et lit le "."
Procedure TForm1.send_DATE_Click(Sender: TObject);
Var l_response: String;
l_now, l_next_week: tDateTime; Begin
With IdTcpClient1 Do Begin
WriteLn('LA_DATE'); l_response:= ReadLn;
display(l_response); l_now:= Now;
WriteBuffer(l_now, SizeOf(tDateTime));
ReadBuffer(l_next_week, SizeOf(tDateTime));
display('now= '+ DateToStr(l_now)+ ' in_a_week '+ DateToStr(l_next_week));
l_response:= ReadLn; display(l_response);
End; // with IdTcpClient1 End; // send_DATE_Click |
| | côté Serveur, créez un tIdCommandHandler LA_DATE, créez son événement
OnCommand qui renverra "231 ok", lira la date, renverra la semaine prochaine et un "."
Procedure TForm1.command_handler_LA_DATE_command(ASender: TIdCommand);
Var l_date: tDateTime; Begin
display('Writeln(231 ok)');
ASender.Thread.Connection.WriteLn('231 ok');
ASender.Thread.Connection.ReadBuffer(l_date, SizeOf(tDateTime));
l_date:= l_date+ 7;
ASender.Thread.Connection.WriteBuffer(l_date, 8);
ASender.Thread.Connection.WriteLn('.');
End; // command_handler_LA_DATE_command | |
| voici le résultat: |
Notez que
- pour éviter les risques d'interférence avec Response et Reply, nous avons tout codé dans tIdCommandHandler.OnCommand
- nous avons utilisé WriteBuffer et ReadBuffer.
Pour WriteBuffer, le premier paramètre est un paramètre sans Type, et il faut faire attention de fournir comme premier paramètre l'adresse de notre tampon. D'où la locale l_date. Si nous avions utilisé Now comme premier
paramètre, c'est l'adresse de la fonction Now qui aurait été envoyée - il existe toute une kyrielle de primitives pour lire et écrire. En particulier des fonctions de conversion ToBytes, qui convertit un octet, un
caractère, un chaîne, une chaîne en précisant l'encodage.
Pour transmettre des chaînes, il convient de sélectionner la bonne primitive si vous utilisez Unicode
4.5 - Transfert de fichier
Le transfert d'un fichier binaire pourrait utiliser de façon similaire WriteStream et ReadStream: |
dans la commande FILE, le Client envoie "FILE nom_du_fichier", et lit dans un flux fichier le résultat
Procedure TForm1.send_LE_FICHIER_Click(Sender: TObject);
Var l_response: String;
l_size: Integer;
l_c_file_stream: tFileStream; Begin
With IdTcpClient1 Do Begin
WriteLn('LE_FICHIER aha.bin');
l_response:= Readln; display(l_response);
l_size:= ReadInteger;
display('size '+ IntToStr(l_size));
l_c_file_stream:= tFileStream.Create('resu.bin', fmCreate);
ReadStream(l_c_file_stream, l_size, False);
l_c_file_stream.Free; l_response:= Readln;
display(l_response); End; // with IdTcpClient1
display('< send_LE_FICHIER_Click'); End; // send_LE_FICHIER_Click
| | | côté Serveur, créez un tIdCommandHandler LE_FICHIER, créez son événement
OnCommand qui renverra "244 ok_found", lira le fichier et enverra sa taille et son contenu, puis envoyez un texte "255 fini"
Procedure TForm1.command_handler_LE_FICHIER_command(
ASender: TIdCommand);
Var l_file_name: String;
l_request_command: String;
l_c_file_stream: tFileStream; Begin
With ASender.Thread.Connection Do
Begin l_file_name:= ASender.Params[0];
WriteLn('244 ok_found');
l_c_file_stream:= tFileStream.Create(l_file_name, fmOpenRead);
WriteInteger(l_c_file_stream.Size);
WriteStream(l_c_file_stream);
l_c_file_stream.Free; WriteLn('255 finished');
End; // with ASender.Thread.Connection
display('< command_handler_LE_FICHIER_command');
End; // command_handler_LE_FICHIER_ommand | | |
voici le résultat: |
4.6 - Commande Erronnée
Si le Client envoie une commande pour laquelle nous n'avons pas prévu de CommandHandler, nous pouvons automatiser la réponse du Serveur en initialisant tIdTcpServer.ReplyUnknownCommand
| posez un tButton qui envoie la commande "Z" et lit la réponse. Si la réponse commence par "5", le Client lit aussi la ligne qui suit
Procedure TForm1.send_Z_Click(Sender: TObject);
Var l_response: String; Begin
With IdTcpClient1 Do Begin
WriteLn('Z'); display('=Readln response');
l_response:= ReadLn; display(l_response);
If Copy(l_response, 1, 1)= '5'
Then Begin
l_response:= ReadLn;
display(l_response);
End; End; // with IdTcpClient1
End; // send_Z_Click | | |
côté Serveur, sélectionnez IdTcpServer.ReplyUnknownCommand, et initialisez le code à 500, le texte à "unknown command" et "RTFM" | |
voici le résultat: |
4.7 - Disconnection côté Server
Chaque tIdCommandHandler possède une propriété Disconnect qui permet de provoquer la déconnection du client après l'envoi de données |
dans la commande QUIT, envoyez "QUIT" et lisez la réponse
Procedure TForm1.send_QUIT_Click(Sender: TObject);
Var l_response: String; Begin
With IdTcpClient1 Do Begin
WriteLn('QUIT'); l_response:= ReadLn;
display(l_response); End; // with IdTcpClient1
End; // send_QUIT_Click | | |
côté Serveur, créez un tIdCommandHandler QUIT, et - initialisez ReplyNormal à 222 et pour le texte "Bye. See you later"
- basculez sa propriété Disconnect à True
|
| côté Client, connectez vous, cliquez "Z", et pour visualiser que la connexion est rompue, cliquez une quelconque command, par exemple "A". Voici le résultat:
|
5 - Le fonctionnement du mode Commande Indy 9 5.1 - Le source de tIdCommandHandler.Check
Pour comprendre l'ordre des envoi de OnCommand, Reply, Response, la gestion des exceptions et la déconnection, la seule solution est d'examiner le source de Check. Voici une version (Indy 9)
Function TIdCommandHandler.Check(Const p_received_string: String;
p_c_id_peer_thread: TIdPeerThread): boolean;
Var l_unparsed_params: String; Begin
l_unparsed_params:= '';
// -- compare la chaîne reçue à la commade
Result:= AnsiSameText(p_received_string, Command);
If Not Result Then
// -- traite le cas où la chaine ne correspond pas à la commande
If CmdDelimiter<> #0
Then Begin
Result:= AnsiSameText(Copy(p_received_string, 1, Length(Command)+ 1),
Command+ CmdDelimiter);
l_unparsed_params:= Copy(p_received_string,
Length(Command)+ 2, MaxInt);
End
Else Begin
// -- traite un délimiteur #0
// -- retire la commande uniquement en utilisant sa longueur
// -- et pas le délimiteur
Result:= AnsiSameText(Copy(p_received_string, 1, Length(Command)),
Command);
l_unparsed_params:= Copy(p_received_string,
Length(Command)+ 1, MaxInt);
End; If Result
Then // -- ici le verbe correspond.
// -- créé un objet tIdCommand
With TIdCommand.Create Do
Try // -- replit RawLine
FRawLine:= p_received_string;
FCommandHandler:= Self;
FThread:= p_c_id_peer_thread;
FUnparsedParams:= l_unparsed_params;
// -- analyse les paramètres
Params.Clear;
If ParseParams
Then
If Self.FParamDelimiter= #32
Then SplitColumnsNoTrim(l_unparsed_params, Params, #32)
Else SplitColumns(l_unparsed_params, Params, Self.
FParamDelimiter);
// -- par défaut, initialise PerformReplay
// -- pourra être mis à False dans OnCommand
PerformReply:= True;
// -- transfer ReplyNormal dans tIdCommand.Reply
Reply.Assign(Self.ReplyNormal);
While True Do
Begin Try
// -- appelle notre DoCommand
DoCommand;
Except
// -- gestion des erreurs
on E: Exception Do
Begin
// -- envoie Reply, même si a rencontré une exception
If PerformReply
Then Begin
If Self.ReplyExceptionCode> 0
Then Begin
Reply.SetReply(ReplyExceptionCode, E.Message);
SendReply;
End
Else
If p_c_id_peer_thread.Connection.Server.
ReplyExceptionCode> 0
Then Begin
Reply.SetReply(
p_c_id_peer_thread.Connection.Server
.ReplyExceptionCode, E.Message);
SendReply;
End
Else
// -- pas de ReplyException => exception
Raise;
// -- a envoyé ReplyException => quitte la boucle
Break;
End
Else // -- n'a pas demandé Reply => exception qui remonte
// -- à l'appelant
Raise;
End;
End; // try ... except
// -- ici pas levé d'exception dans DoCommand
If PerformReply
Then
// -- si OnCommand n'a pas modifié PerformReply, envoie Reply
SendReply;
If Response.Count> 0
Then
// -- tIdCommand.Response n'est pas vide, envoie (suivi d'un ".")
p_c_id_peer_thread.Connection.WriteRFCStrings(Response);
Else
If CommandHandler.Response.Count> 0
Then
// -- idem pour CommandHandler.Response
p_c_id_peer_thread.Connection.WriteRFCStrings(
CommandHandler.Response);
// -- quitte la boucle
Break;
End; // while True
Finally Try
// -- après toutes les émissions, décide ou non de
// -- déconnecter ce client
If Disconnect
Then p_c_id_peer_thread.Connection.Disconnect;
Finally Free;
End;
End; // try finally
End; // Check | soit, en résumé : - si le premier identificateur est celui contenu dans
tIdCommandHandler.Command, la fonction retourne True et continue le traitement:
- un tIdCommand est créé
- il est initialisé avec les références du CommandHandler, mais surtout
avec le tIdPeerThread, ce qui permettra l'utilisation des primitives de lecture / écriture socket
- les paramètres sont placés dans Params
- par défaut, il est prévu d'envoyer une Reply
- si nous avons créé un tIdCommandHandler.OnCommand, il est appelé en premier
Au cours du traitement (en fonction de ce que nous avons reçu du Client,
nous pouvons, dans OnCommand, basculer PerformReply à False - les erreurs de OnCommand sont éventuellement traitées
- si PerformReply est toujours True, NormalReply est envoyé
- si Response contient des chaînes, elles sont envoyées par WriteRfcStrings (donc avec un "." sur une ligne)
- si nous avons demandé la déconnection (Disconnect True), la connection est rompue
Notez que - la présence du While reste à ce jour assez mystérieuse
5.2 - Le diagramme de Classe - Indy 9 Le diagramme côté Client est le même
Côté Serveur, nous avons :
et: - tIdTcpServer est une simple encapsulation de tIdPeerThreads
- en mode commande, nous créons des tIdCommandHandlers qui sont référencés par tIdTcpServer.IdCommandHandlers
- chaque tIdCommandHandler contient Command, diverses propriétés dont
Reply, Response et Disconnect
- lors de la réception de données, OnExecute appelle Check, qui, comme nous l'avons vu plus haut, créé un tIdCommand, en initialisant sa propriété
Thread. Thread.Connection est un tIdTcpConnection qui contient les méthodes de lecture ed d'écriture
Quelques commentaires - le développeur créé des tIdCommandHandlers. Ceux-ci sont attaché à
tIdTcpServer, et donc communs à tous les Clients
- il est fréquent que nous souhaitions adapter la réponse au client (un client voudra lire le mail 232, un autre le mail 25, qui, peut-être aura des pièces
jointes etc). Ceci explique pourquoi un tIdCommand séparé est créé pour répondre à la commande de chaque Client.
6 - Architecture Indy 10 et tIdTcpServer
6.1 - Les Classes Indy 10 Avant de présenter les deux modes Indy 10, mentionnons les différences entre les deux versions Voici le diagramme de classe UML (résumé) pour le tIdTcpClient:
Donc - toutes les lectures / écritures passent par tIdTcpClient.IoHandler
- il existe de nombreuses variations pour lire et écrire. Notons que
- pour les écritures, Write est très surchargé, le type de la VARIABLE passée en paramètre définissant le nombre d'octets envoyés
- pour la lecture, les fonctions sont spécifique (car si nous utilisions
Read dans une expression, cela n'indiquerait pas la taille à lire)
- il existe un mode tamponné. Et pour éviter ce mode, WriteDirect
- mentionnons aussi LargeStream pour des tailles de 64 bits
Pour la partie tIdTcpServer Et ici - le tIdTcpServer est toujours une encapsulation du Serveur
- il contient une liste de tIdContext
- ces contextes ont un propriété Connection, qui contient un IoHandler, qui, comme pour le Client, donne accès aux même méthodes de lecture / écriture
6.2 - Indy 10 en mode OnExecute L'exemple de lecture / écriture de fichier pourrait être écrit ainsi:
Procedure TForm1.connect_Click(Sender: TObject);
Var l_response: String; Begin
With IdTcpClient1 Do Begin
Port:= 5678; Connect;
l_response:= IoHandler.Readln;
display(l_response); End; // with ItTcpClient1
End; // connect_Click
Procedure TForm1.IdTCPClient1Connected(Sender: TObject);
Begin display('after_connect');
End; // IdTCPClient1Connected
Procedure TForm1.fichier_Click(Sender: TObject);
Var l_response: String;
l_size: Integer;
l_c_file_stream: tFileStream; Begin
With IdTcpClient1 Do Begin
// -- write to the server
IoHandler.WriteLn('aha.bin');
l_size:= IoHandler.ReadLongInt;
display('size '+ IntToStr(l_size));
l_c_file_stream:= tFileStream.Create('resu.bin', fmCreate);
IoHandler.ReadStream(l_c_file_stream, l_size, False);
l_c_file_stream.Free; End; // with IdTcpClient1
display('< fichier_Click'); End; // fichier_Click
Procedure TForm1.IdTCPClient1Disconnected(Sender: TObject);
Begin display('did_disconnect');
End; // IdTCPClient1Disconnected
Procedure TForm1.IdTCPClient1Status(ASender: TObject; Const AStatus: TIdStatus;
Const AStatusText: String); Begin
display('status '+ aStatusText);
End; // IdTCPClient1Status | Notons en particulier - toutes les lectures / écritures passent par IoHandler
- les lectures / écritures de String par Readln / Writeln
- pour lire les 4 octets d'un entier, nous avons utilisé ReadLongInt
- il faut prendre soin de bien préciser IoHandler.Writeln, car sinon Delphi croit q'il s'agit du Writeln écran, et provoque une erreur d'entrée/sortie 105
Et côté Serveur
Procedure TForm1.start_Click(Sender: TObject);
Begin IdTcpServer1.DefaultPort:= 5678;
IdTcpServer1.Active:= True;
End; // start_Click
Procedure TForm1.IdTCPServer1Connect(AContext: TIdContext);
Begin
AContext.Connection.IoHandler.WriteLn('Hello. File Transfer Ready');
End; // IdTCPServer1Connect
Procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
Var l_file_name: String;
l_c_file_stream: tFileStream; Begin
With AContext.Connection.IoHandler Do
Begin l_file_name := ReadLn;
display('request '+ l_file_name+ '<');
l_c_file_stream:= tFileStream.Create(l_file_name, fmOpenRead);
Write(l_c_file_stream.Size);
Write(l_c_file_stream); l_c_file_stream.Free;
// display('= Disconnect'); // Disconnect;
End; // with AThread.Connection End; // IdTCPServer1Execute
Procedure TForm1.IdTCPServer1Disconnect(AContext: TIdContext);
Begin display('a client did Disconnect');
End; // IdTCPServer1Disconnect
Procedure TForm1.IdTCPServer1Exception(AContext: TIdContext;
AException: Exception); Begin
display('*** exc '+ aException.Message);
End; // IdTCPServer1Exception
Procedure TForm1.IdTCPServer1Status(ASender: TObject; Const AStatus: TIdStatus;
Const AStatusText: String); Begin
display('status '+ aStatusText);
End; // IdTCPServer1Status
Procedure TForm1.stop_Click(Sender: TObject);
Begin IdTcpServer1.Active:= False;
End; // stop_Click |
6.3 - Le Serveur en mode Command
Indy 10 a créé un nouveau composant pour gérer le mode commande. Voici le diagramme UML correspondant : qui indique que
- tIdCmdTcpServer est un descendant de tIdTcpServer, chargé de gérer le mode command
- les propriétés (Greeting etc) et événements (OnBeforeCommandHandler) font partie à présent de tIdCmdTcpServer
- nous créons les tIdCommandHandlers comme précédemment, et chaque tIdCommandHandler a une méthode Check, au centre de tout le traitement qui créé un tIdCommand
- chaque tIdCommand a une propriété Context, qui permet via Connection.IoHander d'effectuer les lectures / écritures
Voyons à présent comment se code notre exemple de mode commande
6.3.1 - Le démarrage du Serveur Voici le code du bouton "start_"
Procedure TForm1.start_Click(Sender: TObject);
Begin IdCmdTcpServer1.DefaultPort:= 5678;
IdCmdTcpServer1.Greeting.NumericCode:= 233;
IdCmdTcpServer1.Greeting.Text.Add('server ready');
IdCmdTcpServer1.Greeting.Text.Add('How are you ?');
IdCmdTcpServer1.Active:= True;
End; // start_Click |
6.4 - La Connection d'un Client Côté Client, "connect_":
Procedure TForm1.connect_Click(Sender: TObject);
Var l_response: String; Begin
With IdTcpClient1 Do Begin
Port:= 5678; Connect;
l_response:= IoHandler.Readln;
display(l_response);
l_response:= IoHandler.Readln;
display(l_response);
l_response:= IoHandler.Readln;
display(l_response); End; // with ItTcpClient1
End; // connect_Click | Notez que les 2 premiers Readln lisent le Greeting, et le dernier lit le
texte envoyé dans OnConnect que voici :
Procedure TForm1.IdCmdTCPServer1Connect(AContext: TIdContext);
Begin
AContext.Connection.IoHandler.WriteLn(DateToStr(Now)+ ' manual writeln');
End; // IdTCPServer1Connect |
6.5 - Une simple commande "A" Côté Client, "send_A_":
Procedure TForm1.send_A_Click(Sender: TObject);
Var l_response: String; Begin
With IdTcpClient1.IoHandler Do
Begin WriteLn('A');
l_response:= Readln; display(l_response);
l_response:= Readln; display(l_response);
End; // with IdTcpClient1 End; // send_A_Click |
et côté Serveur
Procedure TForm1.comand_handler_A_Command(ASender: TIdCommand);
// -- événement de réception de la commande A Begin
ASender.Context.Connection.IoHandler.WriteLn(DateToStr(Now)+ ' OnCommand');
ASender.Context.Connection.IoHandler.WriteLn('after_date OnCommand');
End; // comand_handler_A_Command |
6.6 - Utilisation de Response Côté Client, "send_B_":
Procedure TForm1.send_B_Click(Sender: TObject);
Var l_response: String; Begin
With IdTcpClient1.IoHandler Do
Begin WriteLn('B');
l_response:= Readln; display(l_response);
l_response:= Readln; display(l_response);
l_response:= Readln; display(l_response);
l_response:= Readln; display(l_response);
l_response:= Readln; display(l_response);
End; // with IdTcpClient1 End; // send_B_Click |
et côté Serveur - nous avons rempli Response avec les deux chaînes
puis
Procedure TForm1.id_command_handler_B_Command(ASender: TIdCommand);
Begin // -- par défaut le code est 200
// -- ceci ne suffit pas // ASender.Reply.Code:= '';
// ASender.Reply.Text.Clear; ASender.PerformReply:= False;
IdCmdTcpServer1.Greeting.Text.Add('How are you ?');
ASender.Context.Connection.IoHandler.WriteLn(DateToStr(Now)+ ' OnCommand');
ASender.Context.Connection.IoHandler.WriteLn('after_date OnCommand');
End; // id_command_handler_B_Command | Notez que
- nous avons du batailler un peu pour que le Serveur n'envoie pas Reply. En Indy 10
- tIdReply ne contient que Code (le texte du status) et Text (les lignes de Reply)
- PAR DEFAUT Code est égal à "200". Pour éviter un envoi de Reply, il faut mettre Code à ''
- lorsque Response n'est pas vide, cela ne suffit pas, il faut forcer le
bloquage de l'envoi de Reply par PerformReply
6.7 - Response et Reply Côté Client, "send_C_":
Procedure TForm1.send_C_Click(Sender: TObject);
Var l_response: String; Begin
With IdTcpClient1.IoHandler Do
Begin WriteLn('C');
l_response:= Readln; display(l_response);
l_response:= Readln; display(l_response);
l_response:= Readln; display(l_response);
l_response:= Readln; display(l_response);
l_response:= Readln; display(l_response);
l_response:= Readln; display(l_response);
l_response:= Readln; display(l_response);
End; // with IdTcpClient1 End; // send_C_Click |
et côté Serveur - nous avons rempli Response avec les deux chaînes
- et Reply avec Code et deux lignes de texte
puis
Procedure TForm1.command_hander_C_command(ASender: TIdCommand);
Begin
ASender.Context.Connection.IoHandler.WriteLn(DateToStr(Now)+ ' OnCommand');
ASender.Context.Connection.IoHandler.WriteLn('after_date OnCommand');
End; // command_hander_C_command |
6.8 - Envoi de données binaires Côté Client, "send_DATE_":
Procedure TForm1.send_DATE_Click(Sender: TObject);
Var l_response: String;
l_now, l_next_week: tDateTime;
l_id_bytes: tIdBytes;
l_reception_id_bytes: tIdBytes; Begin
display('> send_DATE');
With IdTcpClient1.IoHandler Do
Begin WriteLn('LA_DATE');
l_response:= Readln; display(l_response);
l_now:= Now;
display(DateToStr(l_now));
l_id_bytes:= RawToBytes(l_now, SizeOf(tDateTime));
WriteDirect(l_id_bytes);
SetLength(l_reception_id_bytes, SizeOf(tDateTime));
ReadBytes(l_reception_id_bytes, SizeOf(tDateTime), False);
BytesToRaw(l_reception_id_bytes, l_next_week, SizeOf(tDateTime));
display('now= '+ DateToStr(l_now)+ ' in_a_week '+ DateToStr(l_next_week));
l_response:= Readln; display(l_response);
End; // with IdTcpClient1 End; // send_DATE_Click |
Notez que - tIdBytes est simplement un Array Of Byte
- RawToBytes et BytesToRaw (dans IDGLOBAL.PAS) permettent simplement des
transfert vers ce tableau. En gros, un Move. Mais avec l'information Length que Indy peut exploiter. Inversement, à la lecture, il faut allouer le tableau par SetLength avant de le remplir
- le False to ReadBytes indique que nous ne souhaitons pas concaténer (ajouter à un tIdBytes existant déjà)
et côté Serveur
Procedure TForm1.command_handler_LA_DATE_command(ASender: TIdCommand);
Var l_date: tDateTime;
l_id_bytes: tIdBytes;
l_reception_id_bytes: tIdBytes; Begin
With ASender.Context.Connection.IoHandler Do
Begin
ASender.Context.Connection.IoHandler.WriteLn('231 ok');
SetLength(l_reception_id_bytes, SizeOf(tDateTime));
ReadBytes(l_reception_id_bytes, SizeOf(tDateTime), False);
BytesToRaw(l_reception_id_bytes, l_date, SizeOf(tDateTime));
l_date:= l_date+ 7;
l_id_bytes:= RawToBytes(l_date, SizeOf(tDateTime));
WriteDirect(l_id_bytes); // l_now, SizeOf(tDateTime));
ASender.Context.Connection.IoHandler.WriteLn('.');
End; // with ASender.Context.Connection.IoHandler
End; // command_handler_LA_DATE_command | Notez que
- pour écrire, nous avons utilisé WriteDirect (sinon Indy tamponne et il faut forcer l'envoi)
6.9 - Transfert de fichier Côté Client, "send_FICHIER_":
Procedure TForm1.send_LE_FICHIER_Click(Sender: TObject);
Var l_response: String;
l_size: Integer;
l_c_file_stream: tFileStream; Begin
With IdTcpClient1.IoHandler Do
Begin WriteLn('LE_FICHIER aha.bin');
l_response:= Readln; display(l_response);
l_size:= ReadLongInt;
display('size '+ IntToStr(l_size));
l_c_file_stream:= tFileStream.Create('resu.bin', fmCreate);
ReadStream(l_c_file_stream, l_size, False);
l_c_file_stream.Free; l_response:= Readln;
display(l_response); End; // with IdTcpClient1
End; // send_LE_FICHIER_Click | et côté Serveur
Procedure TForm1.command_handler_LE_FICHIER_command(
ASender: TIdCommand);
Var l_file_name: String;
l_request_command: String;
l_c_file_stream: tFileStream;
l_size: LongInt; Begin
With ASender.Context.Connection.IoHandler Do
Begin ASender.PerformReply:= False;
l_file_name:= ASender.Params[0];
display('requested_file_name '+ l_file_name);
WriteLn('244 ok_found');
l_c_file_stream:= tFileStream.Create(l_file_name, fmOpenRead);
l_size:= l_c_file_stream.Size;
Write(l_size); Write(l_c_file_stream);
l_c_file_stream.Free; WriteLn('255 finished');
End; // with ASender.Context.Connection.IoHandler
End; // command_handler_LE_FICHIER_ommand |
6.10 - Une commande inconnue Côté Client, "send_Z_":
Procedure TForm1.send_Z_Click(Sender: TObject);
Var l_response: String; Begin
With IdTcpClient1.IoHandler Do
Begin WriteLn('Z');
l_response:= Readln; display(l_response);
If Copy(l_response, 1, 1)= '5'
Then Begin
l_response:= Readln;
display(l_response);
End; End; // with IdTcpClient1
End; // send_Z_Click | et côté Serveur
- nous avons rempli tIdCmdTcpServer.ReplyUnknownCommand avec le code 555 et le texte
6.11 - Commande "QUIT" Côté Client, "send_QUIT_":
Procedure TForm1.send_QUIT_Click(Sender: TObject);
Var l_response: String; Begin
With IdTcpClient1.IoHandler Do
Begin WriteLn('QUIT');
l_response:= Readln; display(l_response);
End; // with IdTcpClient1 End; // send_QUIT_Click |
et côté Serveur - nous avons rempli command_handler_QUIT_.Response avec le texte d'adieu
- nous avons demandé Disconnect True
et nous avons forcé l'annulation de Reply
Procedure TForm1.comand_handler_QUIT_comand(ASender: TIdCommand);
Begin ASender.PerformReply:= False;
End; // comand_handler_QUIT_comand |
7 - Un autre Exemple Indy 10 / Delphi 2009
Voici un autre exemple Indy 10 qui exploite quelques autres méthodes (Capture, SendCmd etc). Dans notre cas | le client se Connecte |
| le Serveur retourne "200 Bienvenue" | |
le Client envoie "LA_DATE" | | le Serveur retourne la date du jour, du lendemain, d'aujourd'hui en huit |
| le Client envoie "QUIT" | | le Serveur retourne "Bye" et déconnecte |
Commençons, pour changer, par le Serveur: - nous posons un tIdCmdtcpServeur
- dans Greeting, nous plaçons "200" et "Bienvenue"
- nous créons le tIdCommandHandler "LA_DATE", dans Reply nous plaçons 218, et dans OnCommand nous retournons les 3 dates en ajoutant ces chaînes à la tStrings Response
- nous créons le tIdCommandHandler "QUIT" et dans Reply nous plaçons "202" et "Bye"
Voici le code:
Procedure TForm1.IdCmdTCPServer1Connect(AContext: TIdContext);
Begin display('client_did_connect');
End; // IdTCPServer1Connect
Procedure TForm1.IdCmdTCPServer1BeforeCommandHandler(ASender: TIdCmdTCPServer;
Var AData: String; AContext: TIdContext);
Begin
display('IdCmdTCPServer1BeforeCommandHandler '+ aData+ '<');
End; // IdCmdTCPServer1BeforeCommandHandler
Procedure TForm1.command_handler_LA_DATE_command(ASender: TIdCommand);
Begin
ASender.Response.Add('the_date '+ DateToStr(Now));
ASender.Response.Add('tomorrow '+ DateToStr(Now+ 1));
ASender.Response.Add('in_a_weem '+ DateToStr(Now+ 7));
End;
Procedure TForm1.comand_handler_QUIT_comand(ASender: TIdCommand);
Begin display('comand_handler_QUIT_comand');
End; // comand_handler_QUIT_comand
Procedure TForm1.IdCmdTCPServer1AfterCommandHandler(ASender: TIdCmdTCPServer;
AContext: TIdContext); Begin
display('IdCmdTCPServer1AfterCommandHandler');
End; // IdCmdTCPServer1AfterCommandHandler
Procedure TForm1.stop_Click(Sender: TObject);
Begin IdCmdTcpServer1.Active:= False;
End; // stop_Click
Procedure TForm1.IdCmdTCPServer1Status(ASender: TObject; Const AStatus: TIdStatus;
Const AStatusText: String); Begin
End; // IdCmdTCPServer1Status |
Et à présent le Client :
- un bouton pour connecter et récupérer le message de bienvenue en vérifiant le status 200
- un bouton pour envoyer "LA_DATE" en vérifiant le status 218, puis la
récupération des 3 chaînes que nous ajoutons à un tStrings (Memo1.Lines)
- un bouton pour envoyer "QUIT" en vérifiant le status 202 et en récupérant le texte
Ceci peut être réalisé par le code suivant:
Procedure TForm1.connect_Click(Sender: TObject);
Var l_response: String; Begin
With IdTcpClient1 Do Begin
Port:= 5678; Connect;
GetResponse(200);
display(LastCmdResult.Text.Text+ '%');
End; // with ItTcpClient1 End; // connect_Click
Procedure TForm1.IdTCPClient1Connected(Sender: TObject);
Begin display('after_connect');
End; // IdTCPClient1Connected
Procedure TForm1.send_date_string_Click(Sender: TObject);
Begin IdTcpClient1.SendCmd('LA_DATE', 218);
IdTcpClient1.IOHandler.Capture(Memo1.Lines);
End; // send_date_string_Click
Procedure TForm1.quit_Click(Sender: TObject);
Begin IdTCPClient1.SendCmd('Quit', 202);
display(IdTCPClient1.LastCmdResult.Text.Text+ '%');
End; // quit_Click
Procedure TForm1.disconnect_Click(Sender: TObject);
Begin IdTCPClient1.Disconnect;
End; // disconnect_Click
Procedure TForm1.IdTCPClient1Disconnected(Sender: TObject);
Begin display('did_disconnect');
End; // IdTCPClient1Disconnected
Procedure TForm1.IdTCPClient1Status(ASender: TObject; Const AStatus: TIdStatus;
Const AStatusText: String); Begin
display('status '+ aStatusText);
End; // IdTCPClient1Status |
8 - Commentaires
8.1 - Quelques Recommandations Nos recommandations pour utiliser Indy sont les suivantes: - commencez, avant tout par un exemple simple: envoyer un chaîne, la modifier côté Serveur, et la récupérer côté Client.
La première application doit fonctionner en mode OnExecute. Si vous le souhaitez, faites-la fonctionner en mode commande - une fois que ceci fonctionne, essayez de transférer :
- une chaîne (déjà fait plus haut)
- des octets (données binaires)
- des flux
Commencez par le mode OnExecute. Vous pouvez à ce niveau tester toutes les méthodes de lecture / écriture. Les octets, les caractères, les String,
Unicode, les données binaires, les flux. Prenez un jour ou deux pour lire intégralement l'aide, qui détaille les différentes primitives de lecture / écriture. Vous devez devenir un expert dans ce domaine. C'est un
investissement indispensable, et si vous faites l'impasse, vous passerez plus de temps à lire la doc par la suite. Eventuellement, répétez en mode commande - quand vous en arrivez à l'application réelle
- le PLUS IMPPORTANT est de bien définir le protocole:
- le Client envoie quoi
- quelle est la réponse qu'il attend
Après tout le Client est roi, et les Serveur est là pour fournir ce
qu'on lui demande - implémentez une commande Client et Serveur (en mode OnExecute ou command), puis généralisez
- et si vous débutez en Indy, ou si vous devez utiliser une nouvelle version
ou migrer une ancienne application, démarrez par le mode OnExecute. Pas de surprises, pas d'effet de billard à trois bandes, de source à analyser etc.
Et il faudra, en général, utiliser des techniques non présentées ici, comme
la création de Classes dérivées de tIdPeerThread (ou tIdContext), ou tIdReply, accéder à la liste de tIdPeerThreads (tIdContexts) etc. Techniques présentées dans nos
formations sockets (en particuler Indy) Si maintenant vous devez développer une famille de 5 ou 6 protocoles, qui se prêtent au mode commande, cela vaudra, peut être éventuellement, la
peine d'investir dans la compréhension de son fonctionnement. Sachant que cet investissement a toutes les chances d'avoir à être renouvelé pour chaque nouvelle version d'Indy. Cave Emptor.
Soulignons aussi que pour développer des applications à base de socket, il est indispensable de comprendre le mécanisme de base du Serveur (le socket écoute et crée un socket pour chaque Client qui se connecte). Vous pourrez consulter
les nombreux articles concernant la programmation des sockets Tcp/Ip que nous avons rédigés. Et finalement, si vous choisissez Indy (plutôt que ICS ou les tClientSocket /
tServerSocket Delphi, il faut aussi maîtriser le fonctionnement des threads. Nous avons d'ailleurs déjà mentionné que pour ce tutorial élémentaire nos affichages côté
Serveur n'étaient pas "thread safe".
8.2 - La Volatilité Indy Indy souffre, et a toujours souffert, d'un manque de stabilité d'une version à
l'autre. Pour rendre l'écriture de leurs protocoles plus efficaces, les développeurs Indy n'ont jamais hésité à ajouter une indirection, changer le type d'un paramètre, concocter une nouvelle méthode, séparer certains
traitement dans un nouveau composant. La conséquence est que les applications Indy 8 fonctionnent rarement en Indy 9 ou Indy 10 sans une adaptation, qui n'est pas toujours triviale. Et en
particulier, le mode commande qui utilise beaucoup de propriétés de l'Inspecteur d'Objet est plus pénible à migrer que le mode OnExecute ou les remplacements textuels sont plus faciles.
Quoiqu'il en soit, Indy offre essentiellement - une bonne encapsulation des protocoles standards
- un mode bloquant qui évite d'éparpiller les traitements dans de nombreux événements
- une couche SSL (Secure Socket Layer) nécessaire pour certaines applications sécurisées (et qui n'est pas offerte par les couches ICS ou Delphi).
8.3 - Notre utilisation d'Indy
En fait nous utilisons Indy pour les développements suivants:
Nous regrettons les modifications fréquentes des composants Indy, mais cela ne nous empêche pas de les utiliser !
8.4 - Développements Client Nous avons développé ou maintenons plusieurs applications utilisant des
protocoles sur mesure à base de tidTcpClient ou tIdTcpServer, avec les versions 8 à 10 de Indy, et sur des versions de Delphi allant de Delphi 7 à Delphi 2010.
Mais dans tous ces cas, nous utilisons essentiellement le mode OnExecute.
9 - Télécharger le code source Delphi Vous pouvez télécharger: Ce .ZIP qui comprend: - le .DPR, la forme principale, les formes annexes eventuelles
- les fichiers de paramètres (le schéma et le batch de création)
- dans chaque .ZIP, toutes les librairies nécessaires à chaque projet (chaque .ZIP est autonome)
Ces .ZIP, pour les projets en Delphi 6, contiennent des chemins RELATIFS. Par conséquent: - créez un répertoire n'importe où sur votre machine
- placez le .ZIP dans ce répertoire
- dézippez et les sous-répertoires nécessaires seront créés
- compilez et exécutez
Ces .ZIP ne modifient pas votre PC (pas de changement de la Base de Registre, de DLL ou autre). Pour supprimer le projet, effacez le répertoire.
La notation utilisée est la notation alsacienne qui consiste à préfixer les identificateurs par la zone de compilation: K_onstant, T_ype, G_lobal,
L_ocal, P_arametre, F_unction, C_lasse. Elle est présentée plus en détail dans l'article La
Notation Alsacienne
Comme d'habitude: - nous vous remercions de nous signaler toute erreur, inexactitude ou
problème de téléchargement en envoyant un e-mail à jcolibri@jcolibri.com. Les corrections qui en résulteront pourront aider les prochains lecteurs
- tous vos commentaires, remarques, questions, critiques, suggestion d'article, ou mentions d'autres sources sur le même sujet seront de même les bienvenus à jcolibri@jcolibri.com.
- plus simplement, vous pouvez taper (anonymement ou en fournissant votre e-mail pour une réponse) vos commentaires ci-dessus et nous les envoyer en cliquant "envoyer" :
- et si vous avez apprécié cet article, faites connaître notre site, ajoutez un lien dans vos listes de liens ou citez-nous dans vos
blogs ou réponses sur les messageries. C'est très simple: plus nous aurons de visiteurs et de références Google, plus nous écrirons d'articles.
10 - Références Indy Quelques références:
- pour la programmation socket:
Mentionnons aussi les deux formations suivantes -
Formation TCP/IP sockets Delphi 2 jours pour présenter le mécanisme de base des sockets, la librairie Windows,
les sockets tClientSocket / tServerSocket Delphi et Indy. Pour la partie Indy, les diagrammes de classes sont plus complets, les propriétés décrites plus en détail et les exemples plus nombreux que ce que nous avons pu
présenter ici
- Formation
Threads Delphi : mise en oeuvre du multi-tâche Delphi en utilisant les Threads - communication entre threads, accès aux données, synchronisation entre threads
Et finalement, nous intervenons aussi en tant que
Consultant Delphi pour développer ou assurer la maintenance de projets Delphi, en particulier dans le domaine des threads et des développements Tcp/IP.
11 - L'auteur John COLIBRI est passionné par le développement Delphi et les applications de Bases de Données. Il a écrit de nombreux livres et articles, et partage son temps entre le développement de projets (nouveaux projets, maintenance, audit, migration BDE, migration Xe_n, refactoring) pour ses clients, le
conseil (composants, architecture, test) et la
formation. Son site contient des articles
avec code source, ainsi que le programme et le calendrier des stages de formation Delphi, base de données, programmation objet, Services Web, Tcp/Ip et
UML qu'il anime personellement tous les mois, à Paris, en province ou sur site client. |