Implémentation du travail avec le serveur Long Poll dans le client VKontakte pour Sailfish OS

Présentation


Malheureusement, même maintenant, dans le monde moderne, il n'est pas toujours possible de profiter des avantages de la technologie push, et parfois vous devez implémenter des solutions de contournement, par exemple, sous la forme d'un sondage long, qui vous permet d'émuler le mécanisme des notifications push. En particulier, un tel besoin est apparu lors de l'implémentation du client VKontakte pour Sailfish OS .

Cet article ne traitera pas des principes d'interaction avec le serveur Long Poll VKontakte - il contient une documentation très détaillée et des exemples de base ont déjà été publiés précédemment . Au lieu de cela, une mise en œuvre pratique pour une plate-forme spécifique sera envisagée.

Il est entendu que le lecteur est familier avec le développement de Sailfish OS non seulement en QML , mais aussi en C ++ .

Client Long Poll


La classe client principale est la classe LongPoll , qui interroge le serveur Long Poll et analyse ses réponses.

La méthode getLongPollServer , dont la tâche est d'obtenir des informations pour ouvrir une connexion au serveur, est appelée lors de l'initialisation de l'application, ce qui vous permet de recevoir immédiatement les mises à jour utilisateur:

 /** *       Long Poll  . */ void LongPoll::getLongPollServer() { QUrl url("https://api.vk.com/method/messages.getLongPollServer"); //    API QUrlQuery query; query.addQueryItem("access_token", _accessToken); //  Access Token query.addQueryItem("v", "5.53"); //    API url.setQuery(query); //       _manager->get(QNetworkRequest(url)); //  GET-    } 

Si la demande aboutit, les informations de connexion avec le serveur Long Poll sont enregistrées et la connexion est ouverte à l'aide de la méthode doLongPollRequest :

 /* *      . * @:param: reply --    . */ void LongPoll::finished(QNetworkReply* reply) { QJsonDocument jDoc = QJsonDocument::fromJson(reply->readAll()); //    JSON if (_server.isNull() || _server.isEmpty()) { //      QJsonObject jObj = jDoc.object().value("response").toObject(); _server = jObj.value("server").toString(); //    _key = jObj.value("key").toString(); //    _ts = jObj.value("ts").toInt(); //     doLongPollRequest(); //    Long Poll  } else { // ... //     // ... } reply->deleteLater(); //     } 

Dans la méthode doLongPollRequest Long Poll, les paramètres de connexion nécessaires sont transmis au serveur:

 /* *     Long Poll . */ void LongPoll::doLongPollRequest() { QUrl url("https://" + _server); //    QUrlQuery query; query.addQueryItem("act", "a_check"); //     query.addQueryItem("key", _key); //   query.addQueryItem("ts", QString("%1").arg(_ts)); //    query.addQueryItem("wait", "25"); //  25   query.addQueryItem("mode", "10"); //       url.setQuery(query); //       _manager->get(QNetworkRequest(url)); //  GET-  Long Poll  } 

Il convient de noter que la valeur du champ mode , égale à 10, a été obtenue en ajoutant l'option de réception des pièces jointes (2) et en renvoyant un ensemble étendu d'événements (8).

En réponse à l'ouverture d'une connexion, le serveur renvoie un JSON contenant les derniers événements. La réponse est traitée dans la méthode finished :

 /* *      . * @:param: reply --    . */ void LongPoll::finished(QNetworkReply* reply) { QJsonDocument jDoc = QJsonDocument::fromJson(reply->readAll()); //    JSON if (_server.isNull() || _server.isEmpty()) { // ... //    // ... } else { QJsonObject jObj = jDoc.object(); if (jObj.contains("failed")) { //       if (jObj.value("failed").toInt() == 1) { //    _ts = jObj.value("ts").toInt(); //      doLongPollRequest(); //    Long Poll  } else { _server.clear(); //    _key.clear(); //    _ts = 0; //     getLongPollServer(); //      } } else { //      _ts = jObj.value("ts").toInt(); //      parseLongPollUpdates(jObj.value("updates").toArray()); //     doLongPollRequest(); //    Long Poll  } } reply->deleteLater(); //     } 

Le champ ayant failed dans la réponse peut prendre quatre valeurs, mais une seule d'entre elles, égale à une, ne nécessite pas de demande répétée d'informations pour se connecter au serveur d'interrogation longue. Pour cette raison, la condition a été ajoutée au code.

 jObj.value("failed").toInt() == 1 

La méthode parseLongPollUpdates est un cycle simple pour tous les événements entrants avec une vérification de leur type:

 enum LONGPOLL_EVENTS { NEW_MESSAGE = 4, //   INPUT_MESSAGES_READ = 6, //    OUTPUT_MESSAGES_READ = 7, //    USER_TYPES_IN_DIALOG = 61, //      USER_TYPES_IN_CHAT = 62, //      UNREAD_DIALOGS_CHANGED = 80, //     }; /* *   ,   Long Poll . * @:param: updates --    . */ void LongPoll::parseLongPollUpdates(const QJsonArray& updates) { for (auto value : updates) { //     QJsonArray update = value.toArray(); //    switch (update.at(0).toInt()) { //    case NEW_MESSAGE: emit gotNewMessage(update.at(1).toInt()); break; case INPUT_MESSAGES_READ: emit readMessages(update.at(1).toInt(), update.at(2).toInt(), false); break; case OUTPUT_MESSAGES_READ: emit readMessages(update.at(1).toInt(), update.at(2).toInt(), true); break; case USER_TYPES_IN_DIALOG: emit userTyping(update.at(1).toInt(), 0); break; case USER_TYPES_IN_CHAT: emit userTyping(update.at(1).toInt(), update.at(2).toInt()); break; case UNREAD_DIALOGS_CHANGED: emit unreadDialogsCounterUpdated(update.at(1).toInt()); break; default: break; } } } 

Le code montre que pour chaque nouvel événement Long Poll, le client envoie un signal qui doit être traité par une autre partie de l'application. L'argument signal n'est pas l'objet événement entier, mais seulement ses parties nécessaires. Par exemple, le signal gotNewMessage ne transmet que l'identifiant d'un nouveau message, par lequel son contenu complet est demandé :

 void VkSDK::_gotNewMessage(int id) { _messages->getById(id); } 

Du fait de cette fonction monoligne , une demande est faite au serveur VKontakte pour obtenir des informations complètes sur le message par son identifiant avec la création ultérieure de l' objet de ce message. En conclusion, un signal est envoyé associé à l'interface utilisateur, transmettant des données sur un nouveau message, qui s'affiche dans le panneau de notification:

 import QtQuick 2.0 //      QML import Sailfish.Silica 1.0 //      Sailfish OS import org.nemomobile.notifications 1.0 //      ApplicationWindow //   { // ... //   // ... Notification { //     id: commonNotification //    category: "harbour-kat" //   remoteActions: [ //  -    { "name": "default", "service": "nothing", "path": "nothing", "iface": "nothing", "method": "nothing" } ] } Connections { //     target: vksdk //    SDK  onGotNewMessage: { //      commonNotification.summary = name //      commonNotification.previewSummary = name //     commonNotification.body = preview //      commonNotification.previewBody = preview //     commonNotification.close() //      commonNotification.publish() //    } } } 

Interface de dialogue


Maintenant, sur la base des principes de l'interaction du client avec le serveur Long Poll et des principes de transfert des informations reçues vers l'interface utilisateur, nous pouvons considérer un exemple de mise à jour d'un dialogue ouvert.

La première chose qui attire votre attention est le composant Connections :

 Connections { //     target: vksdk //    SDK  onSavedPhoto: { //       attachmentsList += name + ","; //       attachmentsBusy.running = false; //     } onUserTyping: { //       var tempId = userId; //      if (chatId !== 0) { //    tempId = chatId; //  ,     } if (tempId === historyId) { //       typingLabel.visible = true //      } } } 

Le créneau onUserTyping traite l'événement d'un ensemble par l'interlocuteur du message en affichant la notification correspondante à l'utilisateur. Ici, à la première étape, l'identifiant de la salle est obtenu (la salle signifie le terme généralisé pour les dialogues et les chats), et à la deuxième étape, une notification est affichée si l'identifiant reçu et l'identifiant de la salle actuelle correspondent.

Il convient de noter qu'une notification concernant un ensemble de messages s'affiche pendant dix secondes, si pendant ce temps il n'y a pas eu de nouvel événement qui active à nouveau la notification. Ceci est réalisé en utilisant le composant Timer :

 Label { //     id: typingLabel //    anchors.bottom: newmessagerow.top //       width: parent.width //     horizontalAlignment: Text.AlignHCenter //      font.pixelSize: Theme.fontSizeExtraSmall //    color: Theme.secondaryColor //     text: qsTr("typing...") //    visible: false //      onVisibleChanged: if (visible) typingLabelTimer.running = true //     Timer { //    id: typingLabelTimer //    interval: 10000 //   --   onTriggered: typingLabel.visible = false //       } } 

La fente onSavedPhoto responsable du traitement de la fin du chargement de l'image dans les messages, ce qui dépasse le cadre de l'article actuel.

La deuxième chose qui est intéressante est la liste des messages:

 SilicaListView { //    id: messagesListView //    //        : anchors.left: parent.left anchors.right: parent.right //            : anchors.top: parent.top anchors.bottom: typingLabel.top verticalLayoutDirection: ListView.BottomToTop //      clip: true //   ,     model: vksdk.messagesModel //    delegate: MessageItem { //     //        : anchors.left: parent.left anchors.right: parent.right //     : userId: fromId //   date: datetime //    out_: out //    read_: read //    avatarSource: avatar //    bodyText: body //   photos: photosList //     videos: videosList //     audios: audiosList //     documents: documentsList //     links: linksList //     news: newsList //       geoTile: geoTileUrl //     geoMap: geoMapUrl //         fwdMessages: fwdMessagesList //    Component.onCompleted: { //      if (index === vksdk.messagesModel.size-1) { //       //      : vksdk.messages.getHistory(historyId, vksdk.messagesModel.size) } } } VerticalScrollDecorator {} //      } 

Ici, le composant MessageItem est responsable de l'affichage d'un seul message. Son examen dépasse le cadre de cet article.

Les messages eux-mêmes sont extraits du modèle vksdk.messagesModel . Ce modèle est une liste d'objets Message qui peuvent être mis à jour en temps réel à l'aide clear méthodes add , prepend , addProfile , readMessages et clear :

 /* *    . */ void MessagesModel::clear() { beginRemoveRows(QModelIndex(), 0, _messages.size()); //     _messages.clear(); //   _profiles.clear(); //    endRemoveRows(); //     //    : QModelIndex index = createIndex(0, 0, nullptr); emit dataChanged(index, index); } /* *      . * @:param: message --    . */ void MessagesModel::add(Message* message) { //   : beginInsertRows(QModelIndex(), _messages.size(), _messages.size()); _messages.append(message); //    endInsertRows(); //    //    : QModelIndex index = createIndex(0, 0, static_cast<void *>(0)); emit dataChanged(index, index); } /* *       . * @:param: message --    . */ void MessagesModel::prepend(Message* message) { //        : if (_messages.isEmpty()) return; if (message->chat() && _messages.at(0)->chatId() != message->chatId()) return; if (!message->chat() && _messages.at(0)->userId() != message->userId()) return; beginInsertRows(QModelIndex(), 0, 0); //    _messages.insert(0, message); //   endInsertRows(); //    //    : QModelIndex index = createIndex(0, _messages.size(), nullptr); emit dataChanged(index, index); } /* *       . * @:param: profile --    . */ void MessagesModel::addProfile(Friend* profile) { //   ,       if (_profiles.contains(profile->id())) return; _profiles[profile->id()] = profile; //    : QModelIndex startIndex = createIndex(0, 0, nullptr); QModelIndex endIndex = createIndex(_messages.size(), 0, nullptr); emit dataChanged(startIndex, endIndex); } /* *    . * @:param: peerId --  . * @:param: localId --    . * @:param: out --     . */ void MessagesModel::readMessages(qint64 peerId, qint64 localId, bool out) { //        : if (_messages.isEmpty()) return; if (_messages.at(0)->chat() && _messages.at(0)->chatId() != peerId) return; if (!_messages.at(0)->chat() && _messages.at(0)->userId() != peerId) return; foreach (Message *message, _messages) { //       if (message->id() <= localId && message->isOut() == out) //    message->setReadState(true); //    } //    : QModelIndex startIndex = createIndex(0, 0, nullptr); QModelIndex endIndex = createIndex(_messages.size(), 0, nullptr); emit dataChanged(startIndex, endIndex); } 

L'utilisation des signaux dataChanged , qui indique que les données ont été mises à jour dans le modèle, est commune aux cinq méthodes. L'émission de ce signal met à jour les éléments SilicaListView pour afficher l'état actuel des messages. L'ajout de messages à SilicaListView fourni en appelant les endInsertRows beginInsertRows et endInsertRows qui envoient rowsInserted rowsAboutToBeInserted et rowsInserted . En conséquence, l'utilisateur verra les nouveaux messages et leur statut en temps réel dans la boîte de dialogue.

Conclusion


Cet article a examiné l'interaction avec le serveur Long Poll lors du développement pour Sailfish OS en utilisant l'exemple de l'application VK. Certaines fonctionnalités de l'implémentation client et un moyen de mettre à jour l'interface utilisateur en temps réel ont été envisagés. Le code de l'application décrite dans cet article est disponible sur GitHub.

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


All Articles