Chat sur iOS: Ă  l'aide de sockets


Image créée par rawpixel.com

Dans cet article, nous allons descendre dans la couche TCP et découvrir les sockets et les outils de Core Foundation en développant une application de chat.

Temps de lecture estimé: 25 minutes.

Pourquoi des sockets?


Vous vous demandez peut-ĂȘtre: "Pourquoi devrais-je aller un niveau plus bas que URLSession ?" Si vous ĂȘtes assez intelligent et ne posez pas cette question, passez directement Ă  la section suivante.

La réponse pour pas si intelligent
Grande question! Le fait est que l'utilisation de URLSession est basée sur le protocole HTTP , c'est-à-dire que la communication se produit dans le style de demande-réponse , approximativement comme suit:

  • demander au serveur des donnĂ©es au format JSON
  • obtenir ces donnĂ©es, traiter, afficher, etc.

Mais que se passe-t-il si nous avons besoin d'un serveur de sa propre initiative pour transférer des données vers votre application? Ici, HTTP est sans travail.

Bien sĂ»r, nous pouvons tirer en permanence le serveur et voir s'il y a des donnĂ©es pour nous (aka polling ). Ou nous pouvons ĂȘtre plus sophistiquĂ©s et utiliser des sondages longs . Mais toutes ces bĂ©quilles sont lĂ©gĂšrement inappropriĂ©es dans ce cas.

AprÚs tout, pourquoi vous limiter au paradigme demande-réponse s'il correspond un peu moins à rien à notre tùche?

Dans ce guide, vous apprendrez à plonger à un niveau d'abstraction inférieur et à utiliser directement SOCKETS dans l'application de chat.

Au lieu de vérifier le serveur pour les nouveaux messages, notre application utilisera des flux qui restent ouverts pendant la session de chat.

Pour commencer


Téléchargez le matériel source . Il existe une application client fictive et un serveur simple écrit en Go .

Vous n'avez pas besoin d'écrire dans Go, mais vous devrez exécuter l'application serveur pour que les applications clientes puissent s'y connecter.

Lancer l'application serveur


Les matĂ©riaux source ont Ă  la fois une application compilĂ©e et une source. Si vous avez une paranoĂŻa saine et que vous ne faites pas confiance au code compilĂ© par quelqu'un d'autre, vous pouvez compiler le code source vous-mĂȘme.

Si vous ĂȘtes courageux, ouvrez Terminal , allez dans le rĂ©pertoire avec les documents tĂ©lĂ©chargĂ©s et exĂ©cutez la commande:

sudo ./server

Lorsque vous y ĂȘtes invitĂ©, entrez votre mot de passe. AprĂšs cela, vous devriez voir un message

Écoute sur 127.0.0.1:80.

Remarque: l'application serveur démarre en mode privilégié (la commande «sudo») car elle écoute sur le port 80. Tous les ports avec des nombres inférieurs à 1024 nécessitent un accÚs spécial.

Votre serveur de chat est prĂȘt! Vous pouvez passer Ă  la section suivante.

Si vous souhaitez compiler vous-mĂȘme le code source du serveur,
dans ce cas, vous devez installer Go Ă  l' aide de Homebrew .

Si vous n'avez pas Homebrew, vous devez d'abord l'installer. Ouvrez Terminal et collez-y la ligne suivante:

/usr/bin/ruby -e \
"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"


Utilisez ensuite cette commande pour installer Go:

brew install go

À la fin, allez dans le rĂ©pertoire avec les matĂ©riaux source tĂ©lĂ©chargĂ©s et compilez le code source de l'application serveur:

go build server.go

Enfin, vous pouvez démarrer le serveur avec la commande au début de cette section .

Nous regardons ce que nous avons chez le client


Ouvrez maintenant le projet DogeChat , compilez-le et voyez ce qu'il y a.



Comme vous pouvez le voir, DogeChat vous permet dĂ©sormais de saisir un nom d'utilisateur et d'accĂ©der Ă  la section de chat elle-mĂȘme.

Il semble que le développeur de ce projet ne savait pas comment faire un chat. Donc, tout ce que nous avons est une interface utilisateur et une navigation de base. Nous allons écrire une couche réseau. Hourra!

Créer une salle de chat


Pour accéder directement au développement, accédez à ChatRoomViewController.swift . Il s'agit d'un contrÎleur de vue qui peut recevoir du texte saisi par l'utilisateur et afficher les messages reçus dans une vue de table.

Comme nous avons un ChatRoomViewController , il est logique de développer une classe ChatRoom qui fera tout le travail.

Réfléchissons à ce que la nouvelle classe offrira:

  • ouvrir une connexion Ă  l'application serveur;
  • connecter un utilisateur avec le nom spĂ©cifiĂ© par lui au chat;
  • envoyer et recevoir des messages;
  • fermeture de la connexion Ă  la fin.

Maintenant que nous savons ce que nous voulons de cette classe, appuyez sur Commande-N , sélectionnez Fichier Swift et appelez-le ChatRoom .

Création de flux d'E / S


Remplacez le contenu de ChatRoom.swift par ceci:

 import UIKit class ChatRoom: NSObject { //1 var inputStream: InputStream! var outputStream: OutputStream! //2 var username = "" //3 let maxReadLength = 4096 } 

Ici, nous définissons la classe ChatRoom et déclarons les propriétés dont nous avons besoin.

  1. Nous définissons d'abord les flux d'entrée / sortie. Les utiliser en paire nous permettra de créer une connexion socket entre l'application et le serveur de chat. Bien sûr, nous enverrons des messages en utilisant le flux de sortie et recevrons en utilisant le flux d'entrée.
  2. Ensuite, nous définissons le nom d'utilisateur.
  3. Et enfin, nous définissons la variable maxReadLength, qui limite la longueur maximale d'un seul message.

Accédez maintenant au fichier ChatRoomViewController.swift et ajoutez cette ligne à la liste de ses propriétés:

 let chatRoom = ChatRoom() 

Maintenant que nous avons créé la structure de base de la classe, il est temps de faire la premiÚre des tùches prévues: ouvrir la connexion entre l'application et le serveur.

Connexion ouverte


Nous revenons à ChatRoom.swift et ajoutons cette méthode pour les définitions de propriétés:

 func setupNetworkCommunication() { // 1 var readStream: Unmanaged<CFReadStream>? var writeStream: Unmanaged<CFWriteStream>? // 2 CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, "localhost" as CFString, 80, &readStream, &writeStream) } 

Voici ce que nous faisons ici:

  1. nous définissons d'abord deux variables pour les flux de socket sans utiliser la gestion automatique de la mémoire
  2. puis nous, en utilisant ces mĂȘmes variables, crĂ©ons directement des flux qui sont liĂ©s Ă  l'hĂŽte et au numĂ©ro de port.

La fonction a quatre arguments. Le premier est le type d'allocateur de mĂ©moire que nous utiliserons lors de l'initialisation des threads. Vous devez utiliser kCFAllocatorDefault , bien qu'il existe d'autres options possibles dans le cas oĂč vous souhaitez modifier le comportement des threads.

Note du traducteur
La documentation de la fonction CFStreamCreatePairWithSocketToHost indique: utilisez NULL ou kCFAllocatorDefault . Et la description de kCFAllocatorDefault indique qu'il s'agit d'un synonyme de NULL . Le cercle est fermé!

Ensuite, nous définissons le nom d'hÎte. Dans notre cas, nous nous connectons au serveur local. Si votre serveur est situé à un autre endroit, vous pouvez définir son adresse IP.

Ensuite, le numéro de port que le serveur écoute.

Enfin, nous transmettons des pointeurs à nos flux d'E / S afin que la fonction puisse les initialiser et les connecter aux flux qu'elle crée.

Maintenant que nous avons les flux initialisés, nous pouvons enregistrer des liens vers eux en ajoutant ces lignes à la fin de la méthode setupNetworkCommunication () :

 inputStream = readStream!.takeRetainedValue() outputStream = writeStream!.takeRetainedValue() 

L'utilisation de takeRetainedValue () appliquĂ©e Ă  un objet non gĂ©rĂ© nous permet de conserver une rĂ©fĂ©rence Ă  celui-ci et, en mĂȘme temps, d'Ă©viter de futures fuites de mĂ©moire. Maintenant, nous pouvons utiliser nos fils oĂč nous voulons.

Nous devons maintenant ajouter ces threads à la boucle d'exécution afin que notre application traite correctement les événements réseau. Pour ce faire, ajoutez ces deux lignes à la fin de setupNetworkCommunication () :

 inputStream.schedule(in: .current, forMode: .common) outputStream.schedule(in: .current, forMode: .common) 

Il est enfin temps de naviguer! Pour commencer, ajoutez ceci à la toute fin de la méthode setupNetworkCommunication () :

 inputStream.open() outputStream.open() 

Nous avons maintenant une connexion ouverte entre notre application client et serveur.

Nous pouvons compiler et exĂ©cuter notre application, mais vous ne verrez pas encore de changements, car mĂȘme si nous ne faisons rien avec notre connexion client-serveur.

Connectez-vous au chat


Maintenant que nous avons une connexion établie avec le serveur, il est temps de commencer à faire quelque chose! Dans le cas du chat, vous devez d'abord vous présenter, puis envoyer des messages aux interlocuteurs.

Cela nous amĂšne Ă  une conclusion importante: comme nous avons deux types de messages, nous devons en quelque sorte les distinguer.

Protocole de chat


L'un des avantages de l'utilisation de la couche TCP est que nous pouvons définir notre propre «protocole» de communication.

Si nous utilisions HTTP, nous aurions besoin d'utiliser ces diffĂ©rents mots GET , PUT , PATCH . Nous aurions besoin de former des URL et d'utiliser les bons en-tĂȘtes et tout cela.

Nous n'avons que deux types de messages. Nous enverrons

iam:Luke

pour entrer dans le chat et vous présenter.

Et nous enverrons

msg:Hey, how goes it, man?

pour envoyer un message de discussion à tous les répondants.

C'est trÚs simple, mais absolument sans principes, alors n'utilisez pas cette méthode dans les projets critiques.

Nous savons maintenant ce que notre serveur attend et nous pouvons écrire une méthode dans la classe ChatRoom qui permettra à l'utilisateur de se connecter au chat. Le seul argument est le surnom de l'utilisateur.

Ajoutez cette méthode dans ChatRoom.swift :

 func joinChat(username: String) { //1 let data = "iam:\(username)".data(using: .utf8)! //2 self.username = username //3 _ = data.withUnsafeBytes { guard let pointer = $0.baseAddress?.assumingMemoryBound(to: UInt8.self) else { print("Error joining chat") return } //4 outputStream.write(pointer, maxLength: data.count) } } 

  1. D'abord, nous formons notre message en utilisant notre propre «protocole»
  2. Enregistrez le nom pour référence future.
  3. withUnsafeBytes (_ :) fournit un moyen pratique de travailler avec un pointeur non sécurisé à l'intérieur d'une fermeture.
  4. Enfin, nous envoyons notre message au flux de sortie. Cela peut sembler plus compliqué que ce à quoi vous pourriez vous attendre, mais écrire (_: maxLength :) utilise le pointeur non sécurisé créé à l'étape précédente.

Maintenant, notre mĂ©thode est prĂȘte, ouvrez ChatRoomViewController.swift et ajoutez un appel Ă  cette mĂ©thode Ă  la fin de viewWillAppear (_ :) .

 chatRoom.joinChat(username: username) 

Maintenant, compilez et exécutez l'application. Entrez votre surnom et appuyez sur Retour pour voir ...



... que rien n'a encore changé!

Attendez, ça va! AccĂ©dez Ă  la fenĂȘtre du terminal. LĂ , vous verrez le message que Vasya a rejoint ou quelque chose comme ça si votre nom n'est pas Vasya.

C'est trÚs bien, mais ce serait bien d'avoir une indication d'une connexion réussie sur l'écran de votre téléphone.

Répondre aux messages entrants


Le serveur envoie des messages de jonction client à tous ceux qui sont dans le chat, y compris vous. Heureusement, notre application a déjà tout pour afficher les messages entrants sous forme de cellules dans le tableau des messages dans ChatRoomViewController .

Tout ce que vous avez à faire est d'utiliser inputStream pour «intercepter» ces messages, les convertir en instances de la classe Message et les transmettre à la table pour affichage.

Pour pouvoir répondre aux messages entrants, vous avez besoin de ChatRoom pour se conformer au protocole StreamDelegate .

Pour ce faire, ajoutez cette extension au bas du fichier ChatRoom.swift :

 extension ChatRoom: StreamDelegate { } 

Déclarez maintenant qui deviendra délégué à inputStream.

Ajoutez cette ligne à la méthode setupNetworkCommunication () juste avant les appels à planifier (dans: forMode :):

 inputStream.delegate = self 

Ajoutez maintenant l'implémentation de la méthode stream (_: handle :) à l'extension:

 func stream(_ aStream: Stream, handle eventCode: Stream.Event) { switch eventCode { case .hasBytesAvailable: print("new message received") case .endEncountered: print("The end of the stream has been reached.") case .errorOccurred: print("error occurred") case .hasSpaceAvailable: print("has space available") default: print("some other event...") } } 

Nous traitons les messages entrants


Nous sommes donc prĂȘts Ă  commencer Ă  traiter les messages entrants. L'Ă©vĂ©nement qui nous intĂ©resse est .hasBytesAvailable , qui indique qu'un message entrant est arrivĂ©.

Nous allons écrire une méthode qui traite ces messages. Sous la méthode nouvellement ajoutée, nous écrivons ce qui suit:

 private func readAvailableBytes(stream: InputStream) { //1 let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: maxReadLength) //2 while stream.hasBytesAvailable { //3 let numberOfBytesRead = inputStream.read(buffer, maxLength: maxReadLength) //4 if numberOfBytesRead < 0, let error = stream.streamError { print(error) break } // Construct the Message object } } 

  1. Nous définissons le tampon dans lequel nous lirons les octets entrants.
  2. Nous tournons en boucle, tandis que dans le flux d'entrée, il y a quelque chose à lire.
  3. Nous appelons read (_: maxLength :), qui lit les octets du flux et les place dans le tampon.
  4. Si l'appel a renvoyé une valeur négative, nous renvoyons une erreur et quittons la boucle.

Nous devons appeler cette méthode dÚs que nous avons des données dans le flux entrant, alors allez à l' instruction switch dans la méthode stream (_: handle :) , trouvez le commutateur .hasBytesAvailable et appelez cette méthode immédiatement aprÚs l'instruction print:

 readAvailableBytes(stream: aStream as! InputStream) 

À cet endroit, nous avons un tampon prĂ©parĂ© des donnĂ©es reçues!

Mais nous devons encore transformer ce tampon en contenu de la table des messages.

Placez cette méthode sur readAvailableBytes (stream :) .

 private func processedMessageString(buffer: UnsafeMutablePointer<UInt8>, length: Int) -> Message? { //1 guard let stringArray = String( bytesNoCopy: buffer, length: length, encoding: .utf8, freeWhenDone: true)?.components(separatedBy: ":"), let name = stringArray.first, let message = stringArray.last else { return nil } //2 let messageSender: MessageSender = (name == self.username) ? .ourself : .someoneElse //3 return Message(message: message, messageSender: messageSender, username: name) } 

Tout d'abord, nous initialisons String en utilisant le tampon et la taille que nous transmettons à cette méthode.

Le texte sera en UTF-8, Ă  la fin, nous libĂ©rerons le tampon et diviserons le message par le symbole ':' pour sĂ©parer le nom de l'expĂ©diteur et le message lui-mĂȘme.

Nous analysons maintenant si ce message provient d'un autre participant. Sur le produit, vous pouvez créer quelque chose comme un jeton unique, cela suffit pour la démo.

Enfin, de toute cette économie, nous formons une instance de Message et la renvoyons.

Pour utiliser cette méthode, ajoutez le if-let suivant à la fin de la boucle while dans la méthode readAvailableBytes (stream :) , immédiatement aprÚs le dernier commentaire:

 if let message = processedMessageString(buffer: buffer, length: numberOfBytesRead) { // Notify interested parties } 

Maintenant, tout est prĂȘt Ă  passer Ă  quelqu'un Message ... Mais Ă  qui?

Créer le protocole ChatRoomDelegate


Nous devons donc informer ChatRoomViewController.swift du nouveau message, mais nous n'avons pas de lien vers celui-ci. Puisqu'il contient un lien ChatRoom fort, nous pouvons tomber dans le piĂšge d'un cycle de liens fort.

C'est l'endroit idéal pour créer un protocole de délégué. ChatRoom ne se soucie pas de savoir qui a besoin de connaßtre les nouveaux messages.

En haut de ChatRoom.swift, ajoutez une nouvelle définition de protocole:

 protocol ChatRoomDelegate: class { func received(message: Message) } 

Maintenant à l'intérieur de la classe ChatRoom, ajoutez un lien faible pour stocker qui deviendra le délégué:

 weak var delegate: ChatRoomDelegate? 

Ajoutons maintenant la méthode readAvailableBytes (stream :) , en ajoutant la ligne suivante à l'intérieur de la construction if-let, sous le dernier commentaire de la méthode:

 delegate?.received(message: message) 

Revenez à ChatRoomViewController.swift et ajoutez l'extension de classe suivante, qui garantit la conformité avec le protocole ChatRoomDelegate , immédiatement aprÚs MessageInputDelegate:

 extension ChatRoomViewController: ChatRoomDelegate { func received(message: Message) { insertNewMessageCell(message) } } 

Le projet d'origine contient déjà le nécessaire, donc insertNewMessageCell (_ :) acceptera votre message et affichera la cellule correcte dans la vue de table.

Affectez maintenant le contrÎleur de vue en tant que délégué en l'ajoutant à viewWillAppear (_ :) immédiatement aprÚs avoir appelé super.viewWillAppear ()

 chatRoom.delegate = self 

Maintenant, compilez et exécutez l'application. Saisissez un nom et appuyez sur Retour.



Vous verrez une cellule sur votre connexion au chat. Hourra, vous avez réussi à envoyer un message au serveur et à en recevoir une réponse!

Publier des messages


Maintenant que ChatRoom peut envoyer et recevoir des messages, il est temps de donner à l'utilisateur la possibilité d'envoyer ses propres phrases.

Dans ChatRoom.swift, ajoutez la méthode suivante à la fin de la définition de classe:

 func send(message: String) { let data = "msg:\(message)".data(using: .utf8)! _ = data.withUnsafeBytes { guard let pointer = $0.baseAddress?.assumingMemoryBound(to: UInt8.self) else { print("Error joining chat") return } outputStream.write(pointer, maxLength: data.count) } } 

Cette méthode est similaire à joinChat (username :) , que nous avons écrit plus tÎt, sauf qu'elle a le préfixe msg devant le texte (pour indiquer qu'il s'agit d'un vrai message de discussion).

Puisque nous voulons envoyer des messages par le bouton Envoyer , nous revenons Ă  ChatRoomViewController.swift et y trouvons MessageInputDelegate .

Ici, nous voyons la méthode sendWasTapped (message :) vide. Pour envoyer un message, envoyez-le à chatRoom:

 chatRoom.send(message: message) 

En fait, c’est tout! Étant donnĂ© que le serveur recevra le message et le transmettra Ă  tout le monde, ChatRoom sera informĂ© du nouveau message de la mĂȘme maniĂšre que lors de la participation au chat.

Compilez et exécutez l'application.



Si vous n'avez personne avec qui discuter, lancez une nouvelle fenĂȘtre de terminal et entrez:

nc localhost 80

Cela vous connectera au serveur. Vous pouvez maintenant vous connecter au chat en utilisant le mĂȘme "protocole":

iam:gregg

Et donc - envoyez un message:

msg:Ay mang, wut's good?



Félicitations, vous avez écrit un client pour le chat!

Nous nous nettoyons


Si vous avez déjà développé des applications qui lisent / écrivent activement des fichiers, sachez que les bons développeurs ferment les fichiers lorsqu'ils ont fini de travailler avec eux. Le fait est que la connexion via le socket est fournie par le descripteur de fichier. Cela signifie qu'à la fin du travail, vous devez le fermer, comme tout autre fichier.

Pour ce faire, ajoutez la méthode suivante à ChatRoom.swift aprÚs avoir défini send (message :) :

 func stopChatSession() { inputStream.close() outputStream.close() } 

Comme vous l'avez probablement deviné, cette méthode ferme les threads afin que vous ne puissiez plus recevoir et envoyer de messages. De plus, les threads sont supprimés de la boucle d'exécution dans laquelle nous les avons précédemment placés.

Ajoutez un appel à cette méthode dans la section .endEncountered de l' instruction switch à l' intérieur de stream (_: handle :) :

 stopChatSession() 

Revenez ensuite Ă  ChatRoomViewController.swift et faites de mĂȘme dans viewWillDisappear (_ :) :

 chatRoom.stopChatSession() 

Voilà! Maintenant c'est sûr!

Conclusion


Maintenant que vous maßtrisez les bases de la mise en réseau avec des sockets, vous pouvez approfondir vos connaissances.

Prises UDP


Cette application est un exemple de communication réseau utilisant TCP, qui garantit la livraison des paquets à destination.

Cependant, vous pouvez utiliser des sockets UDP. Ce type de connexion ne garantit pas la livraison des colis Ă  leur destination, mais il est beaucoup plus rapide.

Ceci est particuliÚrement utile dans les jeux. Avez-vous déjà connu un décalage? Cela signifiait que votre connexion était mauvaise et que de nombreux paquets UDP étaient perdus.

Websockets


Une autre alternative au HTTP dans les applications est une technologie appelée sockets Web.

Contrairement aux sockets TCP classiques, les sockets Web utilisent HTTP pour Ă©tablir la communication. Avec leur aide, vous pouvez obtenir la mĂȘme chose qu'avec des prises ordinaires, mais avec confort et sĂ©curitĂ©, comme dans un navigateur.

Source: https://habr.com/ru/post/fr467909/


All Articles