Chatten Sie unter iOS: Verwenden von Sockets


Bild erstellt von rawpixel.com

In dieser Veröffentlichung gehen wir auf die TCP-Ebene ein und lernen am Beispiel der Entwicklung einer Chat-Anwendung die Sockets und Tools von Core Foundation kennen.

Geschätzte Lesezeit: 25 Minuten.

Warum Steckdosen?


Sie fragen sich vielleicht: "Warum sollte ich eine Stufe niedriger als URLSession gehen ?" Wenn Sie klug genug sind und diese Frage nicht stellen, fahren Sie direkt mit dem nächsten Abschnitt fort.

Die Antwort für nicht so klug
Gute Frage! Tatsache ist, dass die Verwendung von URLSession auf dem HTTP-Protokoll basiert, dh die Verbindung erfolgt im Stil der Anforderungs-Antwort , ungefähr wie folgt:

  • Fordern Sie vom Server einige Daten im JSON-Format an
  • Holen Sie sich diese Daten, verarbeiten, anzeigen, etc.

Was aber, wenn wir von sich aus einen Server benötigen, um Daten an Ihre Anwendung zu übertragen? Hier ist HTTP arbeitslos.

Natürlich können wir den Server kontinuierlich ziehen und prüfen, ob Daten für uns vorhanden sind (auch als Polling bezeichnet ). Oder wir können anspruchsvoller sein und Langzeitabfragen verwenden . Aber all diese Krücken sind in diesem Fall etwas ungeeignet.

Warum sollten Sie sich schließlich auf das Anforderungs-Antwort-Paradigma beschränken, wenn es etwas weniger als nichts zu unserer Aufgabe passt?

In diesem Handbuch erfahren Sie, wie Sie in eine niedrigere Abstraktionsebene eintauchen und SOCKETS direkt in der Chat-Anwendung verwenden.

Anstatt den Server auf neue Nachrichten zu überprüfen, verwendet unsere Anwendung Streams, die während der Chat-Sitzung geöffnet bleiben.

Erste Schritte


Laden Sie die Quellmaterialien herunter. Es gibt eine Schein-Client-Anwendung und einen einfachen Server, der in Go geschrieben ist .

Sie müssen nicht in Go schreiben, sondern müssen die Serveranwendung ausführen, damit Clientanwendungen eine Verbindung dazu herstellen können.

Starten Sie die Serveranwendung


Die Quellmaterialien haben sowohl eine kompilierte Anwendung als auch eine Quelle. Wenn Sie eine gesunde Paranoia haben und dem kompilierten Code eines anderen nicht vertrauen, können Sie den Quellcode selbst kompilieren.

Wenn Sie mutig sind, öffnen Sie Terminal , wechseln Sie in das Verzeichnis mit den heruntergeladenen Materialien und führen Sie den folgenden Befehl aus:

sudo ./server

Wenn Sie dazu aufgefordert werden, geben Sie Ihr Passwort ein. Danach sollten Sie eine Nachricht sehen

Hören auf 127.0.0.1:80.

Hinweis: Die Serveranwendung wird im privilegierten Modus (Befehl "sudo") gestartet, da sie Port 80 überwacht. Alle Ports mit Nummern unter 1024 erfordern einen speziellen Zugriff.

Ihr Chat-Server ist bereit! Sie können mit dem nächsten Abschnitt fortfahren.

Wenn Sie den Server-Quellcode selbst kompilieren möchten,
In diesem Fall müssen Sie Go mit Homebrew installieren.

Wenn Sie kein Homebrew haben, müssen Sie es zuerst installieren. Öffnen Sie Terminal und fügen Sie dort die folgende Zeile ein:

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


Verwenden Sie dann diesen Befehl, um Go zu installieren:

brew install go

Wechseln Sie am Ende in das Verzeichnis mit den heruntergeladenen Quellmaterialien und kompilieren Sie den Quellcode der Serveranwendung:

go build server.go

Schließlich können Sie den Server mit dem Befehl am Anfang dieses Abschnitts starten.

Wir schauen uns an, was wir im Kunden haben


Öffnen Sie nun das DogeChat- Projekt, kompilieren Sie es und sehen Sie, was da ist.



Wie Sie sehen können, können Sie mit DogeChat jetzt einen Benutzernamen eingeben und zum Chat-Bereich selbst gehen.

Es scheint, dass der Entwickler dieses Projekts keine Ahnung hatte, wie man einen Chat macht. Wir haben also nur eine grundlegende Benutzeroberfläche und Navigation. Wir werden eine Netzwerkschicht schreiben. Hurra!

Erstellen Sie einen Chatraum


Um direkt zur Entwicklung zu gelangen, gehen Sie zu ChatRoomViewController.swift . Dies ist ein Ansichts-Controller, der vom Benutzer eingegebenen Text empfangen und empfangene Nachrichten in einer Tabellenansicht anzeigen kann.

Da wir einen ChatRoomViewController haben , ist es sinnvoll, eine ChatRoom- Klasse zu entwickeln, die die ganze harte Arbeit erledigt .

Lassen Sie uns darüber nachdenken, was die neue Klasse bieten wird:

  • Öffnen einer Verbindung zur Serveranwendung;
  • Verbinden eines Benutzers mit dem von ihm angegebenen Namen mit dem Chat;
  • Senden und Empfangen von Nachrichten;
  • Schließen der Verbindung am Ende.

Nachdem wir wissen, was wir von dieser Klasse wollen, drücken Sie Befehlstaste -N , wählen Sie Swift File und nennen Sie es ChatRoom .

E / A-Streams erstellen


Ersetzen Sie den Inhalt von ChatRoom.swift durch Folgendes :

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

Hier definieren wir die ChatRoom- Klasse und deklarieren die Eigenschaften, die wir benötigen.

  1. Zuerst definieren wir die Eingabe- / Ausgabestreams. Wenn wir sie als Paar verwenden, können wir eine Socket-Verbindung zwischen der Anwendung und dem Chat-Server herstellen. Natürlich werden wir Nachrichten über den Ausgabestream senden und über den Eingabestream empfangen.
  2. Als nächstes definieren wir den Benutzernamen.
  3. Und schließlich definieren wir die Variable maxReadLength, die die maximale Länge einer einzelnen Nachricht begrenzt.

Gehen Sie nun zur Datei ChatRoomViewController.swift und fügen Sie diese Zeile zur Liste ihrer Eigenschaften hinzu:

 let chatRoom = ChatRoom() 

Nachdem wir die Grundstruktur der Klasse erstellt haben, ist es Zeit, die erste der geplanten Aufgaben zu erledigen: die Verbindung zwischen der Anwendung und dem Server zu öffnen.

Verbindung öffnen


Wir kehren zu ChatRoom.swift zurück und fügen diese Methode für Eigenschaftsdefinitionen hinzu:

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

Folgendes machen wir hier:

  1. Zuerst definieren wir zwei Variablen für Socket-Streams ohne automatische Speicherverwaltung
  2. Dann erstellen wir unter Verwendung derselben Variablen direkt Streams, die an die Host- und Portnummer gebunden sind.

Die Funktion hat vier Argumente. Der erste ist der Typ des Speicherzuordners, den wir beim Initialisieren der Threads verwenden. Sie sollten kCFAllocatorDefault verwenden , obwohl es andere mögliche Optionen gibt, falls Sie das Verhalten der Threads ändern möchten.

Anmerkung des Übersetzers
In der Dokumentation zur Funktion CFStreamCreatePairWithSocketToHost heißt es: Verwenden Sie NULL oder kCFAllocatorDefault . Und die Beschreibung von kCFAllocatorDefault besagt, dass es ein Synonym für NULL ist . Der Kreis ist geschlossen!

Dann setzen wir den Hostnamen. In unserem Fall stellen wir eine Verbindung zum lokalen Server her. Befindet sich Ihr Server an einem anderen Ort, können Sie seine IP-Adresse festlegen.

Dann die Portnummer, die der Server abhört.

Schließlich übergeben wir Zeiger an unsere E / A-Streams, damit die Funktion sie initialisieren und mit den von ihr erstellten Streams verbinden kann.

Nachdem wir die initialisierten Flows haben, können wir Links zu ihnen speichern, indem wir diese Zeilen am Ende der setupNetworkCommunication () -Methode hinzufügen :

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

Durch die Verwendung von takeRetainedValue (), wie es auf ein nicht verwaltetes Objekt angewendet wird, können wir einen Verweis darauf beibehalten und gleichzeitig zukünftige Speicherverluste vermeiden. Jetzt können wir unsere Threads verwenden, wo immer wir wollen.

Jetzt müssen wir diese Threads zur Ausführungsschleife hinzufügen, damit unsere Anwendung Netzwerkereignisse korrekt verarbeitet. Fügen Sie dazu diese beiden Zeilen am Ende von setupNetworkCommunication () hinzu :

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

Endlich ist es Zeit zu segeln! Fügen Sie dies zunächst ganz am Ende der setupNetworkCommunication () -Methode hinzu:

 inputStream.open() outputStream.open() 

Jetzt haben wir eine offene Verbindung zwischen unserer Client- und Serveranwendung.

Wir können unsere Anwendung kompilieren und ausführen, aber Sie werden noch keine Änderungen sehen, da wir mit unserer Client-Server-Verbindung nichts anfangen.

Verbindung zum Chat herstellen


Nachdem wir eine Verbindung zum Server hergestellt haben, ist es Zeit, etwas dagegen zu unternehmen! Im Falle eines Chats müssen Sie sich zuerst vorstellen und können dann Nachrichten an die Gesprächspartner senden.

Dies führt uns zu einer wichtigen Schlussfolgerung: Da wir zwei Arten von Nachrichten haben, müssen wir sie irgendwie unterscheiden.

Chat-Protokoll


Einer der Vorteile der Verwendung der TCP-Schicht besteht darin, dass wir unser eigenes „Protokoll“ für die Kommunikation definieren können.

Wenn wir HTTP verwenden würden, müssten wir diese verschiedenen Wörter GET , PUT , PATCH verwenden . Wir müssten URLs bilden und die richtigen Header und all das verwenden.

Wir haben nur zwei Arten von Nachrichten. Wir werden senden

iam:Luke

um in den Chat einzutreten und sich vorzustellen.

Und wir werden senden

msg:Hey, how goes it, man?

um eine Chat-Nachricht an alle Befragten zu senden.

Es ist sehr einfach, aber absolut prinzipienlos. Verwenden Sie diese Methode daher nicht in kritischen Projekten.

Jetzt wissen wir, was unser Server erwartet, und können eine Methode in die ChatRoom- Klasse schreiben, mit der der Benutzer eine Verbindung zum Chat herstellen kann. Das einzige Argument ist der Spitzname des Benutzers.

Fügen Sie diese Methode in ChatRoom.swift hinzu :

 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. Zuerst bilden wir unsere Nachricht mit unserem eigenen „Protokoll“.
  2. Speichern Sie den Namen zur späteren Bezugnahme.
  3. withUnsafeBytes (_ :) bietet eine bequeme Möglichkeit, mit einem unsicheren Zeiger in einem Abschluss zu arbeiten.
  4. Schließlich senden wir unsere Nachricht an den Ausgabestream. Dies sieht möglicherweise komplizierter aus als erwartet. Write (_: maxLength :) verwendet jedoch den im vorherigen Schritt erstellten unsicheren Zeiger.

Jetzt ist unsere Methode fertig, öffnen Sie ChatRoomViewController.swift und fügen Sie dieser Methode am Ende von viewWillAppear (_ :) einen Aufruf hinzu.

 chatRoom.joinChat(username: username) 

Kompilieren Sie nun die Anwendung und führen Sie sie aus. Geben Sie Ihren Spitznamen ein und tippen Sie auf Zurück, um zu sehen ...



... dass sich wieder nichts geändert hat!

Warten Sie, es ist alles in Ordnung! Gehen Sie zum Terminalfenster. Dort sehen Sie die Nachricht, der Vasya beigetreten ist oder so, wenn Sie nicht Vasya heißen.

Das ist großartig, aber es wäre schön, wenn auf dem Bildschirm Ihres Telefons ein Hinweis auf eine erfolgreiche Verbindung angezeigt würde.

Antworten auf eingehende Nachrichten


Der Server sendet Client-Beitrittsnachrichten an alle Mitglieder des Chats, einschließlich Sie. Glücklicherweise verfügt unsere Anwendung bereits über alles, um eingehende Nachrichten in Form von Zellen in der Nachrichtentabelle in ChatRoomViewController anzuzeigen .

Sie müssen lediglich inputStream verwenden , um diese Nachrichten zu „fangen“, sie in Instanzen der Message- Klasse zu konvertieren und sie zur Anzeige an die Tabelle zu übergeben.

Um auf eingehende Nachrichten antworten zu können, benötigen Sie ChatRoom , um das StreamDelegate- Protokoll einzuhalten .

Fügen Sie dazu diese Erweiterung am Ende der Datei ChatRoom.swift hinzu :

 extension ChatRoom: StreamDelegate { } 

Geben Sie nun an, wer Delegierter von inputStream werden soll.

Fügen Sie diese Zeile der setupNetworkCommunication () -Methode unmittelbar vor den aufzurufenden Aufrufen hinzu (in: forMode :):

 inputStream.delegate = self 

Fügen Sie nun die Implementierung der Stream-Methode (_: handle :) zur Erweiterung hinzu:

 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...") } } 

Wir verarbeiten eingehende Nachrichten


Wir sind also bereit, eingehende Nachrichten zu verarbeiten. Das Ereignis, das uns interessiert, ist .hasBytesAvailable , was darauf hinweist, dass eine eingehende Nachricht eingetroffen ist.

Wir werden eine Methode schreiben, die diese Nachrichten verarbeitet. Unterhalb der neu hinzugefügten Methode schreiben wir Folgendes:

 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. Wir legen den Puffer fest, in dem die eingehenden Bytes gelesen werden.
  2. Wir drehen uns in einer Schleife, während im Eingabestream etwas zu lesen ist.
  3. Wir rufen read (_: maxLength :) auf, das die Bytes aus dem Stream liest und in den Puffer legt.
  4. Wenn der Aufruf einen negativen Wert zurückgibt, geben wir einen Fehler zurück und verlassen die Schleife.

Wir müssen diese Methode aufrufen, sobald wir Daten im eingehenden Stream haben. Gehen Sie also zur switch-Anweisung in der stream- Methode (_: handle :) , suchen Sie den Schalter .hasBytesAvailable und rufen Sie diese Methode unmittelbar nach der print-Anweisung auf:

 readAvailableBytes(stream: aStream as! InputStream) 

An dieser Stelle haben wir einen vorbereiteten Puffer mit empfangenen Daten!

Wir müssen diesen Puffer jedoch noch in den Inhalt der Nachrichtentabelle umwandeln.

Platzieren Sie diese Methode auf 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) } 

Zuerst initialisieren wir String mit dem Puffer und der Größe, die wir an diese Methode übergeben.

Der Text befindet sich in UTF-8. Am Ende wird der Puffer freigegeben und die Nachricht durch das Symbol ':' geteilt, um den Namen des Absenders und die Nachricht selbst zu trennen.

Jetzt analysieren wir, ob diese Nachricht von einem anderen Teilnehmer stammt. Auf dem Produkt können Sie so etwas wie ein einzigartiges Token erstellen. Dies reicht für die Demo aus.

Schließlich bilden wir aus all dieser Wirtschaft eine Instanz der Botschaft und geben sie zurück.

Um diese Methode zu verwenden, fügen Sie unmittelbar nach dem letzten Kommentar das folgende if-let am Ende der while-Schleife in die Methode readAvailableBytes (stream :) ein :

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

Jetzt ist alles bereit, an jemanden weiterzugeben. Nachricht ... Aber an wen?

Erstellen Sie das ChatRoomDelegate-Protokoll


Wir müssen ChatRoomViewController.swift über die neue Nachricht informieren, haben jedoch keinen Link dazu. Da es einen starken ChatRoom- Link enthält, können wir in die Falle eines starken Linkzyklus geraten .

Dies ist der perfekte Ort, um ein Delegatenprotokoll zu erstellen. ChatRoom ist es egal, wer über neue Beiträge Bescheid wissen muss.

Fügen Sie oben in ChatRoom.swift eine neue Protokolldefinition hinzu:

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

Fügen Sie nun in der ChatRoom- Klasse ein schwaches Glied hinzu, um zu speichern, wer der Delegat wird:

 weak var delegate: ChatRoomDelegate? 

Fügen wir nun die Methode readAvailableBytes (stream :) hinzu und fügen die folgende Zeile innerhalb des if-let-Konstrukts unter dem letzten Kommentar in der Methode hinzu:

 delegate?.received(message: message) 

Kehren Sie zu ChatRoomViewController.swift zurück und fügen Sie unmittelbar nach MessageInputDelegate die folgende Klassenerweiterung hinzu, die die Einhaltung des ChatRoomDelegate- Protokolls sicherstellt:

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

Das ursprüngliche Projekt enthält bereits die erforderlichen Informationen. InsertNewMessageCell (_ :) akzeptiert Ihre Nachricht und zeigt die richtige Zelle in der Tabellenansicht an.

Weisen Sie nun den View Controller als Delegaten zu, indem Sie diesen unmittelbar nach dem Aufruf von super.viewWillAppear () zu viewWillAppear (_ :) hinzufügen.

 chatRoom.delegate = self 

Kompilieren Sie nun die Anwendung und führen Sie sie aus. Geben Sie einen Namen ein und tippen Sie auf Zurück.



Sie sehen eine Zelle über Ihre Verbindung zum Chat. Hurra, Sie haben erfolgreich eine Nachricht an den Server gesendet und eine Antwort von ihm erhalten!

Nachrichten posten


Jetzt, da ChatRoom Nachrichten senden und empfangen kann, ist es Zeit, dem Benutzer die Möglichkeit zu geben, seine eigenen Phrasen zu senden.

Fügen Sie in ChatRoom.swift am Ende der Klassendefinition die folgende Methode hinzu:

 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) } } 

Diese Methode ähnelt joinChat (Benutzername :) , den wir zuvor geschrieben haben, außer dass das msg- Präfix vor dem Text steht (um anzuzeigen, dass es sich um eine echte Chat-Nachricht handelt).

Da wir Nachrichten über die Schaltfläche Senden senden möchten, kehren wir zu ChatRoomViewController.swift zurück und finden dort MessageInputDelegate .

Hier sehen wir die leere Methode sendWasTapped (message :) . Um eine Nachricht zu senden, senden Sie sie an chatRoom:

 chatRoom.send(message: message) 

Eigentlich ist das alles! Da der Server die Nachricht empfängt und an alle weiterleitet, wird ChatRoom über die neue Nachricht auf die gleiche Weise benachrichtigt wie beim Beitritt zum Chat.

Kompilieren Sie die Anwendung und führen Sie sie aus.



Wenn Sie noch niemanden zum Chatten haben, öffnen Sie ein neues Terminalfenster und geben Sie Folgendes ein:

nc localhost 80

Dadurch werden Sie mit dem Server verbunden. Jetzt können Sie mit demselben "Protokoll" eine Verbindung zum Chat herstellen:

iam:gregg

Und so - senden Sie eine Nachricht:

msg:Ay mang, wut's good?



Herzlichen Glückwunsch, Sie haben einen Kunden für den Chat geschrieben!

Wir reinigen uns


Wenn Sie jemals Anwendungen entwickelt haben, die Dateien aktiv lesen / schreiben, sollten Sie wissen, dass gute Entwickler Dateien schließen, wenn sie mit ihnen fertig sind. Tatsache ist, dass die Verbindung über den Socket vom Dateideskriptor bereitgestellt wird. Dies bedeutet, dass Sie es nach Abschluss der Arbeiten wie jede andere Datei schließen müssen.

Fügen Sie dazu ChatRoom.swift die folgende Methode hinzu, nachdem Sie send (message :) definiert haben :

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

Wie Sie wahrscheinlich vermutet haben, schließt diese Methode Threads, sodass Sie keine Nachrichten mehr empfangen und senden können. Außerdem werden Threads aus der Ausführungsschleife entfernt, in der wir sie zuvor platziert haben.

Fügen Sie dieser Methode im Abschnitt .endEncountered der switch-Anweisung im Stream einen Aufruf hinzu (_: handle :) :

 stopChatSession() 

Gehen Sie dann zurück zu ChatRoomViewController.swift und machen Sie dasselbe in viewWillDisappear (_ :) :

 chatRoom.stopChatSession() 

Das ist alles! Jetzt sicher!

Fazit


Nachdem Sie die Grundlagen der Vernetzung mit Sockets beherrschen, können Sie Ihr Wissen vertiefen.

UDP-Sockets


Diese Anwendung ist ein Beispiel für die Netzwerkkommunikation mit TCP, die die Zustellung von Paketen an das Ziel garantiert.

Sie können jedoch UDP-Sockets verwenden. Diese Art der Verbindung garantiert nicht die Lieferung von Paketen zu ihrem beabsichtigten Zweck, ist jedoch viel schneller.

Dies ist besonders nützlich in Spielen. Schon mal eine Verzögerung erlebt? Dies bedeutete, dass Sie eine schlechte Verbindung hatten und viele UDP-Pakete verloren gingen.

Websockets


Eine weitere Alternative zu HTTP in Anwendungen ist eine Technologie namens Web Sockets.

Im Gegensatz zu normalen TCP-Sockets verwenden Web-Sockets HTTP, um die Kommunikation herzustellen. Mit ihrer Hilfe können Sie dasselbe erreichen wie mit normalen Steckdosen, jedoch mit Komfort und Sicherheit, wie in einem Browser.

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


All Articles