Accueil > Programmation Cocoa > Gestion de Réseau sous Cocoa

Gestion de Réseau sous Cocoa

Par Mike Beam, co-auteur de Cocoa in a Nutshell, le 13/05/2003

Traduit par Thierry, le 16/05/2003

Note de l’Editeur : Nous sommes heureux de retrouver Mike Beam après sa participation à l’écriture de Cocoa in a Nutshell avec James Duncan Davidson.

Dans le dernier article nous en avons un peu appris sur la programmation des réseaux Unix avec l’API Sockets. Aujourd’hui, nous allons terminer le programme de chat RCE que nous avons commencé quelques articles auparavant. Nous aurons besoin d’une bonne dose de programmation sockets en provenance du dernier article arrosée d’un peu de Foundation framework avec la classe NSFileHandle.

Le Protocole RCE

La première chose à faire est d’établir le protocole de chat RCE. C’est un protocole très simple qui décrit le format des messages de chat entre deux clients. Un message de chat sera une chaîne de caractères avec trois composants : la chaîne “_rce_” (qui permet de déterminer que la chaîne de caractère est un message RCE), le nom de l’émetteur et le contenu du message. Chacun de ces composants sera séparé par un “:”. Par exemple, un message de ma provenance ressemblerait à ceci :

_rce_:Mike:How's it going?

NSFileHandle pour les Communications Socket

Dans l’article précédent nous avons appris à créer un socket (dispositif de transfert d’entrée et de sortie sur un réseau) et comment lire et écrire des données dans le socket en utilisant les fonctions Unix standard d’E/S (Entrée/Sortie), read() et write(). Ces deux fonctions prennent comme argument un descripteur de fichier qui identifie un fichier sur un disque ou un socket ouvert. Dans le cas des réseaux, read() et write() gèrent les E/S sur socket. Nous avons aussi vu dans le dernier article comment nous devions appeler accept() dans une boucle infinie de type “for” de façon à ce que notre programme puisse gérer les nouvelles connexions de nos clients.

Le framework Foundation comporte cette fonctionnalité d’E/S dans la classe NSFileHandle, qui fournit une interface à l’écriture et à la lecture de fichiers ou de chaines de communication tel qu’un socket. Les fonctionnalités d’E/S de NSFileHandle sont particulièrement adaptées aux communications avec socket puisqu’elles fournissent différentes manières d’écouter des connexions, de les accepter et de lire des données de manière asynchronique dans un processus d’arrière-plan. NSFileHandle alerte les objects des nouvelles connections et des données reçues en utilisant des notifications, ce qui est une pratique de programmation familière dans Cocoa.

Communication asynchrone d’arrière-plan signifie que l’on peut taper une commande pour lire dans le socket et qu’elle sera executée dans un processus séparé tandis que le processus principal continuera son travail. La première chose que l’on fait est d’attendre les nouvelles connexions et de les accepter lorsqu’elles arrivent. Le dernier article montrait comment ceci était effectué avec une boucle de type “for” et un appel à accept(). NSFileHandle fournit une fonctionnalité similaire dans la méthode acceptConnectionInBackgroundAndNotify. Lorsque nous invoquons cette méthode, un nouveau processus est lancé et il sera utilisé par le gestionnaire de fichier pour écouter et accepter les nouvelles connexions. Lorsque le gestionnaire de fichier accepte une nouvelle connexion, il poste un message NSFileHandleConnectionAcceptedNotification au centre de notification et continue à écouter les nouvelles requêtes. C’est ainsi que nous acceptons les connexions socket en utilisant NSFileHandle.

L’étape suivante consiste à lire les données à partir du gestionnaire de fichier, ce qui est aussi effectué de manière asynchrone en arrière-plan. Pour indiquer à un gestionnaire de fichier de lire des données, nous invoquons la méthode readInBackgroundAndNotify. Lorsque le gestionnaire de fichier a lu toutes les données qu’il a reçues il postera une notification NSFileHandleReadCompletionNotification au centre de notification. Les objects intéressés par l’obtention de données à partir d’un socket se déclarent pour recevoir cette notification. Lorsqu’une telle notification est postée, l’objet notification passé à l’observateur contient les données que le gestionnaire de fichier a lues. Ces données sont accessibles à partir du dictionnaire userInfo de l’objet notification. L’objet notification de cette notification est le gestionnaire de fichier qui a posté la notification. Nous verrons plus bas comment utiliser cet objet pour initier de nouveau une requête de lecture. Cette notification contient aussi un dictionnaire userInfo qui contient les données réelles qui ont été lues dans un NSFileHandleNotificationDataItem NSData.

Après la lecture, l’écriture. Elle s’effectue simplement avec la méthode writeData:, qui prend un objet NSData contenant les données à envoyer de l’autre côté de la connexion socket. Il n’y a rien d’asynchrone dans writeData:. Elle écrit simplement les données dans le socket et revient.

Pour créer un gestionnaire de fichiers utilisé pour la communication socket, nous utilisons l’initialisateur initWithFileDescriptor:, qui prend comme argument le descripteur de fichier retourné par socket(). Un initialisateur plus général est initWithFileDescriptor:closeOnDealloc:. Il nous permet de spécifier explicitement si oui ou non le socket devra être automatiquement fermé lorsque le gestionnaire de fichier sera libéré. Le comportement par défaut de initWithFileDescriptor: consiste à configurer le gestionnaire de fichier de façon à ce que le socket ne soit pas fermé au moment de sa libération.

Nous allons voir dans un moment comment tout ceci s’imbrique les uns aux autres pour former un programme.

ChatWindowController

La classe ChatWindowController est une sous-classe de NSWindowController et elle est responsable de l’appropriation des fenêtres de chat et des interactions avec ces fenêtres. En plus de la création de cette classe, nous devons créer un nib contenant la fenêtre de chat à proprement parler. ChatWindowController deviendra le File’s Owner de ce nib, et nous allons configurer l’initialisateur de ChatWindowController pour qu’il charge proprement le nib.

ChatWindowController est une classe simple dotée de cinq méthodes et de trois variables d’instance. Pour démarrer, créez une nouvelle sous-classe Objective-C de NSWindowController et nommez la ChatWindowController. La première chose que nous souhaitons faire avant de créer notre interface consiste à configurer l’en-tête de classe puisqu’elle sera requise dans Interface Builder. Le contenu du fichier interface de la classe (ChatWindowController.h) est le suivant :

#import <AppKit/AppKit.h>

@interface ChatWindowController : NSWindowController {
    IBOutlet NSTextView *textView;
    NSFileHandle *fileHandle;
    NSString *myName;
}
- (id)initWithConnection:(NSFileHandle *)aFileHandle myName:(NSString *)me;
- (IBAction)sendMessage:(id)sender;
- (void)receiveMessage:(NSNotification *)notification;
- (void)postMessage:(NSString *)message fromPerson:(NSString *)person;
- (void)windowWillClose:(NSNotification *)notification;
@end

La première méthode de cette classe est initWithConnection:myName:, qui, au travers de aFileHandle, configure la fenêtre de chat avec une connexion au client RCE de qui que ce soit ; elle positionne la variable d’instance myName avec le paramètre myName: de cette méthode. La méthode sendMessage: représente l’action du champ texte dans lequel l’utilisateur tape son message. Lorsque l’utilisateur tape sur la touche Entrée, le message est envoyé à son pair dans cette méthode. La méthode receiveMessage: est enregistrée dans le centre de notification comme celle à invoquer lorsque des données deviennent disponibles dans l’identificateur de fichier fileHandle, qui est connecté au pair. Ensuite, nous avons la méthode postMessage:fromPerson:. Nous l’utilisons pour afficher un message dans la vue texte de la conversation en cours, vue assignée à la variable d’instance d’outlet textView. La dernière méthode est windowWillClose:, qui est une méthode déléguée de NSWindow ; dans Interface Builder nous assignerons ChatWindowController au rôle de délégué de la fenêtre de chat en cours.

L’Interface de la Fenêtre de Chat

Avant de poursuivre avec l’implémentation de ces méthodes, passons dans Interface Builder pour construire notre interface. Dans Interface Builder, créez un nouveau nib à partir du menu File. Dans le dialogue Starting Point sélectionnez Empty dans la liste des modèles de nib. Ajoutez à ce nib une fenêtre à partir de la palette Cocoa-Windows. A cette fenêtre, ajoutez une vue texte à partir de la palettre Cocoa-Data, et ajoutez un champ texte à partir de la palette Cocoa-Views. Arrangez ces deux objets dans la fenêtre de façon à ce que la vue texte soit placée plus haut dans la fenêtre, au dessus du champ texte. Ensuite, sélectionnez ces deux objets et faites en sorte qu’ils forment une sous-vue d’un NSSplitView en sélectionnant Make subviews of->Split View à partir du menu Layout. Arrangez la séparation de façon à ce que la vue supérieure remplisse la fenêtre. Dans l’inspecteur Size (Command-3), changez l’autosizing de la vue de façon à ce que les deux supports intérieurs soient des ressorts. Cela provoquera le redimensionnement de la vue en même temps que celui de la fenêtre. L’image ci-dessous montre à quoi ressemble ma fenêtre de chat.

Copie d'écran
Une fenêtre simple de chat.

Ensuite, nous devons importer l’en-tête de ChatWindowController. Faites ceci en dépposant le fichier ChatWindowController.h à partir de Project Builder dans la fenêtre du nib, ou en sélectionnant Read Files… dans le menu Classes, et en cherchant et sélectionnant ChatWindowController.h dans le navigateur de fichiers. Après que l’interface de classe ait été importée, sélectionnez File’s Owner dans la fenêtre nib et changez sa classe en ChatWindowController à partir de l’inspecteur Custom Class (Command-5). En changeant la classe en ChatWindowController, File’s Owner va prendre la responsabilité de toutes les outlets et de toutes les actions que nous avons créées pour cette classe dans Project Builder. Nous sommes maintenant prêts à établir les connexions entre ChatWindowController et la fenêtre de chat.

D’abord, nous souhaitons que l’action du champ texte soit l’action sendMessage: du File’s Owner. Tirez un cable entre le champ texte et le File’s Owner et établissez la connexion. Ensuite, nous voulons connecter la vue texte de la partie supérieure de la fenêtre à l’outlet textView du File’s Owner. Faites cela en tirant un cable à partir du File’s Owner vers la vue texte et établissez la connexion.

Maintenant, nous souhaitons vérifiez que l’oulet window (qui est définie par la super-classe de ChatWindowController, NSWindowController) du File’s Owner est connectée à la fenêtre de chat. Si ce n’est pas le cas, établissez cette connexion. La dernière chose consiste à faire en sorte que ChatWindowController soit la déléguée de la fenêtre de chat. Faites cela en tirant un cable à partir de la fenêtre vers le File’s Owner et en établissant la connexion avec l’outlet delegate.

Enregistrez le nib sous ChatWindow.nib. Lorsque la fenêtre de sauvegarde apparaît, vous devez naviguer vers le répertoire du projet et enregistrer le fichier dans le répertoire .lproj de votre langage primaire—English.lproj dans mon cas. Après avoir cliqué sur OK, on vous demandera d’ajouter le fichier au projet. Vérifiez que la case à cocher est cochée aux côtés du nom cible de votre projet et cliquez sur le bouton Add. Nous sommes maintenant prêt à implémenter ChatWindowController.

Initialisation et destruction des objets de ChatWindowController

Maintenant, implémentons ChatWindowController. Commençons par initWithConnection:myName:. Cette méthode est implémentée comme suit :

- (id)initWithConnection:(NSFileHandle *)aFileHandle myName:(NSString *)me
{
self = [super initWithWindowNibName:@"ChatWindow"];

if ( self ) {
    fileHandle = [aFileHandle retain];
    myName = [me copy];

    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    [nc addObserver:self
           selector:@selector(receiveMessage:)
               name:NSFileHandleReadCompletionNotification
             object:fileHandle];

    [fileHandle readInBackgroundAndNotify];
}
return self;
}

Nous commeçons par invoquer l’initialisateur de la super-classe, dont le nom est initWithWindowNibName:. A cette méthode, nous passons le nom du nib, ChatWindow, qui contient l’interface qui sera accaparée et gérée par le contrôleur de fenêtre. Cette méthode de NSWindowController passe par les étapes nécessaires au chargement du nib en mémoire. En supposant que tout s’est bien passé dans initWithWindowNibName:, nous assignons maintenant des objets à nos variables d’instance en retenant l’identificateur de fichier passé dans le premier paramètre de l’invocation de la méthode et en copiant la chaîne passée dans le second paramètre.

La raison pour laquelle nous faisons un copy plutôt qu’un retain de la chaîne tient dans le fait que la chaîne passée à l’initialisateur peut être une instance de NSMutableString, qui peut être modifiée par d’autres objets n’étant pas sous le contrôle de ChatWindowController (en vérité, dans notre programme, cela ne sera probablement pas d’actualité). En copiant me au lieu de la retenir, nous préservons la chaîne dans l’état qu’elle était lorsque l’objet ChatWindowController a été initialisé. Ainsi, si me est effectivement une chaîne transformable, tout autre objet pourra la changer dans déranger la valeur de la variable d’instance myName dans ChatWindowController. La raison de cela tient dans le fait que retain ne fait qu’incrémenter simplement le compteur de références à l’objet identifié par le nom, alors que copy fera un nouvel objet identique à l’original (enfin, presque : copy fera une copie non modifiable de l’objet. Si nous souhaitons une copie modifiable, nous devons alors utiliser une méthode appropriée, mutableCopy, qui n’est supportée que par les classes qui se conforme au protocole NSMutableCopying).

Après que les objets aient été passés à l’initialisateur, nous nous déclarons au centre de notification. La notification qui nous intéresse est NSFileHandleReadCompletionNotification, que fileHandle postera au moment où il aura terminé la lecture des nouvelles données. Toute instance de ChatWindowController n’est intéressée que par les notifications postées par son propre objet fileHandle, nous passons donc fileHandle dans l’argument object: pour restreindre le nombre de notifications que ChatWindowController recevra. Si nous avions passé nil ici, alors toutes les instances de ChatWindowController iraient lire les notifications postées par tout identificateur de fichier dans le RCE, ce qui serait problématique si nous avions plusieurs fenêtres de chat ouvertes en même temps. La méthode qui est invoquée en réponse à cette notification est receiveMessage:, dont on a parlé plus haut.

La dernière chose à faire avant de continuer consiste à indiquer à l’identificateur de fichier de commencer à attendre l’arrivée des données reçues et à les lire. Cela est effectué en envoyant un message readDataInBackgroundAndNotify au fileHandle.

dealloc

Au moment de créer l’initialisateur d’une classe, vous devriez toujours créer une méthode dealloc de façon à être sûr que les objets assignés aux variables d’instance et possédées par la classe seront proprement libérés. Dans initWithConnection:myName: nous avons revendiqué la possession de deux objets : fileHandle et myName. A la fois copy et retain ont pour effet d’incrémenter le compteur de références de l’objet. Donc, nous devons envoyer un message release à ces objets dans la méthode dealloc de ChatWindowController :

- (void)dealloc
{
    [fileHandle release];
    [myName release];

    [super dealloc];
}

La dernière chose que nous faisons toujours dans dealloc consiste à envoyer un message dealloc à super de façon que la super-classe ait une chance d’effectuer des opérations similaires de nettoyage.

Il y a quelques manques dans cette méthode dealloc. Un identificateur de fichier apporte une manière de lire et d’écrire des données dans un fichier ou un socket ouvert. Dans notre programme, l’objet fileHandle est le seul lien que nous ayons avec le socket, donc si nous détruisons l’identificateur de fichier nous nous retrouverons avec un socket ouvert que nous ne pourrons pas refermer. Si nous avions gardé une trace du descripteur de fichier socket que nous avons initialement utilisé pour initialiser l’identificateur de fichier, alors nous pourrions fermer le socket en utilisant la fonction close. Mais ce n’est pas le cas, nous devons donc fermer le socket par le biais de l’identificateur de fichier, ce qui s’effectue avec la méthode closeFile de NSFileHandle. Nous invoquons cette méthode immédiatement avant de libérer l’identificateur de fichier, ce qui transforme dealloc ainsi :

- (void)dealloc
{
    [fileHandle closeFile];
    [fileHandle release];
    [myName release]; 

    [super dealloc];
}

Les objets qui ont été déclarés pour recevoir des notifications devraient toujours se désinscrire eux mêmes du centre de notification avant qu’ils ne soient détruits. Si nous ne prenons pas le soin de faire ceci, nous créons des problèmes dans le centre de notification qui essaiera d’envoyer des messages à un objet inexistant. Donc, nous finissons notre implémentation de dealloc comme suit :

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [fileHandle closeFile];
    [fileHandle release];
    [myName release];

    [super dealloc];
}

Envoi et réception de Messages

Avant de parler des méthodes chargées de lire et d’écrire dans l’identificateur de fichier, je voudrais vous montrer l’implémentation de la méthode postMessage:fromPerson:, puisqu’elle est utilisée à la fois par sendMessage: et par receiveMessage:. Cette méthode est utilisée pour afficher un message dans la vue texte et est implémentée comme suit :

- (void)postMessage:(NSString *)message fromPerson:(NSString *)person
{
    NSString *str = [NSString stringWithFormat:@"%@: %@n", person, message];
    NSAttributedString *aStr = [[NSAttributedString alloc] initWithString:str];

    [[textView textStorage] appendAttributedString:aStr];
    [aStr release];
}

Dans cette méthode, tout ce que nous faisons consiste à concaténer les chaînes person et message séparées par un point-virgule, à créer un objet NSAttributedString, et à afficher cette chaîne dans la vue texte. Un caractère de nouvelle ligne est ajouté à la fin de la chaîne de façon à ce que les messages soient disposés sur des lignes séparées.

sendMessage:

Implémentons l’action du champ texte du message, sendMessage:. Cette méthode lit la chaîne dans le champ texte, emballe la représentation UTF8 de la chaîne dans un objet NSData, et écrit cet objet data dans le socket par le biais du fileHandle. Elle ressemble à ceci :

- (IBAction)sendMessage:(id)sender
{
    NSString *message = [NSString stringWithFormat:@"_rce_:%@:%@",
                                  myName, [sender stringValue]];
    NSData *messageData = [NSData dataWithBytes:[message UTF8String]
                                         length:[message length]];
    [fileHandle writeData:messageData];

    [self postMessage:[sender stringValue] fromPerson:@"Me"];
    [sender setStringValue:@""];
}

D’abord, nous construisons la chaîne du message en respectant notre protocole et en utilisant stringWithFormat: de NSString. Comme vous pouvez le voir, nous avons la chaîne identificatrice “_rce_”, le nom de l’émetteur du message et le texte du message, le tout séparé par des virgules. Nous utilisons le constructeur de commodité dataWithBytes:length: de NSData pour créer un objet data contenant le message. Le contenu du data est formé de caractères 8-bit que nous obtenons avec la méthode UTF8String de NSString. Avec cet objet data dans les mains, nous pouvons l’écrire dans le socket en utilisant la méthode writeData: de NSFileHandle. Ensuite, nous invoquons postMessage:fromPerson: pour afficher le message dans la vue de la conversation. Nous passons [sender stringValue] ici plutôt que message puisque message est formatté pour l’application et n’est pas présentable à un oeil humain. Pour finir, nous effaçons le champ texte du message en lui attribuant la valeur d’une chaîne vide.

receiveMessage:

De l’autre côté de la connexion, un client RCE pair attendra l’arrivée des messages. Lorsque des données sont reçues sur le socket, l’identificateur de fichier poste une notification pour alerter les observateurs de ce fait. Le contrôleur de notre fenêtre de chat est un observateur pour le NSFileHandleReadCompletionNotification, et receiveMessage: est invoquée en réponse à cette notification. La méthode receiveMessage: est un peu plus compliquée que sendMessage:—la complexité supplémentaire provient du fait que nous ne souhaitons afficher que les messages qui sont formattés selon le protocole RCE. Dans ce but, nous ajoutons plusieurs contrôles de façon à vérifier que le formattage est correct avant de poster le message dans la fenêtre de chat. En plus, nous devons ajouter un peu de logique à cette méthode pour que le contrôleur de la fenêtre de chat puisse afficher la fenêtre si elle n’est pas déjà visible. Cela sera vrai lorsqu’une conversation démarrera. Jetons un oeil à cette méthode :

- (void)receiveMessage:(NSNotification *)notification
{
    NSData *messageData = [[notification userInfo]
                    objectForKey:NSFileHandleNotificationDataItem];

    if ( [messageData length] == 0 ) {
        [fileHandle readInBackgroundAndNotify];
        return;
    }

    NSString *message = [NSString stringWithUTF8String:[messageData bytes]];
    NSArray *msgComponents = [message componentsSeparatedByString:@":"];

    if ( [msgComponents count] != 3 ) {
        [fileHandle readInBackgroundAndNotify];
        return;
    }

    if ( ![[msgComponents objectAtIndex:0] isEqualToString:@"_rce_"] ) {
        [fileHandle readInBackgroundAndNotify];
        return;
    }

    if ( ![[self window] isVisible] )
        [self showWindow:nil];

    [self postMessage:[msgComponents objectAtIndex:2]
           fromPerson:[msgComponents objectAtIndex:1]];

    [fileHandle readInBackgroundAndNotify];
}

La première et la dernière ligne sont les plus importantes. Comme mentionné auparavant, quand un objet NSFileHandle reçoit un message readInBackgroundAndNotify il se place en arrière plan pour attendre les données. Lorsque les données sont reçues, l’identificateur de fichier lit les données dans le socket et les stocke dans un objet NSData. Il poste alors la notification NSFileHandleReadCompletionNotification. A ce point, receiveMessage: est invoquée et se charge d’obtenir les données à partir de l’identificateur de fichier. L’identificateur de fichier rend les données lues accessibles aux observateurs de notification par le biais du dictionnaire userInfo de la notification dans la clé NSFileHandleNotificationDataItem. Ce que nous voyons dans la première ligne c’est la manière dont nous obtenons cet objet data.

Une fois que receiveMessage: a été invoquée, l’identificateur de fichier n’attend plus de données. Il a fait ce qu’on lui demandait, lire les données et notifier aux autres qu’il l’a fait. Ainsi, une fois que nous avons en main la notification relative à la lecture de données, nous devons dire à l’identificateur de fichier de continuer à écouter l’arrivée de données en réinvoquant readInBackgroundAndNotify. Nous faisons ceci non seulement dans la dernière ligne de la méthode mais à chaque fois que la méthode est sur le point de se terminer, c’est à dire dans les trois instructions conditionnelles.

Dans la première instruction conditionnelle, nous vérifions qu’il y a effectivement des données à manipuler. Les sockets effectuent certaines communications en coulisse qui ne génèrent pas forcément des données consommables mais qui engendreraient une notification. Nous ne souhaitons pas continuer la méthode s’il n’y a aucune donnée à traiter, nous disons donc à l’identificateur de fichier de continuer à attendre des données et nous quittons la méthode (en réalité, stringWithUTF8String: générera une erreur si l’argument est nil).

Ensuite, nous convertissons les données en un objet NSString et analysons les composants de cette chaîne. Un objet NSString est créé à partir des données en utilisant le constructeur de commodité stringWithUTF8String:. En accord avec le protocole, nous séparons les composants de la chaîne collés par des virgules en utilisant la méthode componentsSeparatedByString:. Cette méthode renvoi un tableau contenant les composants sous forme de chaîne.

Le protocole RCE spécifie qu’un message proprement formatté comporte trois composants et que son premier composant est la chaîne “_rce_”. Nous vérifions deux de ces conditions dans les deux instructions conditionnelles suivantes. Si l’un de ces tests échoue, nous indiquons à l’identificateur de fichier de reprendre son attente de données et nous sortons de la méthode. Si le message est proprement formatté, nous postons alors le message dans la vue texte en utilisant postMessage:fromPerson:. Le contenu du message provient du troisième élément du tableau msgComponents, et l’argument fromPerson: provient du second élément de ce tableau. Bien sûr, nous vérifions que la fenêtre est bien visible et nous la faisons apparaître si nécessaire avant de poster le message.

La Méthode Déléguée

La dernière chose à faire avant d’en finir avec cette classe consiste à implémenter la méthode déléguée de NSWindow, windowWillClose:. Dans un moment nous allons voir que Controller ne garde pas trace de tous les contrôleurs de fenêtre qu’il crée, nous n’avons donc aucune manière de les libérer du Controller. En conséquence, le contrôleur de fenêtre est lui-même responsable de sa propre destruction au moment opportun, c’est à dire lorsque la fenêtre est fermée.

C’ets là qu’intervient windowWillClose:. Dans cette méthode nous envoyons simplement un message release au contrôleur de fenêtre, self:

- (void)windowWillClose:(NSNotification *)notification
{
    [self release];
}

Avec cela, notre classe ChatWindowController est complète. Maintenant, nous devons jeter un oeil aux modifications nécessaires à apporter à la classe Controller, dont nous avions commencé l’implémentation dans les paragraphes précédents de cet article.

De retour dans le Contrôleur

Nous avons vu comment implémener la classe ChatWindowController. Il est de la responsabilité de la classe Controller d’instancier cette classe, et ainsi afficher une fenêtre de chat, lorsque deux choses arrivent : l’utilisateur double-clique sur un nom de la liste des contacts ou un autre client RCE initie une conversation en envoyant un premier message. Dans ce but, nous implémentons deux méthodes supplémentaires dans la classe Controller : openNewChatWindowAsChatInitiator: et openNewChatWindowAsMessageReceiver:. Ces deux méthodes, cependant, ne sont pas les seuls changements que nous allons apporter au Controller. Nous devons configurer la vue table de façon à ce qu’elle puisse répondre aux double-clics et nous devons ajouter un peu de code pour monter les sockets et les notifications pour les fonctionnalités serveur de RCE.

awakeFromNib

Précédemment, cette méthode était assignée à la tache très simple d’attribuer une valeur initiale au champ texte du nom de service. Ce que nous souhaitons faire aujourd’hui consiste à régler la cible et l’action double de la vue table des services découverts. Certains éléments d’interface sont dotés d’une action double, qui est une méthode invoquée en réponse à un double-clic. NSTableView est une de ces classes qui envoient cette action lorsqu’une ligne est double-cliquée dans la vue. Pour supporter ce comportement, nous faisons de awakeFromNib la chose suivante :

- (void)awakeFromNib
{
    // Donne une valeur initiale à nameField
    [nameField setStringValue:NSFullUserName()];

    // Règle l'action du double-clic de discoveredServicesList
    [discoveredServicesList setTarget:self];
    SEL actionSel = @selector(openNewChatWindowAsChatInitiator:);
    [discoveredServicesList setDoubleAction:actionSel];
}

Comme vous pouvez le voir, la méthode openNewChatWindowAsChatInitiator: sera invoquée lorsque l’utilisateur double-cliquera sur un nom dans la liste des services connus.

toggleServiceActivation:

Lorsque nous avons abandonné RCE plusieurs articles auparavant, nous avions l’implémentation suivante de la méthode toggleServiceActivation:, qui est l’action du bouton Démarrer/Arrêt un Service.

- (IBAction)toggleServiceActivation:(id)sender
{
    switch ( [sender state] ) {
        case NSOnState:
            [self setupSocket];
            [self setupService];
            [self setupBrowser];

            [nameField setEnabled:NO];
            break;

        case NSOffState:
            [serviceBrowser stop];
            [domainBrowser stop];
            [service stop];

            [nameField setEnabled:YES];
            break;
    }
}

La méthode setupSocket était en fait une méthode qui n’était pas à sa place et aujourd’hui je vais déplacer sa fonctionnalité dans setupService, nous allons donc retirer entièrement setupSocket de Controller. En plus, lorsque nous arrêtons le service, il y a certaines choses que nous devons faire en rapport avec la fermeture du socket serveur, donc, je souhaite placer ce code dans une nouvelle méthode, stopService. Cette méthode prendra aussi soin d’arrêter le service Rendezvous en cours, nous pouvons donc retirer [service stop] de la méthode du dessus. toggleServiceActivation: devrait donc avoir l’implémentation suivante :
<

- (IBAction)toggleServiceActivation:(id)sender
{
    switch ( [sender state] ) {
        case NSOnState:
            [self setupService];
            [self setupBrowser];

            [nameField setEnabled:NO];
            break;

        case NSOffState:
            [self stopService];

            [serviceBrowser stop];
            [domainBrowser stop];

            [nameField setEnabled:YES];
            break;
    }
}

setupService

Auparavant, setupService: était implémentée de la manière suivante :

- (void)setupService
{
    service = [[NSNetService alloc] initWithDomain:@""
                                    type:@"_rce._tcp."
                                    name:[nameField stringValue]
                                    port:12345];
    [service setDelegate:self];
    [service publish];
}

Cela créait un service sur le port numéro 12345. Un changement important que nous devons apporter consiste à laisser le système décider du port sur lequel servir le RCE. En laissant le système décider au moment de l’exécution du port à utiliser pour le serveur RCE, nous pouvons lancer plusieurs instances de RCE en même temps sur la même machine. Si nous avions laisser le port codé en dur nous aurions alors rencontré des problèmes au moment où deux instances de RCE auraient essayé de se connecter au même port. Enfin, nous n’avons pas besoin de connaître le numéro du port attribué au service puisque Rendezvous prendra soin de communiquer tous ces détails aux clients.

Avant de passer à la nouvelle implementation de setupService, nous devons faire en sorte que Controller importe les en-têtes de sockets Unix et l’interface de ChatWindowController. Tout en haut de Controller.m, ajoutez ceci :

#import "ChatWindowController.h"
#import <sys/socket.h>
#import <netinet/in.h>

Et maintenant, jetons un oeil au nouveau setupService::

- (void)setupService
{
    struct sockaddr_in addr;
    int sockfd;

    // Crée un socket
    sockfd = socket( AF_INET, SOCK_STREAM, 0 );

    // Configure sa structure d'adresse
    bzero( &addr, sizeof(struct sockaddr_in));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl( INADDR_ANY );
    addr.sin_port = htons( 0 );

    // Le connecte à une adresse et un port
    bind( sockfd, (struct sockaddr *)&addr, sizeof(struct sockadd));

    // Le met à l'écoute des connexions
    listen( sockfd, 5 );

    // Recherche à quel port le socket a été relié
    int namelen = sizeof(struct sockaddr_in);
    getsockname( sockfd, (struct sockaddr *)&addr, &namelen );

    // Crée un NSFileHandle pour communiquer avec le socket
    listeningSocket = [[NSFileHandle alloc]
                                     initWithFileDescriptor:sockfd];

    // S'enregistre pour obtenir les notifications relatives au socket
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    [nc addObserver:self
        selector:@selector(openNewChatWindowAsMessageReceiver:)
        name:NSFileHandleConnectionAcceptedNotification
        object:nil];

    // Commence à attendre et à accepter les connexions
    [listeningSocket acceptConnectionInBackgroundAndNotify];

    // Règle le service Rendezvous
    service = [[NSNetService alloc] initWithDomain:@""
                                    type:@"_rce._tcp."
                                    name:[nameField stringValue]
                                    port:addr.sin_port];
    [service setDelegate:self];
    [service publish];
}

Nous commençons par créer un nouveau socket en utilisant la fonction socket(). Rien de particulier ici : nous ne faisons que créer un socket et attraper le descripteur de fichier qui nous est retourné pour le placer dans la variable sockfd.

Ensuite, nous configurons la structure de l’adresse du socket, ce qui doit être fait avant d’appeler la fonction bind(). Dans le dernier article sur les réseaux nous avons parlé du rôle que tenait le type struct sockaddr_in, nous n’allons pas nous attarder trop longtemps ici. Nous initialisons d’abord le contenu de la structure à zéro. La famille d’adresse est réglée sur AF_INET, la même famille qui était spécifiée lorsque nous avons créé le socket. Dans les deux lignes suivantes nous spécifions à quelle adresse et à quel port dévrait être relié le socket. En positionnant addr.sin_addr.s_addr à INADDR_ANY, nous indiquons à bind() de relier le socket à n’importe quel adresse de l’hôte plutôt qu’à une adresse spécifique. Dans la ligne qui suit, nous réglons le numéro de port à zéro, ce qui poussera bind() à choisir un port disponible pour le socket.

Après avoir configuré la structure d’adresse, la chose suivante à faire consiste à appeler bind(), puis listen(). bind() associe le socket à un port et une adresse (ou des adresses), et listen() place le socket dans un état prêt à accepter les nouvelles connexions des clients. Encore une fois, ces deux fonctions ont été abordées dans l’article précédent, nous n’en dirons donc pas plus ici.

Récupérer le Numéro de Port

Afin d’initialiser proprement une instance de NSNetService nous devons fournir le numéro de port à partir duquel le service est accessible. Dans notre version originale de RCE, nous avions codé en dur le numéro 12345. Cette fois ci, dans cette nouvelle version, nous avons fait en sorte que bind() sélectionne un port à notre place, ce qui implique que nous avons un peu de travail à effectuer pour récupérer le port que bind() a choisi pour nous.

Pour arriver à cette fin, nous utilisons la fonction getsockname(), qui a la signature suivante :

int getsockname( int s, struct sockaddr *name, int *namelen );

L’appel de getsockname() provoque le remplissage d’une structure de type sockaddr, name, avec les informations d’adresse et de port pour le socket spécifié, s. getsockname() retourne 0 si l’operation réussit; -1 sinon. Le paramètre namelen est utilisé pour deux choses : indiquer à getsockname() la taille de l’espace mémoire pointé par name, puis lorsque getsockname() a fini, récupérer la taille réelle, contenue dans namelen, de la structure d’adresse retournée.

Dans la méthode précédente, nous avons positionné namelen à la taille du type struct sockaddr_in. Dans la ligne suivante, nous effectuons notre appel à getsockname(), passant sockfd en tant que descripteur de socket et l’adresse (mémoire, pas réseau) de addr. Nous réutilisons ici la structure addr : getsockname() va écraser ce qui était précédemment contenu dans cette structure. Après que getsockname() ait fini, addr contient le numéro de port du socket dans son membre sin_port. Plus loin dans la méthode nous voyons que nous passons addr.sin_port au paramètre port:à l’initialisateur du service réseau.

Créer un NSFileHandle, s’enregistrer pour les notifications, commencer à attendre les connexions

La dernière chose à faire avant de créer le service réseau est d’enregistrer l’objet Controller dans le centre de notification de façon à observer la notification NSFileHandleConnectionAcceptedNotification. Nous avons appris plus haut dans l’article que cette notification est celle qui est postée par l’identificateur de fichier socket lorsque le socket sousjacent reçoit et accepte une nouvelle connexion, ce qui arrivera lorsque le serveur de chat recevra un nouveau message de la part d’un autre client. La méthode qui est invoquée en réponse à cette notofication est openNewChatWindowAsMessageReceiver:. Notez que nous passons nil au paramètre object:, ce qui est contraire à ce que nous avions fait dans ChatWindowController. Nous pouvons ici nous en tirer comme ça puisqu’il y a toujours au plus qu’un socket serveur par application RCE et qu’il n’y a pas de danger d’avoir plusieurs identificateurs de fichier posatnt des notifications de connexions acceptées. Finalement, pour démarrer le serveur de chat, nous envoyons un message acceptConnectionInBackgroundAndNotify à l’identificateur de fichier listeningSocket. Cela indiquera au socket qu’il peut écouter et accepter les connexions.

stopService

La méthode stopService est utilisée pour éteindre le socket serveur et arrêter le service réseau. Son implémentation est directe et les opérations que nous avons vues dans la méthode ChatWindowController’s dealloc sont effectuées :

- (void)stopService
{
    [service stop];
    [[NSNotificationCenter defaultCenter] removeObserver:self];

    [listeningSocket closeFile];
    [listeningSocket release];
}

openNewChatWindowAsMessageReceiver:

Maintenant, jetons un oeil à la méthode qui sera invoquée lorsqu’une nouvelle connexion sera établie avec le serveur de chat : openNewChatWindowAsMessageReceiver:. Cette méthode est implémentée de la manière suivante :

- (void)openNewChatWindowAsMessageReceiver:(NSNotification *)notification
{
    NSFileHandle *remoteFH = [[notification userInfo]
                       objectForKey:NSFileHandleNotificationFileHandleItem];

    ChatWindowController *chatWC;
    chatWC = [[ChatWindowController alloc] initWithConnection:remoteFH
                                           myName:[nameField stringValue]];
}

Comme nous le savons, cette méthode est invoquée lorsque notre socket serveur de chat reçoit des données provenant d’un autre client de chat. Dans l’article précédent, nous avons découvert la routine accept(), et comment elle renvoyait un nouveau descripteur de fichier socket pour la connexion au client distant. Ce socket est ce que nous utilisons pour communiquer avec le client de façon à ce que le socket serveur puisse continuer d’écouter l’arrivée de nouvelles demandes de connexion. D’une manière similaire, NSFileHandle va créer un nouvel identificateur de fichier qui sera utilisé pour communiquer avec le socket connecté au client distant. Cela nous permet de continuer à utiliser l’identificateur de fichier existant pour écouter les nouvelles connexions. L’identificateur de fichier nous est passé dans le dictionnaire userInfo de l’objet notification et est accédé en utilisant la clé NSFileHandleNotificationFileHandleItem. Nous assignons cet identificateur de fichier à la variable remoteFH.

Ensuite nous instancions ChatWindowController et l’initialisons avec l’identificateur de fichier distant, remoteFH, et le nom de notre service local. ChatWindowController gèrera le reste, y compris l’ouverture de la fenêtre de chat lorsque le premier message est réellement reçu (cette méthode est invoquée lorsqu’un client établit une connexion au serveur RCE local).

openNewChatWindowAsChatInitiator:

La dernière méthode que nous devons écrire est openNewChatWindowAsChatInitiator:, dont l’implémentation est la suivante :

- (void)openNewChatWindowAsChatInitiator:(id)sender
{
    // Obtient le service distant basé sur le nom sélectionné dans la liste
    NSNetService *remoteService;
    remoteService = [discoveredServices objectAtIndex:[sender selectedRow]];

    // Récupère la structure d'adresse du socket pour le service distant
    NSData *address = [[remoteService addresses] objectAtIndex:0];

    // Crée un socket qui sera utilisé pour se conencter au client distant
    int sockfd = socket( AF_INET, SOCK_STREAM, 0 );
    connect( sockfd, [address bytes], [address length] );

    // Crée un identificateur de fichier pour ce socket
    NSFileHandle *remoteFH;
    remoteFH = [[NSFileHandle alloc] initWithFileDescriptor:sockfd];
    [remoteFH autorelease];

    // Ouvre une fenêtre avec une connexion au client distant
    ChatWindowController *chatWC;
    chatWC = [[ChatWindowController alloc] initWithConnection:remoteFH
                                           myName:[nameField stringValue]];
    [chatWindow showWindow:nil];
}

A la première ligne nous obtenons l’instance NSNetService représentant le client chat auquel nous souhaitons nous connecter. Nous pouvons découvrir l’index de la ligne double-cliquée par l’utilisateur en envoyant un message selectedRow à la vue table qui est le sender de la méthode openChatWindowAsChatIntiator:. Cet index est alors utilisé pour récupérer le service réseau correspondant dans le tableau discoveredServices.

Dans la ligne suivante, nous obtenons la structure d’adresse qui nous indique comment se connecter au socket du service distant. Cela se fait en utilisant la méthode addresses de NSNetService, qui renvoie un NSArray d’objets data, un pour chaque adresse valide pour le service.

L’importante chose que nous effectuons ensuite est de créer un socket en utilisant la fonction socket puis d’établir une connexion à un socket serveur en utilisant la fonction connect. Pour se connecter à un socket distant nous devons fournir l’adresse du socket auquel nous souhaitons nous connecter. Cette information est la même que celle retournée par un message addresses envoyé à l’instance du service distant. Pour faire accéder un pointeur à la sockaddr struct réelle nous utilisons la méthode bytes de NSData, et la taille de la structure sockaddr est obtenue avec la méthode length.

Après avoir créé et connecté le socket, nous créons un NSFileHandle qui sera notre interface d’E/S au socket. Cet identificateur de fichier est initialisé par la méthode initWithFileDescriptor:, à laquelle nous fournissons le descripteur de fichier socket retourné par socket. Notez que nous envoyons à cet identificateur de fichier une méthode autorelease puisque nous ne sommes pas intéressé par le fait de le posséder : nous le fournissons juste au contrôleur de fenêtre de chat qui le retiendra avec un retain dans la méthode d’initialisation.

Finalement, pour terminer cette méthode, nous avons trois lignes pour créer et afficher la fenêtre de chat. Nous passons remoteFH en tant qu’identificateur de fichier de connexion, et comme avant, nous passons [nameField stringValue] dans le paramètre myName:.

C’est parti

Voici enfin le moment de compiler le code et de le lancer. Nous aurons encore à faire une ou deux choses pour être sûr que tout se passera correctement.

La première chose à faire consiste à vérifier que tout démarre correctement lorsque vous entamer une conversation. Au moment de cliquer sur le bouton “Démarrer le Service de Chat” vous verrez une série de messages sur la sortie standard indiquant ce qui se passe avec le service réseau. Si le service a démarré correctement, vous devriez voir votre nom dans la liste.

lsof

Ensuite, nous souhaitons vérifier que le socket serveur a été créé proprement. Nous faisons cette vérification en utilisant le programme lsof, un utilitaire à ligne de commande listant des informations sur les fichiers ouverts, y compris les sockets, utilisés par les applications. Nous pouvons nous servir de ce programme pour en apprendre plus sur la manière dont RCE—ou tout autre programme—utilise les sockets. Le lancement de lsof renvoie des informations sur les fichiers ouverts par tous les processus de votre machine. Voici un exemple de ce qu’il sait d’iTunes (j’ai du faire des coupures toutes les trois lignes. Le “” de chaque ligne ne fait pas partie de l’affichage de lsof ) :

[southpark:~] mike% lsof
[...lots of open files...]
iTunes     2521 mike   14u  inet 0x0238dfac        0t0      TCP
                            *:3689 (LISTEN)
iTunes     2521 mike   16u  VREG       14,2    3939857   433783
                            / -- iTunes 4 Music Library
iTunes     2521 mike   17r  VREG       14,2    7165777   252967
                            /Users/mike/Music/Seefeel/Quique/Industrious.mp3
[...and it continues on...]

Ce ne sont que trois des 22 descripteurs de fichier ouvert que lsof a trouvé pour iTunes. Dans la première ligne, nous voyons le socket serveur auquel les gens se connectent pour accéder à mes musiques partagées. Dans la ligne suivante, nous avons le fichier audiothèque d’iTunes et, dans la dernière ligne, une référence au morceau que j’écoutais au moment où j’ai lancé lsof.

Pour trier les informations fournies par lsof de manière à n’avoir que celles en rapport avec les sockets RCE, j’ai du taper ceci :

[southpark:~] mike% lsof | grep RCE | grep inet

Le premier grep filtre tout sauf les lignes de texte contenant RCE (le nom du programme, analogue à iTunes) et, le second grep filtre les lignes RCE de façon à ne garder que les lignes relatives aux sockets. Si vous lancez cette commande dans le Terminal, vous devriez voir une ligne qui identifie le socket serveur RCE à l’écoute, qui en ce qui me concerne était :

[southpark:~] mike% lsof | grep RCE | grep inet
RCE       11554 mike    8u  inet 0x03564c9c        0t0      TCP *:51604 (LISTEN)

Cela nous indique que le RCE a proprement créé son socket serveur et qu’il écoute le port 51604. L’astérisque précédent le numéro de port signifie que le socket est à l’écoute de toutes les adresses de la machine (c’est à dire : localhost, southpark.local., etc).

Lancer deux RCE

Essayons maintenant de lancer deux instances de RCE et d’établir une conversation avec nous même. Faites une copie de RCE en faisant un option-déplacement de RCE.app à partir du groupe Products de Project Builder vers le Bureau. Lancez cette nouvelle copie de RCE avec un nom de service différent que celui par défaut et lancez RCE à partir de Project Builder. Après avoir lancé les deux services, vous devriez voir deux noms différents dans la liste des contacts. A partir d’un de ces contacts, sélectionnez le nom de l’autre client RCE et double-cliquez le. Après avoir tapé un message dans le champ texte, vous devriez voir une fenêtre apparaître contenant le message. (Vous aurez peut-être à déplacer les fenêtres puisque l’emplacement est spécifié dans Interface Builder et qu’il sera le même pour les deux instances de RCE).

La Fin

Vous voila avec un programme de chat. Heureusement, tout s’est passé comme prévu. Cela prendra peut-être pas mal de temps et d’effort sachant que ce que nous avons fait aujourd’hui était quelque peu compliqué. Si vous avez besoin d’aide, téléchargez le projet complet ici.

Textes originaux en anglais sur O’Reilly : Networking in Cocoa par Mike Beam

Thierry Programmation Cocoa , , , ,

  1. Pas encore de commentaire
  1. Pas encore de trackbacks
Vous devez être identifié pour poster un commentaire