qtqthreadqtcpsocketqtconcurrentmarble

Handle multiple Qtcp connections concurrently to show on marble widget


I have an application with qt creator that gets geocoordinate data and shows it on the map.it works fine with just one connection but when it comes to having more than once makes it a riddle. I want to get data in a loop from several qtcp connections with different addresses and ports then put data in separated lists and after reading the list of each connection geodata add separate layers on a map in different threads which updates every 3 seconds while all of them getting data concurrently from the network packets. suppose I have many different gps receivers to gather data of their location I want to integrate it on the map. here is my code sample:

1.define list of servers that qtcp client should be connected:

globals.cpp

#define PACKET 50
struct Connects{
    static QMap<QString,int> list()
        {
          QMap<QString,int> m;
          m["sender1.com"] = 4456;
          m["sender2.com"] = 4457;
          m["sender3.com"] = 4458;
          return m;
        }
    static const QMap<QString,int> myMap;

};
QMap<QString, int> const Connects::myMap = list();

2.main window to launch map:

main.cpp

void add_layer(MarbleWidget *mapWidget,MainWindow *window,Client *client,QTimer *timer ){
        //get data and append to a list every 3 sec
        timer = new QTimer;
        QObject::connect(timer,SIGNAL(timeout()),window,SLOT(next_client()));
        QObject::connect(client,SIGNAL(Client_Connected()),window,SLOT(online_slot()));
        timer->start(3000);
        // Add New Layer based on positions in the list
        MPaintLayer* layer = new MPaintLayer(mapWidget);
        for(int j = 0; j < client->data.count(); ++j){
                layer->setPosition(client->data.at(j).Lat,client->data.at(j).Lon);
        }
        mapWidget->addLayer(layer);
}

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
{
    // Load Marble
    MarbleWidget *mapWidget = new MarbleWidget;
    mapWidget->show();
    //iterate over connections address and port list
    QMapIterator<QString,int> i(Connects::myMap);
    while (i.hasNext()) {
        i.next();
        client = new Client;
        client->ConnectToServer(i.key(),i.value());
        //make thread pool and create new thread for adding each layer
        QThreadPool pool;
        QFuture<void> tr;
        tr = QtConcurrent::run(&pool,add_layer,mapWidget,this,client,timer);
    }
}
void MainWindow::online_slot()
{
     //fetch data from the buffer and separate each entity based on size
     while (client->GetLanBufferSize() >= PACKET) {
          client->ReadData((char*)buffer,PACKET);
          double lat,lon;
          memcpy(&lat,&buffer[8],sizeof(double));
          memcpy(&lon,&buffer[16],sizeof(double));
          //put buffer data to target list which is defined in client
          Target trg;
          client->data.append(trg);
     }
    //remove has-readed data
    for (int var = 0; var < client->data.count(); ++var) {
        client->data.removeAt(var);
        var--;
    }
}

3.declare class for add or update a new layer based on recieved data on the map:

paintlayer.cpp

MPaintLayer::MPaintLayer(MarbleWidget* widget) 
{
    S_N = QPixmap(":/mode-s.png");
}
bool MPaintLayer::render( GeoPainter *painter,ViewportParams *viewport,
                          const QString& renderPos, GeoSceneLayer * layer )
{
    //throw a tiny png file on map based on retrieved locations
    for (int var = 0; var < pos.count(); ++var) {
        painter->drawPixmap(pos[var],S_N));
    }
    return true;
}
void MPaintLayer::setPosition(double lat,double lon)
{
  pos.append(GeoDataCoordinates(lat,lon,0,GeoDataCoordinates::Degree));

}

5.define a class for the format of received data:

client.h

class Target
{
public:
    Target()
    {
        Lat = Lon = 0;
    }
    double Lat,Lon;
    GeoDataCoordinates Position;
    void update(double lat,double lon)
    {
        Lat = lat;
        Lon = lon;
        Position = GeoDataCoordinates(Lon,Lat,0,GeoDataCoordinates::Degree);
    }
};

6.declare class for making new qtcp connection and receive data:

client.cpp

Client::Client(QObject *parent) : QObject(parent)
{
    socket = new QTcpSocket(this);
    connect(socket, SIGNAL(connected()),this, SLOT(on_connected()));
}
void Client::on_connected()
{
    connect(socket, SIGNAL(disconnected()),this, SLOT(on_Disconnected()));
    socket->setReadBufferSize(12e6);
    emit Client_Connected();
}
bool Client::ReadData(char *buffer, int Leanth)
{
    socket->read(buffer , Leanth);
    return true;
}
int Client::GetLanBufferSize()
{
    return socket->bytesAvailable();
}
void Client::on_Disconnected()
{
    emit Client_DisConnected();
}
void Client::ConnectToServer(QString Address , int Port)
{
    socket->connectToHost(Address, Port);
    server = Address;
    server_Port = Port;
}

the data format which is received from the senders is location of moving object something like this:

35.51243 51.22478
35.51260 51.22667
35.69270 51.03961

what is the best practice solution? because it shows nothing on the map. when I used hard code, it showed layers equivalent to each connection but reading stream of data is challengeable. any clue would be appreciated.


Solution

  • You are not using QTcpSocket in its proper way. In your Client class, you can handle all reading stuff and you won't even need those while loops. Use QIODevice::readyRead() signal and QIODevice::read(char *data, qint64 maxSize) method inside a slot which should read data and parse it.

    The problem here is that it's not guaranteed that those data will be received with alignments happening on the sender side. For example, if you send >>>SOME_LENGTHY_DATA<<< from the sender, QTcpSocket may emit readyRead() signal two times and you can read >>>SOME_LENG and THY_DATA<<< in two different calls to your reading slot.

    So, you should take care of parsing your data as data is coming in byte by byte. You are free to buffer the incoming data somewhere in your class or just use the QTcpSocket internal buffer. You should really take care of separating message chunks that are coming in.

    Here is an implementation of Client. I use QPointF to store point instead of your Target class because I don't know what it is. You are free to create your own class and append it to Client::data based on parsed lat/lon. I did not test the code but it should work. I'm not sure if you ever need concurrency with this implementation but you won't need one IMHO.

    //Client.h
    
    class Client : public QTcpSocket
    {
        Q_OBJECT
    public:
        explicit Client(QObject *parent = nullptr);
    
    private:
        QString serverAddress;
        int serverPort;
        QList<QPointF> data; //you can use your own type, just for test
    
        QByteArray lastLine;
    
        void parseData();
    
    signals:
        void Client_Connected();
    
    public slots:
        void connectToServer(const QString& address, quint16 port);
        void processData();
    
    };
    
    //Client.cpp
    
    #include "client.h"
    
    Client::Client(QObject *parent) : QTcpSocket (parent)
    {
        // we don't really want Client_Connected signal!
        // you can use QTcpSocket::connected signal
        connect(this, SIGNAL(connected()),this, SIGNAL(Client_Connected()));
        // readyRead will be emitted when there is data in buffer to be read
        connect(this, SIGNAL(readyRead()),this, SLOT(processData()));
    }
    
    void Client::parseData()
    {
        QString str(lastLine);
        str = str.trimmed();
        QStringList parts = str.split(" ",QString::SkipEmptyParts);
        if(parts.count()==2){
            bool success = true;
            double latitude = success ? parts[0].toDouble(&success) : 0;
            double longitude = success ? parts[1].toDouble(&success) : 0;
            if(success)
                data.append(QPointF(longitude,latitude));
        }
    }
    
    void Client::connectToServer(const QString& address, quint16 port)
    {
        data.clear();
        lastLine.clear();
    
        serverPort = port;
        serverAddress = address;
        this->connectToHost(address,port);
    }
    
    void Client::processData()
    {
        QByteArray d = this->readAll();
        for(int i = 0 ; i < d.count() ; ++i){
            if(d.at(i)=='\r')
            {
                parseData();
                lastLine.clear();
            }
            else
                lastLine.append(d.at(i));
        }
    }