Implementación del trabajo con el servidor Long Poll en el cliente VKontakte para Sailfish OS

Introduccion


Desafortunadamente, incluso ahora, en el mundo moderno, no siempre es posible aprovechar los beneficios de la tecnología push y, a veces, es necesario implementar soluciones alternativas, por ejemplo, en forma de Long Poll, que le permite emular el mecanismo de las notificaciones push. En particular, surgió tal necesidad al implementar el cliente VKontakte para el sistema operativo Sailfish .

Este artículo no analizará los principios de interacción con el servidor Long Poll VKontakte: tiene documentación muy detallada y ya se han publicado ejemplos básicos anteriormente . En cambio, se considerará una implementación práctica para una plataforma específica.

Se entiende que el lector está familiarizado con el desarrollo del sistema operativo Sailfish no solo en QML , sino también en C ++ .

Encuesta larga al cliente


La clase de cliente principal es la clase LongPoll , que consulta el servidor Long Poll y analiza sus respuestas.

El método getLongPollServer , cuya tarea es obtener información para abrir una conexión con el servidor, se llama durante la inicialización de la aplicación, lo que le permite recibir actualizaciones de usuario de inmediato:

 /** *       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 solicitud se completa con éxito, la información de conexión con el servidor Long Poll se guarda y la conexión se abre utilizando el método 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(); //     } 

En el método doLongPollRequest Long Poll, los parámetros de conexión necesarios se pasan al servidor:

 /* *     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  } 

Vale la pena señalar que el valor del campo de mode , igual a 10, se obtuvo agregando la opción de recibir archivos adjuntos (2) y devolviendo un conjunto extendido de eventos (8).

Como respuesta a la apertura de una conexión, el servidor devuelve un JSON que contiene los últimos eventos. La respuesta se procesa en el método 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(); //     } 

El campo failed en la respuesta puede tomar cuatro valores, pero solo uno de ellos, igual a uno, no requiere una solicitud repetida de información para conectarse al servidor Long Poll. Por esta razón, la condición se agregó al código.

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

El método parseLongPollUpdates es un ciclo simple para todos los eventos entrantes con una verificación de su tipo:

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

El código muestra que para cada nuevo evento Long Poll, el cliente envía una señal que debe ser procesada por otra parte de la aplicación. El argumento de la señal no es el objeto del evento completo, sino solo sus partes necesarias. Por ejemplo, la señal gotNewMessage transmite solo el identificador de un nuevo mensaje, por el cual se solicita su contenido completo:

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

Como resultado de esta función de línea única , se realiza una solicitud al servidor VKontakte para obtener información completa sobre el mensaje mediante su identificador con la creación posterior del objeto de este mensaje. En conclusión, se envía una señal asociada con la interfaz de usuario, transmitiendo datos sobre un nuevo mensaje, que se muestra en el panel de notificaciones:

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

Interfaz de diálogo


Ahora, según los principios de interacción del cliente con el servidor Long Poll y los principios de transferir la información recibida a la interfaz de usuario, podemos considerar un ejemplo de actualización de un diálogo abierto.

Lo primero que llama la atención es el componente 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 //      } } } 

La ranura onUserTyping procesa el evento de un conjunto por el interlocutor del mensaje al mostrar la notificación correspondiente al usuario. Aquí, en el primer paso, se obtiene el identificador de la sala (la sala significa el término generalizado para diálogos y chats), y en la segunda etapa, se muestra una notificación si el identificador recibido y el identificador de la sala actual coinciden.

Vale la pena señalar que una notificación sobre un conjunto de mensajes se muestra durante diez segundos, si durante este tiempo no ha llegado un nuevo evento que activará la notificación nuevamente. Esto se logra usando el componente 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 //       } } 

El espacio onSavedPhoto responsable de procesar el final de la carga de imágenes en los mensajes, lo que está más allá del alcance del artículo actual.

La segunda cosa que es de interés es la lista de mensajes:

 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 {} //      } 

Aquí, el componente MessageItem es responsable de mostrar un solo mensaje. Su consideración está más allá del alcance de este artículo.

Los mensajes en sí se toman del modelo vksdk.messagesModel . Este modelo es una lista de objetos Message que se pueden actualizar en tiempo real mediante los métodos add , addProfile , readMessages , readMessages y 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); } 

Común a los cinco métodos es el uso de la señal dataChanged , que indica que los datos se han actualizado en el modelo. La emisión de esta señal actualiza los elementos de SilicaListView para mostrar el estado actual de los mensajes. La adición de mensajes a SilicaListView realiza llamando a los endInsertRows beginInsertRows y endInsertRows que envían las rowsInserted rowsAboutToBeInserted y rowsInserted respectivamente. Como resultado, el usuario verá nuevos mensajes y su estado en tiempo real en el cuadro de diálogo.

Conclusión


Este artículo examinó la interacción con el servidor Long Poll al desarrollar para el sistema operativo Sailfish utilizando la aplicación VK como ejemplo. Se consideraron algunas características de la implementación del cliente y una forma de actualizar la interfaz de usuario en tiempo real. El código para la aplicación descrita en este artículo está disponible en GitHub.

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


All Articles