menu
  Home  ==>  articles  ==>  web  ==>  client_serveur_tcp_indy   

Sockets Client Serveur TCP Indy - John COLIBRI.


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 :

indy_9_client

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(SenderTObject);
  Var l_greetingString;
  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(SenderTObject);
  Var l_sizeInteger;
      l_c_file_streamtFileStream;
  Begin
    With IdTcpClient1 Do
    Begin
      WriteLn('aha.txt');

      l_size:= ReadInteger;

      l_c_file_stream:= tFileStream.Create('resu.txt'fmCreate);
      ReadStream(l_c_file_streaml_sizeFalse);
      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(SenderTObject);
  Begin
    IdTcpClient1.Disconnect;
  End// disconnect_Click



2.3 - Le Serveur tIdTcpServer Indy 9

Le Serveur utilise les Classes suivantes:

server_execute

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(SenderTObject);
  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(AThreadTIdPeerThread);
  Var l_request_commandString;
      l_c_file_streamtFileStream;
  Begin
    With AThread.Connection Do
    Begin
      l_request_command := ReadLn;

      l_c_file_stream:= tFileStream.Create(l_request_commandfmOpenRead);

      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(SenderTObject);
  Begin
    IdTcpServer1.Active:= True;
  End// stop_Click



2.4 - Exécution

Voici le Serveur

00_server_tcp_indy_9

et voici le Client

01_client_tcp_indy_9



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

    VERBE { paramètres ]

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(AThreadTIdPeerThread);
  Var l_request_commandString;
  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(AThreadTIdPeerThread): boolean;
  Var l_command_handler_countinteger;
      l_command_handler_indexInteger;
      l_client_lineString;
  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(AThreadl_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_indexDo
                            If Enabled And Check(l_client_lineAThread)
                              Then Break;
                          inc(l_command_handler_index);
                        End//while

                        If l_command_handler_indexl_command_handler_count
                          Then DoOnNoCommandHandler(l_client_lineAThread);
                      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_stringString;
    p_c_id_peer_threadTIdPeerThread): boolean;
  Begin
    // -- compare la chaîne reçue à la commade
    Result:= AnsiSameText(p_received_stringCommand);

    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(SenderTObject);
  Var l_responseString;
  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
   créez une application \Serveur, posez-y un tIdTcpServer
   posez un tButton "start_" et dans son OnClic initialisez le DefaultPort, et basculez Active à True
   pour créer un tIdCommandHandler, sélectionnez IdTcpServer1 et dans l'Inspecteur d'Objet, cliquez 2 fois sur l'ellipse (...) de CommandHandlers
   un dialogue de création des CommandHandlers est affiché
   clic droit et Add (ou l'icône jaune)
   l'Inspecteur d'Objet présente le nouveau CommandHandler0
   dans Command, tapez le texte de la commande, "A" dans notre cas
   créez l'événement OnCommand qui sera appelé lorsque le Serveur recevra "A"
   tapez le code qui renverra "aaa"

Procedure TForm1.IdTCPServer1TIdCommandHandler0Command(ASenderTIdCommand);
  Begin
    ASender.Thread.Connection.WriteLn('aaa');
  End// IdTCPServer1TIdCommandHandler0Command

   compilez le tout
   lancez le Serveur et cliquez "start_"
   lancez le Client et cliquez "send_A"
   voici le résultat

00_server_tcp_indy_9



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(AThreadTIdPeerThread);
  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
   le Client se connecte
   le Serveur répond deux lignes
    200 Bienvenue
    Le serveur est prêt
    Connection le 22 mars 2010
   le Client envoie
    A
   le Serveur retourne
    22/mars/2010
    après la date

   le Client envoie
    B
   le Serveur retourne
    22/mars/2010
    après la date
    reponse_1
    reponse_2
    .

   le Client envoie
    C
   le Serveur retourne
    22/mars/2010
    après la date
    reply_1
    reply_2
    reponse_1
    reponse_2
    .

   le Client envoie
    LA_DATE
    les octets de Now
   le Serveur retourne
    231 ok
    les octets de Now+ 8
    .

   le Client envoie
    LE_FICHIER aha.bin
   le Serveur répond par
    244 ok_found
    la taille du fichier
    son contenu
    255 fini

   le Client envoie une commande inconnue du Serveur
    Z
   le Serveur répond courtoisement
    500 unknown command
    RTFM

   le Client envoie
    QUIT
   le Serveur envoie
    bye. See you later
    coupe la connection avec ce Client



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(SenderTObject);
  Var l_responseString;
  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
   créez un projet Serveur, avec un bouton "start_" pour basculer Active à True
   dans IdTcpServer1.Greeting, cliquez "+", et placez 200 dans dans NumericCode
   cliquez l'ellipse ... de Text, et tapez les deux premières lignes de bienvenue
    Bienvenue
    Le serveur est prêt
   créez l'événement IdTcpServer1.Connect, et envoyez au Client la date

Procedure TForm1.IdTCPServer1Connect(AThreadTIdPeerThread);
  Begin
    AThread.Connection.WriteLn(DateToStr(Now));
  End// IdTCPServer1Connect

   lancez le Serveur, lancez le Client et cliquez "connect_"
   voici le résultat

00_tcp_client_connects



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.GenerateReplyString;
  Var l_row_indexInteger;
  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_indexFText.Count- 1
                  Then Result:= ResultIntToStr(NumericCode)
                      + '-'FText[l_row_index]+ EOL;
                  Else Result:= ResultIntToStr(NumericCode)
                      + ' 'FText[l_row_index]+ EOL;
            Else Result:= ResultIntToStr(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(SenderTObject);
  Var l_responseString;
  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(ASenderTIdCommand);
  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:

01_tcp_server_on_client_connected



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(AStringsTStrings);
      Var iInteger;
      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(SenderTObject);
  Var l_responseString;
  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(ASenderTIdCommand);
  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:

03_tcp_server_response



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(SenderTObject);
  Var l_responseString;
  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(ASenderTIdCommand);
  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:

04_tcp_server_reply



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(SenderTObject);
  Var l_responseString;
      l_nowl_next_weektDateTime;
  Begin
    With IdTcpClient1 Do
    Begin
      WriteLn('LA_DATE');

      l_response:= ReadLn;
      display(l_response);

      l_now:= Now;
      WriteBuffer(l_nowSizeOf(tDateTime));

      ReadBuffer(l_next_weekSizeOf(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(ASenderTIdCommand);
  Var l_datetDateTime;
  Begin
    display('Writeln(231 ok)');
    ASender.Thread.Connection.WriteLn('231 ok');
    ASender.Thread.Connection.ReadBuffer(l_dateSizeOf(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:

05_tcp_server_binary



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(SenderTObject);
  Var l_responseString;
      l_sizeInteger;
      l_c_file_streamtFileStream;
  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_streaml_sizeFalse);
      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(
    ASenderTIdCommand);
  Var l_file_nameString;
      l_request_commandString;
      l_c_file_streamtFileStream;
  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_namefmOpenRead);

      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:

05_tcp_server_file



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(SenderTObject);
  Var l_responseString;
  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:

08_unknown_command



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(SenderTObject);
  Var l_responseString;
  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:

10_server_command_disconnect




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_stringString;
    p_c_id_peer_threadTIdPeerThread): boolean;
  Var l_unparsed_paramsString;
  Begin
    l_unparsed_params:= '';

    // -- compare la chaîne reçue à la commade
    Result:= AnsiSameText(p_received_stringCommand);
    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),
                  CommandCmdDelimiter);
              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_paramsParams, #32)
                  Else SplitColumns(l_unparsed_paramsParamsSelf.
                          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 EException Do
                Begin
                  // -- envoie Reply, même si a rencontré une exception
                  If PerformReply
                    Then Begin
                        If Self.ReplyExceptionCode> 0
                          Then Begin
                              Reply.SetReply(ReplyExceptionCodeE.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
                                          .ReplyExceptionCodeE.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 :

indy_9_server_command_handler

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:

indy_10_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

indy_10_tidtcpserver_execute

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(SenderTObject);
  Var l_responseString;
  Begin
    With IdTcpClient1 Do
    Begin
      Port:= 5678;
      Connect;

      l_response:= IoHandler.Readln;
      display(l_response);
    End// with ItTcpClient1
  End// connect_Click

Procedure TForm1.IdTCPClient1Connected(SenderTObject);
  Begin
    display('after_connect');
  End// IdTCPClient1Connected

Procedure TForm1.fichier_Click(SenderTObject);
  Var l_responseString;
      l_sizeInteger;
      l_c_file_streamtFileStream;
  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_streaml_sizeFalse);
      l_c_file_stream.Free;
    End// with IdTcpClient1
    display('< fichier_Click');
  End// fichier_Click

Procedure TForm1.IdTCPClient1Disconnected(SenderTObject);
  Begin
    display('did_disconnect');
  End// IdTCPClient1Disconnected

Procedure TForm1.IdTCPClient1Status(ASenderTObjectConst AStatusTIdStatus;
    Const AStatusTextString);
  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(SenderTObject);
  Begin
    IdTcpServer1.DefaultPort:= 5678;
    IdTcpServer1.Active:= True;
  End// start_Click

Procedure TForm1.IdTCPServer1Connect(AContextTIdContext);
  Begin
    AContext.Connection.IoHandler.WriteLn('Hello. File Transfer Ready');
  End// IdTCPServer1Connect

Procedure TForm1.IdTCPServer1Execute(AContextTIdContext);
  Var l_file_nameString;
      l_c_file_streamtFileStream;
  Begin
    With AContext.Connection.IoHandler Do
    Begin
      l_file_name := ReadLn;
      display('request 'l_file_name'<');

      l_c_file_stream:= tFileStream.Create(l_file_namefmOpenRead);

      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(AContextTIdContext);
  Begin
    display('a client did Disconnect');
  End// IdTCPServer1Disconnect

Procedure TForm1.IdTCPServer1Exception(AContextTIdContext;
    AExceptionException);
  Begin
    display('*** exc 'aException.Message);
  End// IdTCPServer1Exception

Procedure TForm1.IdTCPServer1Status(ASenderTObjectConst AStatusTIdStatus;
    Const AStatusTextString);
  Begin
    display('status 'aStatusText);
  End// IdTCPServer1Status

Procedure TForm1.stop_Click(SenderTObject);
  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 :

indy_10_tidcmdtcpserver

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(SenderTObject);
  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(SenderTObject);
  Var l_responseString;
  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(AContextTIdContext);
  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(SenderTObject);
  Var l_responseString;
  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(ASenderTIdCommand);
    // -- é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(SenderTObject);
  Var l_responseString;
  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(ASenderTIdCommand);
  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(SenderTObject);
  Var l_responseString;
  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(ASenderTIdCommand);
  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(SenderTObject);
  Var l_responseString;
      l_nowl_next_weektDateTime;
      l_id_bytestIdBytes;
      l_reception_id_bytestIdBytes;
  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_nowSizeOf(tDateTime));
      WriteDirect(l_id_bytes);

      SetLength(l_reception_id_bytes,  SizeOf(tDateTime));
      ReadBytes(l_reception_id_bytesSizeOf(tDateTime), False);
      BytesToRaw(l_reception_id_bytesl_next_weekSizeOf(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(ASenderTIdCommand);
  Var l_datetDateTime;
      l_id_bytestIdBytes;
      l_reception_id_bytestIdBytes;
  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_bytesSizeOf(tDateTime), False);
      BytesToRaw(l_reception_id_bytesl_dateSizeOf(tDateTime));

      l_date:= l_date+ 7;
      l_id_bytes:= RawToBytes(l_dateSizeOf(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(SenderTObject);
  Var l_responseString;
      l_sizeInteger;
      l_c_file_streamtFileStream;
  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_streaml_sizeFalse);
      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(
    ASenderTIdCommand);
  Var l_file_nameString;
      l_request_commandString;
      l_c_file_streamtFileStream;
      l_sizeLongInt;
  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_namefmOpenRead);

      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(SenderTObject);
  Var l_responseString;
  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(SenderTObject);
  Var l_responseString;
  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(ASenderTIdCommand);
  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(AContextTIdContext);
  Begin
    display('client_did_connect');
  End// IdTCPServer1Connect

Procedure TForm1.IdCmdTCPServer1BeforeCommandHandler(ASenderTIdCmdTCPServer;
    Var ADataStringAContextTIdContext);
  Begin
    display('IdCmdTCPServer1BeforeCommandHandler 'aData'<');
  End// IdCmdTCPServer1BeforeCommandHandler

Procedure TForm1.command_handler_LA_DATE_command(ASenderTIdCommand);
  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(ASenderTIdCommand);
  Begin
    display('comand_handler_QUIT_comand');
  End// comand_handler_QUIT_comand

Procedure TForm1.IdCmdTCPServer1AfterCommandHandler(ASenderTIdCmdTCPServer;
    AContextTIdContext);
  Begin
    display('IdCmdTCPServer1AfterCommandHandler');
  End// IdCmdTCPServer1AfterCommandHandler

Procedure TForm1.stop_Click(SenderTObject);
  Begin
    IdCmdTcpServer1.Active:= False;
  End// stop_Click

Procedure TForm1.IdCmdTCPServer1Status(ASenderTObjectConst AStatusTIdStatus;
    Const AStatusTextString);
  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(SenderTObject);
  Var l_responseString;
  Begin
    With IdTcpClient1 Do
    Begin
      Port:= 5678;
      Connect;

      GetResponse(200);
      display(LastCmdResult.Text.Text'%');
    End// with ItTcpClient1
  End// connect_Click

Procedure TForm1.IdTCPClient1Connected(SenderTObject);
  Begin
    display('after_connect');
  End// IdTCPClient1Connected

Procedure TForm1.send_date_string_Click(SenderTObject);
  Begin
    IdTcpClient1.SendCmd('LA_DATE', 218);
    IdTcpClient1.IOHandler.Capture(Memo1.Lines);
  End// send_date_string_Click

Procedure TForm1.quit_Click(SenderTObject);
  Begin
    IdTCPClient1.SendCmd('Quit', 202);
    display(IdTCPClient1.LastCmdResult.Text.Text'%');
  End// quit_Click

Procedure TForm1.disconnect_Click(SenderTObject);
  Begin
    IdTCPClient1.Disconnect;
  End// disconnect_Click

Procedure TForm1.IdTCPClient1Disconnected(SenderTObject);
  Begin
    display('did_disconnect');
  End// IdTCPClient1Disconnected

Procedure TForm1.IdTCPClient1Status(ASenderTObjectConst AStatusTIdStatus;
    Const AStatusTextString);
  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:
  • mi décembre, notre hébergeur Corse a mis fin à notre contrat car la mutualisation des hébergements lui causait trop de soucis. Nous avions 15 jours pour trouver une solution.

    Nous en avons profité pour, enfin, héberger notre propre site

    • nous avons écrit le Serveur à partir d'un tIdHttpServer Indy 9 codé en Delphi 6.
    • cette application est placée sur un mini-PC de 20 cm de haut et 1 cm de large avec Windows 7 qui nous a royalement coûté 280 Euros !
    • le serveur de mail à base de tIdSmtpServer a été ajouté
    Pour fonctionner, nous utilisons une ligne fixe Orange (abonnement Orange Pro)
    Dans cette histoire, ce qui nous a pris le plus de temps a été de paramétrer Gandi pour rediriger les requêtes vers notre IP fixe Orange. Quoiqu'il en soit, en 8 jours tout a été bouclé

  • nous avons aussi utilisé tIdNntpClient pour lire les newsgroups bêta testeur Borland, qui nécessitaient un mode sécurisé
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" :
    Nom :
    E-mail :
    Commentaires * :
     

  • 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:

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.
Created: jan-04. Last updated: mar-2020 - 250 articles, 620 .ZIP sources, 3303 figures
Contact : John COLIBRI - Tel: 01.42.83.69.36 / 06.87.88.23.91 - email:jcolibri@jcolibri.com
Copyright © J.Colibri   http://www.jcolibri.com - 2001 - 2020
Retour:  Home  Articles  Formations  Développement Delphi  Livres  Pascalissime  Liens  Download
l'Institut Pascal

John COLIBRI

+ Home
  + articles_avec_sources
    + bases_de_donnees
    + web_internet_sockets
      – http_client
      – moteur_de_recherche
      – javascript_debugger
      + asp_net
      – client_serveur_tcp
      – site_editor
      – pascal_to_html
      – cgi_form
      + les_logs_http_bruts
      + les_blogs
      – intraweb_tutorial
      + services_web
      – serveur_web_iis
      – client_serveur_pop3
      – lecteur_mails_free
    + services_web_
    + prog_objet_composants
    + office_com_automation
    + colibri_utilities
    + uml_design_patterns
    + graphique
    + delphi
    + outils
    + firemonkey
    + vcl_rtl
    + colibri_helpers
    + colibri_skelettons
    + admin
  + formations
  + developpement_delphi
  + présentations
  + pascalissime
  + livres
  + entre_nous
  – télécharger

contacts
plan_du_site
– chercher :

RSS feed  
Blog

Formation Bases de Données Multi Tiers Delphi Gestion de bases de données Mulit Tiers : architecture, le serveur d'application : dbExorss et DataSnap, les clients légers, édition d'états - 3 jours
Formation Bases de Données Oracle avec Delphi Gestion de bases de données Oracle : connexion, accès aux tables, édition d'états - 3 jours