Une introduction aux WebSockets | raywenderlich.com

Online Coding Courses for Kids

Un WebSocket est un protocole réseau qui permet une communication bidirectionnelle entre un serveur et un client. Contrairement à HTTP, qui utilise un modèle de demande et de réponse, les homologues WebSocket peuvent envoyer des messages dans les deux sens à tout moment.

Les WebSockets sont souvent utilisés pour les applications basées sur le chat et d’autres applications qui doivent communiquer en permanence entre le serveur et le client.

Dans ce didacticiel WebSockets, vous apprendrez à:

  • Lancez une négociation WebSocket, y compris la manière dont la connexion est mise à niveau vers un WebSocket.
  • Envoyez des messages via WebSocket.
  • Traitez les messages reçus envoyés via WebSocket.
  • Intégration avec un serveur Vapor, une application iOS et une page Web à l’aide de WebSockets.

Dans ce didacticiel, vous apprendrez à intégrer une simple application de questions et réponses iOS à un serveur Vapor pour envoyer et recevoir des informations via WebSockets. L’application iOS vous permet de créer des questions et l’application Vapor stockera ces questions avec leur statut de Sans réponse ou Répondu.

Commencer

Téléchargez le projet de démarrage en cliquant sur le Télécharger les matériaux bouton en haut ou en bas du didacticiel.

dans le Entrée répertoire sont deux dossiers:

  • websockets-backend: Contient le back-end Vapor 4 que vous utiliserez comme serveur WebSocket.
  • websocket-ios: Contient l’application iOS qui sera le client WebSocket.

Dans la section suivante, vous allez configurer la partie serveur de WebSocket et en savoir plus sur le fonctionnement d’un WebSocket.

Création d’un serveur WebSocket

Une connexion WebSocket commence par une poignée de main. C’est une petite danse que le client et le serveur font pour démarrer la connexion, comme deux oiseaux en train de danser. :]

Le client démarre avec une requête HTTP normale qui contient deux en-têtes spéciaux: Mise à niveau: WebSocket et Connexion: mise à niveau, ainsi que toute autre donnée de demande requise comme l’authentification.

Le serveur renvoie ensuite un Protocoles de commutation HTTP 101 indication du code d’état au client, qui dit essentiellement: nous parlions en HTTP, mais nous utiliserons autre chose à l’avenir. Parallèlement à cette réponse HTTP 101, il envoie également le Mise à niveau: WebSocket et Connexion: mise à niveau en-têtes à nouveau.

Après cela, la poignée de main est terminée et la connexion WebSocket est en place!

Diagramme montrant le flux de données entre le client et le serveur à l'aide d'un WebSocket

Avec la poignée de main à l’écart, vous pouvez commencer à configurer le serveur principal. Aller vers Starter / websocket-backend et double-cliquez Package.swift. Cela ouvre le projet dans Xcode et démarre le téléchargement des dépendances.

Lorsque Xcode a fini de récupérer les dépendances, ouvrez QuestionsController.swift, où vous ajouterez l’itinéraire auquel le client WebSocket se connectera.

Votre première étape consiste à ajouter la nouvelle fonction suivante ci-dessus index dans QuestionsController:

func webSocket(req: Request, socket: WebSocket) {
  socket.onText { _, text in print(text) }
}

Pour l’instant, tout ce que vous voulez faire est d’imprimer tous les messages texte que vous avez reçus via votre connexion WebSocket. Pour vous assurer que le client peut se connecter à ce serveur, ajoutez l’itinéraire suivant en haut de boot(routes:):

routes.webSocket("socket", onUpgrade: self.webSocket)

En utilisant routes.webSocket ici au lieu d’une méthode HTTP habituelle, Vapor s’occupe de toute la négociation pour vous… génial!

Tester le WebSocket

Votre serveur devrait fonctionner maintenant, mais c’est une bonne idée d’en être sûr. Pour tester votre nouveau serveur WebSocket, créez et exécutez votre code. Cela a fonctionné avec succès si les journaux montrent ce qui suit:

[ NOTICE ] Server starting on http://127.0.0.1:8080

Maintenant, il est temps de faire du WebSocket-ing! Ouvrez votre navigateur et accédez à http://www.websocket.org/echo.html.

Remarque: Tu dois utiliser http ici, pas https, car vous vous connectez à un serveur localhost.

Ensuite, entrez ws: // localhost: 8080 / socket dans le Emplacement champ et appuyez sur le Relier bouton. dans le Journal champ à droite, CONNECTÉ apparaîtra et votre back-end Vapor enregistrera:

[ INFO ] GET /socket

Maintenant que vous avez configuré la connexion, saisissez du texte dans le champ Message champ et frapper Envoyer. Votre message apparaîtra désormais comme par magie dans votre console Xcode.

Félicitations, vous venez de créer votre propre serveur WebSocket!

Écriture de données WebSocket

Bien entendu, vous ne pouvez pas vous attendre à ce que vos utilisateurs accèdent à ce site de test et saisissent toutes leurs données manuellement. Dans cette section, vous allez configurer l’application compagnon iOS pour envoyer des messages WebSocket directement à votre serveur. Mais avant de faire cela, prenez un moment pour savoir ce qu’est exactement un message WebSocket.

Comprendre les messages WebSocket

Dans le test ci-dessus, vous avez simplement entré du texte, mais il se passe beaucoup plus dans les coulisses. Par le Protocole WebSocket RFC: “Dans le protocole WebSocket, les données sont transmises à l’aide d’une séquence de trames.”

Un cadre WebSocket se compose de quelques bits que vous pouvez comparer aux en-têtes HTTP, suivis d’un message réel. Si un message est trop volumineux, vous pouvez utiliser plusieurs cadres pour l’envoyer.

Les éléments de cadre suivants sont particulièrement intéressants:

  • AILETTE: Ce bit unique indique s’il s’agit de la dernière trame d’un message ou si d’autres trames suivront.
  • opcode: Ces quatre bits indiquent de quel type de message il s’agit et comment gérer les données utiles.
  • MASQUE: Ce bit unique indique si le message est masqué. Les messages client-serveur doivent toujours être masqués.
  • Longueur de la charge utile: Utilisez ces 7, 23 ou 71 bits pour expliquer la taille des données utiles en bits, un peu comme celles de HTTP Content-Length entête.
  • Clé de masquage: Ces quatre bits facultatifs contiennent la clé que vous avez utilisée pour masquer le message. Vous pouvez les omettre si le MASK le bit est 0.
  • Données de charge utile: Le reste du message contient la charge utile réelle.

Il existe deux types d’opcodes: ceux pour les cadres sans contrôle et pour les cadres de contrôle. Les codes de trame sans contrôle sont: continuation, text et binary. Ceux-ci traitent de la façon de décoder la charge utile. Les codes du cadre de contrôle sont: connection close, ping et pong. Ce sont des types de messages spéciaux.

Comme l’indique la RFC, vous devez masquer tous les messages client-serveur. Vous masquez un message en générant quatre bits aléatoires comme clé de masque. Chaque octet dans les données de charge utile est XORed en fonction de la clé de masquage. Cela n’affecte pas la longueur de la charge utile.

Le démasquage utilise le même processus que le masquage.

Maintenant que vous savez comment fonctionnent les messages WebSocket, il est temps de commencer à en créer.

Configuration de l’interface utilisateur

Pour échanger des messages WebSocket avec votre serveur, vous allez créer une application SwiftUI simple que les utilisateurs peuvent utiliser pour poser des questions après une conférence.

Votre première étape consiste à configurer l’interface utilisateur de l’application pour permettre aux utilisateurs d’envoyer leurs questions.

Ouvrez le projet iOS en accédant à Démarreur / websocket-ios et double-cliquez Conférence Q & A.xcodeproj. Construisez et exécutez et vous verrez une vue simple “Bonjour, monde”. Vous allez changer cela ensuite!

Ouvert ContentView.swift et remplacer le contenu de ContentView avec ce qui suit:

    // 1
    @State var newQuestion: String = ""
    
    // TODO: Remove following line when socket is implemented.
    @State var questions: [String] = []
    
    // 2
    @ObservedObject var keyboard: Keyboard = .init()
    @ObservedObject var socket: WebSocketController = .init()
    
    var body: some View {
      // 3
      VStack(spacing: 8) {
        Text("Your asked questions:")
        Divider()
        // 4
        // TODO: Update list when socket is implemented.
        List(self.questions, id: .self) { q in
          VStack(alignment: .leading) {
            Text(q)
            Text("Status: Unanswered")
              .foregroundColor(.red)
          }
        }
        Divider()
        // 5
        TextField("Ask a new question", text: $newQuestion, onCommit: {
          guard !self.newQuestion.isEmpty else { return }
          self.socket.addQuestion(self.newQuestion)
          // TODO: Remove following line when socket is implemented.
          self.questions.append(self.newQuestion)
          self.newQuestion = ""
        })
          .textFieldStyle(RoundedBorderTextFieldStyle())
          .padding(.horizontal)
          .padding(.bottom, keyboard.height)
          .edgesIgnoringSafeArea(keyboard.height > 0 ? .bottom : [])
      }
      .padding(.vertical)
      // 6
      .alert(item: $socket.alertWrapper) { $0.alert }
    }

Maintenant, il se passe beaucoup de choses ici:

  1. Au sommet se trouvent deux State variables pour les vues. L’un contient l’entrée utilisateur pour les nouvelles questions tandis que l’autre stocke ces questions temporairement.
  2. Viens ensuite le ObservedObjects. Keyboard déplace le champ de texte vers le haut lorsque le clavier est actif. WebSocketController contient tout le code WebSocket requis.
  3. Dans le corps de la vue, vous commencez par un simple VStack pour mettre toutes les vues les unes sur les autres.
  4. La première section du VStack est un List contenant toutes les questions posées. Vous allez connecter ceci au WebSocketController données plus tard.
  5. Vient ensuite le TextField pour de nouvelles questions. Une fois que vous connectez le WebSocket, vous supprimez le local self.questions array et stockez les données dans le back-end.
  6. Enfin, vous disposez d’un espace réservé pour une alerte que vous utiliserez lorsque WebSocket signale une erreur.

Créez et exécutez l’application et essayez de saisir une question. presse Entrer et vous verrez la question apparaître dans la liste avec un statut de Sans réponse. Impressionnant!

Maintenant que l’interface utilisateur est terminée, vous êtes prêt à vous connecter à votre serveur WebSocket.

Connexion au serveur

Votre application ne fait pas grand-chose si elle ne peut pas se connecter au serveur pour envoyer les questions de l’utilisateur. Votre prochaine étape consiste à établir cette connexion.

Ouvert WebSocketController.swift et localiser connect(). Remplacer // TODO: Implement avec le code suivant pour démarrer la connexion socket:

    self.socket = session.webSocketTask(with: 
      URL(string: "ws://localhost:8080/socket")!)
    self.listen()
    self.socket.resume()

Cela définira self.socket à un URLSessionWebSocketTask, démarrez votre cycle d’écoute et reprenez la tâche. Si vous avez utilisé URLSession, cela vous semblera familier.

Ensuite, vous direz URLSession pour recevoir de nouveaux messages dans listen. Remplacez le commentaire TODO par:

    // 1
    self.socket.receive { [weak self] (result) in
      guard let self = self else { return }
      // 2
      switch result {
      case .failure(let error):
        print(error)
        // 3
        let alert = Alert(
            title: Text("Unable to connect to server!"),
            dismissButton: .default(Text("Retry")) {
              self.alert = nil
              self.socket.cancel(with: .goingAway, reason: nil)
              self.connect()
            }
        )
        self.alert = alert
        return
      case .success(let message):
        // 4
        switch message {
        case .data(let data):
          self.handle(data)
        case .string(let str):
          guard let data = str.data(using: .utf8) else { return }
          self.handle(data)
        @unknown default:
          break
        }
      }
      // 5
      self.listen()
    }

Voici ce qui se passe avec cet extrait:

  1. Tout d’abord, vous ajoutez un rappel à URLSessionWebSocketTask.receive. Lorsque le serveur reçoit un message, il exécute cette fermeture. self est faiblement capturé donc avant d’exécuter le reste de la fermeture, le protecteur s’assure self n’est pas nil.
  2. La valeur passée dans la fermeture est de type Result. Le commutateur vous permet de gérer à la fois les messages réussis et les erreurs.
  3. En cas d’erreur, créez un SwiftUI Alert, que vous présenterez à l’utilisateur ultérieurement.
  4. En cas de message réussi, passez le raw Data à ton handle.
  5. URLSessionWebSocketTask.receive enregistre un rappel unique. Ainsi, une fois le rappel exécuté, réenregistrez-le pour continuer à écouter les nouveaux messages.

Votre serveur est maintenant prêt à recevoir des messages. Cependant, vous avez encore un peu de travail à faire pour informer le serveur des types de messages qu’il recevra.

Création de la structure de données

Avant de traiter des données, vous devez définir votre structure de données. Vous ferez cela maintenant.

Commencez par créer un nouveau fichier Swift appelé WebSocketTypes.swift contenant le code suivant:

import Foundation

enum QnAMessageType: String, Codable {
  // Client to server types
  case newQuestion
  // Server to client types
  case questionResponse, handshake, questionAnswer
}

struct QnAMessageSinData: Codable {
  let type: QnAMessageType
}

Il s’agit de la couche de base de votre structure de données. QnAMessageType déclare les différents types de messages, à la fois client-serveur et serveur-client.

Chaque message que vous envoyez entre votre client et votre serveur contiendra un type afin que le destinataire sache quel type de message décoder.

Maintenant, créez les types de messages réels au-dessus de cette couche. En bas de WebSocketTypes.swift, ajoutez ce qui suit:

struct QnAHandshake: Codable {
  let id: UUID
}

struct NewQuestionMessage: Codable {
  var type: QnAMessageType = .newQuestion
  let id: UUID
  let content: String
}

class NewQuestionResponse: Codable, Comparable {
  let success: Bool
  let message: String
  let id: UUID?
  var answered: Bool
  let content: String
  let createdAt: Date?
  
  static func < (lhs: NewQuestionResponse, rhs: NewQuestionResponse) -> Bool {
    guard let lhsDate = lhs.createdAt,
      let rhsDate = rhs.createdAt else {
        return false
    }

    return lhsDate < rhsDate
  }
  
  static func == (lhs: NewQuestionResponse, rhs: NewQuestionResponse) -> Bool {
    lhs.id == rhs.id
  }
}

struct QuestionAnsweredMessage: Codable {
  let questionId: UUID
}

Tous les types ci-dessus, sauf pour NewQuestionMessage, sont des types de serveur à client et, par conséquent, n’ont pas type jeu de champs.

Traitement des données

Maintenant que vous avez configuré votre structure de données, il est temps d’indiquer au serveur comment gérer les questions qu’il reçoit des clients qui se connectent.

Ouvert WebSocketController.swift. Maintenant, vous pouvez maintenant implémenter le reste des méthodes, en commençant par handle(_:).

Remplacez le TODO par le code suivant:

    do {
      // 1
      let sinData = try decoder.decode(QnAMessageSinData.self, from: data)
      // 2
      switch sinData.type {
      case .handshake:
        // 3
        print("Shook the hand")
        let message = try decoder.decode(QnAHandshake.self, from: data)
        self.id = message.id
      // 4
      case .questionResponse:
        try self.handleQuestionResponse(data)
      case .questionAnswer:
        try self.handleQuestionAnswer(data)
      default:
        break
      }
    } catch {
      print(error)
    }

Ce code effectue les opérations suivantes:

  1. Décode le QnAMessageSinData tapez que vous venez de déclarer. Cela décodera uniquement le type champ.
  2. Allume le décodé type et effectue une gestion spécifique au type.
  3. Pour le type de prise de contact, décode le message complet et stocke l’ID.
  4. Pour une question réponse et réponse, transmet les données à des fonctions d’implémentation spécifiques.

Vous faites de grands progrès! Vous voudrez probablement l’essayer, mais avant de pouvoir tester le système complet, vous devez faire deux choses:

  1. Le serveur doit envoyer une poignée de main personnalisée au client pour identifier les clients ultérieurement.
  2. Le client doit envoyer de nouvelles questions au serveur.

Ce seront les deux prochaines étapes. Vous allez commencer par le serveur.

Configuration de la négociation du serveur

Dans ton backend websocket projet, ouvert QuestionsController.swift et remplacer le contenu de webSocket(req:socket:) avec ce qui suit:

self.wsController.connect(socket)

À ce stade, votre collection de routes ne contrôlera plus la connexion socket.

Ouvert WebSocketController.swift implémenter connect(_:). Remplacez le commentaire TODO par ce qui suit:

    // 1
    let uuid = UUID()
    self.lock.withLockVoid {
      self.sockets[uuid] = ws
    }
    // 2
    ws.onBinary { [weak self] ws, buffer in
      guard let self = self,
        let data = buffer.getData(
          at: buffer.readerIndex, length: buffer.readableBytes) else {
            return
      }

      self.onData(ws, data)
    }
    // 3
    ws.onText { [weak self] ws, text in
      guard let self = self,
        let data = text.data(using: .utf8) else {
          return
      }

      self.onData(ws, data)
    }
    // 4
    self.send(message: QnAHandshake(id: uuid), to: .socket(ws))

Cela fait ce qui suit:

  1. Génère un UUID aléatoire pour chaque connexion socket pour identifier chacune d’elles, puis stocke la connexion dans un dictionnaire basé sur cet UUID.
  2. Lors de la réception de données binaires, il stocke les données dans SwiftNIO ByteBuffer. En utilisant getData, vous obtenez un Data instance du tampon, puis transmettez-la pour qu’elle soit traitée.
  3. Pour les données textuelles, vous utilisez String.data pour obtenir un Data instance et, encore une fois, le transmettre pour qu’il soit traité.
  4. Enfin, vous envoyez un QnAHandshake message à la nouvelle connexion, contenant l’UUID que vous avez généré ci-dessus.

le QnAHandshake le type ici est le même que celui que vous avez créé dans l’application iOS plus tôt. Regardez le modèle de données côté serveur dans WebSocketTypes.swift. C’est la même chose que du côté iOS, sauf les types qui ont déjà leur type les jeux de champs sont inversés.

Ensuite, de retour dans WebSocketController.swift, mettez le code suivant dans send(message:to:):

    logger.info("Sending (T.self) to (sendOption)")
    do {
      // 1
      let sockets: [WebSocket] = self.lock.withLock {
        switch sendOption {
        case .id(let id):
          return [self.socketshttps://www.raywenderlich.com/13209594-an-introduction-to-websockets].compactMap { $0 }
        case .socket(let socket):
          return [socket]
        }
      }
      
      // 2
      let encoder = JSONEncoder()
      let data = try encoder.encode(message)
      
      // 3
      sockets.forEach {
        $0.send(raw: data, opcode: .binary)
      }
    } catch {
      logger.report(error: error)
    }

Voici ce qui se passe ici:

  1. Basé sur WebSocketSendOption, vous sélectionnez une prise parmi les prises connectées.
  2. Vous encodez le message que vous souhaitez envoyer en utilisant JSONEncoder.
  3. Vous envoyez les données encodées à toutes les sockets sélectionnées à l’aide de l’opcode binaire.

Défi: étendre WebSocketSendOption

Dans cette application, vous n’enverrez des données qu’à des sockets uniques. Cependant, comme un défi amusant pour vous-même, essayez d’étendre WebSocketSendOption pour autoriser l’envoi à:

  • Toutes les prises
  • Plusieurs sockets basés sur des ID

Ouvrez le spoiler ci-dessous pour trouver la réponse.

[spoiler title=”Solution”]
Ajoutez les cas suivants à WebSocketSendOption:

case all, ids([UUID])

Et ajoutez ce qui suit à switch dans send:

case .all:
  return self.sockets.values.map { $0 }
case .ids(let ids):
  return self.sockets.filter { key, _ in ids.contains(key) }.map { $1 }

[/spoiler]

Une fois cela en place, il est temps de revenir au projet iOS et de commencer à envoyer des questions.

Envoi des questions

Maintenant que le serveur est prêt à recevoir des questions, il est temps de préparer l’application à les envoyer.

Revenir au Conférence Q&A Projet Xcode et ouvrir WebSocketController.swift. Localiser addQuestion(_:) et remplacez le contenu par ce qui suit:

    guard let id = self.id else { return }
    // 1
    let message = NewQuestionMessage(id: id, content: content)
    do {
      // 2
      let data = try encoder.encode(message)
      // 3
      self.socket.send(.data(data)) { (err) in
        if err != nil {
          print(err.debugDescription)
        }
      }
    } catch {
      print(error)
    }

Ce code va:

  1. Construction Codable avec le contenu de la question et l’ID transmis par le message de prise de contact.
  2. Encodez-le en Data exemple.
  3. Envoyez le résultat au serveur en utilisant l’opcode binaire. Ici, URLSession s’occupe du masquage pour vous.

Bon travail! Maintenant, il est temps de l’essayer.

Donner un test à votre connexion WebSocket

Avec cela, tout est en place pour donner à l’ensemble du processus un test complet!

Tout d’abord, créez et exécutez le backend websocket projet pour vous assurer que le serveur est prêt à accepter les connexions.

Lorsque cela est fait, créez et exécutez le Questions et réponses sur la conférence application iOS.

Dans les messages du journal du serveur, vous verrez:

[ INFO ] GET /socket
[ INFO ] Sending QnAHandshake to socket(WebSocketKit.WebSocket)

L’application iOS se connectera Shook the hand. Connection établie!

Maintenant, entrez une question dans l’application et appuyez sur Revenir. Le serveur enregistrera quelque chose comme ceci:

[ INFO ] {"type":"newQuestion","id":"1FC4044C-7E20-4332-9349-E4FDD69A10B3","content":"Awesome question!"}

Génial, tout fonctionne!

Réception de données WebSocket

Une fois la première connexion établie, il ne reste que peu de choses à faire. À savoir:

  • Stockez les questions posées dans la base de données.
  • Mettez à jour l’application iOS pour utiliser le back-end comme source des questions posées.
  • Ajoutez la possibilité de répondre aux questions.
  • Mettez à jour l’application iOS lorsque le présentateur répond à une question.

Vous aborderez ces problèmes ensuite.

Stockage des données

Pour stocker les données, ouvrez le backend websocket projet et ouvrir WebSocketController.swift. Au dessus de onData, ajoutez une nouvelle fonction appelée onNewQuestion et ajoutez-y le code suivant:

  func onNewQuestion(_ ws: WebSocket, _ id: UUID, _ message: NewQuestionMessage) {
    let q = Question(content: message.content, askedFrom: id)
    self.db.withConnection {
      // 1
      q.save(on: $0)
    }.whenComplete { res in
      let success: Bool
      let message: String
      switch res {
      case .failure(let err):
        // 2
        self.logger.report(error: err)
        success = false
        message = "Something went wrong creating the question."
      case .success:
        // 3
        self.logger.info("Got a new question!")
        success = true
        message = "Question created. We will answer it as soon as possible :]"
      }
      // 4
      try? self.send(message: NewQuestionResponse(
        success: success,
        message: message,
        id: q.requireID(),
        answered: q.answered,
        content: q.content,
        createdAt: q.createdAt
      ), to: .socket(ws))
    }
  }

Voici ce qui se passe:

  1. Tout d’abord, vous créez un Question model et enregistrez-la dans la base de données.
  2. Si l’enregistrement échoue, vous enregistrez l’erreur et indiquez l’échec.
  3. D’un autre côté, si l’enregistrement a réussi, vous vous connectez à l’arrivée d’une nouvelle question.
  4. Enfin, vous envoyez un message au client indiquant que vous avez reçu la question, avec succès ou non.

Ensuite, remplacez la ligne de journal dans onData avec ce qui suit:

    let decoder = JSONDecoder()
    do {
      // 1
      let sinData = try decoder.decode(QnAMessageSinData.self, from: data)
      // 2
      switch sinData.type {
      case .newQuestion:
        // 3
        let newQuestionData = try decoder.decode(NewQuestionMessage.self,
                                                 from: data)
        self.onNewQuestion(ws, sinData.id, newQuestionData)
      default:
        break
      }
    } catch {
      logger.report(error: error)
    }

Cette volonté:

  1. Décoder QnAMessageSinData pour voir quel type de message est entré.
  2. Allumez le type. Actuellement, le serveur n’accepte qu’un seul type de message entrant.
  3. Décoder le plein NewQuestionMessage et le passer à onNewQuestion.

Ces deux fonctions vous permettront de stocker toutes les questions reçues dans la base de données et de renvoyer une confirmation aux clients.

Connexion au serveur 2: connectez-vous plus fort

Vous vous souvenez qu’il y avait un tableau local dans l’application iOS qui stockait les questions posées? Il est temps de se débarrasser de ça! Des points bonus si vous avez obtenu la référence «Die Hard». :]

Ouvert WebSocketController.swift dans le Questions et réponses sur la conférence Projet Xcode. Au sommet de WebSocketController, ajoutez ce qui suit:

  @Published var questions: [UUID: NewQuestionResponse]

Ce dictionnaire contiendra vos questions.

Ensuite, vous devez définir une valeur initiale dans le init. Ajoutez ceci en haut:

  self.questions = [:]

Ensuite, trouvez handleQuestionResponse(_:) et remplacez le TODO par ce qui suit:

    // 1
    let response = try decoder.decode(NewQuestionResponse.self, from: data)
    DispatchQueue.main.async {
      if response.success, let id = response.id {
        // 2
        self.questionshttps://www.raywenderlich.com/13209594-an-introduction-to-websockets = response
        let alert = Alert(title: Text("New question received!"),
                          message: Text(response.message),
                          dismissButton: .default(Text("OK")) { self.alert = nil })
        self.alert = alert
      } else {
        // 3
        let alert = Alert(title: Text("Something went wrong!"),
                          message: Text(response.message),
                          dismissButton: .default(Text("OK")) { self.alert = nil })
        self.alert = alert
      }
    }

Voici ce que fait ce code:

  1. Il décode le plein NewQuestionResponse message.
  2. Si la réponse a réussi, il stocke la réponse dans le questions dictionnaire et affiche une alerte positive.
  3. Si la réponse échoue, il affiche une alerte d’erreur.

Maintenant, votre prochaine étape consiste à connecter l’interface utilisateur à cette nouvelle source de données. Ouvert ContentView.swift et remplacer List à l’intérieur body avec ce qui suit:

      List(socket.questions.map { $1 }.sorted(), id: .id) { q in
        VStack(alignment: .leading) {
          Text(q.content)
          Text("Status: (q.answered ? "Answered" : "Unanswered")")
            .foregroundColor(q.answered ? .green : .red)
        }
      }

Ce code est essentiellement le même que la version précédente, mais il utilise le socket comme source de données et prend le answered propriété de la question en compte.

Assurez-vous également de retirer le @State var questions Au sommet de ContentView et le temporaire self.questions.append.

Une fois cela fait, créez et exécutez le serveur et l’application iOS. La poignée de main se reproduira, comme avant.

Maintenant, envoyez une question. Le serveur n’enregistrera plus le JSON brut, mais enregistrera plutôt [ INFO ] Got a new question!. Vous verrez également l’alerte de réussite apparaître dans l’application iOS.

Impressionnant!

Répondre à des questions

Pour répondre aux questions, vous utiliserez Feuille pour créer une page d’administration. Feuille est le langage de création de modèles de Vapor pour créer des pages HTML.

Ouvrez le backend websocket projet et ouvrir QuestionsController.swift. Remplacer index avec ce qui suit:

  struct QuestionsContext: Encodable {
    let questions: [Question]
  }

  func index(req: Request) throws -> EventLoopFuture {
    // 1
    Question.query(on: req.db).all().flatMap {
      // 2
      return req.view.render("questions", QuestionsContext(questions: $0))
    }
  }

Voici ce que fait ce code:

  1. Tout sélectionner Question objets de la base de données.
  2. Renvoie la vue feuille avec le des questions objet défini dans le contexte des questions tirées à l’étape 1 ci-dessus.

Avant de voir le tableau de bord en action, vous devez dire Feuille où trouver vos modèles.

Pour ce faire, appuyez sur Commande-Option-R ou, si vous préférez ne pas plier vos doigts dans des positions inconfortables, cliquez sur backend websocket dans le coin supérieur gauche, puis cliquez sur Modifier le schéma….

Cela fait apparaître l’éditeur de schéma. À l’intérieur, accédez au Options et localisez le Directeur de travail réglage.

Vérifier Utiliser un répertoire de travail personnalisé et assurez-vous que le répertoire de travail pointe vers le répertoire contenant votre Package.swift. Par exemple: / Users / lotu / Downloads / Conference_Q & A / Starter / websockets-backend.

Répertoire de travail personnalisé

Maintenant, créez et exécutez le serveur et ouvrez http: // localhost: 8080 dans votre navigateur. Vous verrez un tableau HTML de base avec trois colonnes: Question, Réponse et Réponse. Utilisez l’application iOS pour envoyer une question et actualiser la page.

Vous verrez maintenant une nouvelle ligne de tableau avec votre question, qui a false dans la colonne Réponse et un Répondre bouton dans la dernière colonne.

Cependant, lorsque vous cliquez sur Répondre, vous obtenez une erreur 404 pour le moment. Vous allez résoudre ce problème ensuite.

Implémentation du bouton de réponse

Pour obtenir le Répondre bouton fonctionne correctement, ouvert QuestionsController.swift, trouver index(_:)et ajoutez la nouvelle route directement sous son implémentation:

  func answer(req: Request) throws -> EventLoopFuture {
    // 1
    guard let questionId = req.parameters.get("questionId"),
      let questionUid = UUID(questionId) else {
        throw Abort(.badRequest)
    }
    // 2
    return Question.find(questionUid, on: req.db)
                   .unwrap(or: Abort(.notFound))
                   .flatMap { question in
      question.answered = true
      // 3
      return question.save(on: req.db).flatMapThrowing {
        // 4
        try self.wsController.send(message: 
          QuestionAnsweredMessage(questionId: question.requireID()),
          to: .id(question.askedFrom))
        // 5
        return req.redirect(to: "https://www.raywenderlich.com/")
      }
    }
  }

Ce code va:

  1. Assurez-vous que l’ID de la question à laquelle vous essayez de répondre possède un UUID valide.
  2. Essayez de trouver la question dans la base de données.
  3. Ensemble answered à true et enregistrez la question dans la base de données.
  4. Envoyez une mise à jour au client indiquant que vous avez répondu à la question.
  5. Redirigez vers la page d’index pour afficher le frontal mis à jour.

Enfin, vous devez enregistrer l’itinéraire dans boot comme ça:

    routes.post(":questionId", "answer", use: answer)

OK, il est maintenant temps de voir si les réponses aux questions fonctionnent comme prévu.

Connexion au serveur: avec une vengeance

Oui, plus de blagues «Die Hard». :]Avant de lancer un test de réponse aux questions, vous devez vous assurer que l’application iOS est prête pour cela.

Encore une fois, ouvrez le Questions et réponses sur la conférence Projet Xcode et ouvrir WebSocketController.swift. Ajoutez le code suivant à handleQuestionAnswer(_:):

    // 1
    let response = try decoder.decode(QuestionAnsweredMessage.self, from: data)
    DispatchQueue.main.async {
      // 2
      guard let question = self.questions[response.questionId] else { return }
      question.answered = true
      self.questions[response.questionId] = question
    }

Voici ce qui se passe:

  1. Vous décodez le message complet.
  2. Dans la file d’attente principale, vous mettez à jour la question dans le questions dictionnaire. Cela met également à jour l’interface utilisateur !.

Maintenant, il est temps de s’assurer que tout fonctionne.

Créez et exécutez le serveur principal et l’application iOS. Depuis l’application, saisissez une question et ouvrez le tableau de bord à l’adresse http: // localhost: 8080. Clique le Répondre et regardez avec admiration la façon dont l’interface utilisateur de l’application iOS se met à jour instantanément!

Maintenant, comme toute dernière étape, prenez votre main droite, déplacez-la sur votre épaule gauche et donnez-vous une tape ferme dans le dos. Vous venez de créer votre propre serveur et client WebSocket!

Où aller en partant d’ici?

Téléchargez le projet final à l’aide du Télécharger les matériaux bouton en haut ou en bas de cette page.

Pour en savoir plus sur les WebSockets, lisez le Protocole WebSocket RFC, le Documentation NIOWebSocket ou Vapor’s Documentation WebSocket.

Si vous avez des questions ou des commentaires, rejoignez la discussion du forum ci-dessous!

Close Menu