2013-12-12 3 views
9

Я знаю, что некоторые подобные вопросы, возможно, уже заданы, но ответы на те, которые я нашел, охватывали очень специфические проблемы, и я до сих пор не понял этого.QTcpSocket: чтение и письмо

В моей программе я создаю QObject (QPeer), который использует QTcpSocket для связи с другим таким объектом по сети. QPeer имеет слот, который принимает QByteArray с данными (sendData(QByteArray)). Все содержимое этого массива считается одним «сообщением», и они записываются в сокет. Я хочу сделать следующее: каждый раз, когда сообщение записывается, я хочу, чтобы принимающий QPeer выдавал свой сигнал dataReceived(QByteArray) ровно один раз, что QByteArray содержит все сообщение. (Примечание:. Все сигналы/слоты, как частные, соединяющие QPeer с его гнездом и общественные, такие как те, sendData(QByteArray) сериализуются с помощью Qt::QueuedConnection всякий раз, когда это необходимо)

Я использую сигнал QTcpSocket::readyRead() для асинхронного чтения из сокета. Теперь я знаю, что не могу просто позвонить QTcpSocket::write() один раз в sendData, а затем предположить, что для каждой записи, которую я делаю, QTcpSocket с другой стороны производит ровно один сигнал readyRead. И что же мне делать?

Это моя идея, пожалуйста, скажите мне, если это будет работать:

ЗАПИСЬ:

void QPeer::sendData(QByteArray data) 
{ 
    // TODO: write data.size() as raw int of exactly 4 bytes to socket 
    const char *bytes = data.constData(); 
    int bytesWritten = 0; 
    while (bytesWritten < data.size()) 
     bytesWritten += _socket->write(bytes + bytesWritten); 
} 

ЧТЕНИЕ:

теперь я хочу функцию чтения (подключенный к QTcpSocket::readyRead()) использовать заголовок (4 байта int, определяющий длину сообщения), а затем прочитайте это количество байтов; next emit dataReceived с точно этими байтами. У меня серьезные проблемы с этим. Например: что делать, если readyRead испускается, и я могу прочитать заголовок сообщения, но не количество байтов, указанное? Или что, если заголовок был получен только частично?

1. Как правильно записать заголовок (4 байта int) в сокет?

2. Как правильно реализовать функцию чтения, чтобы она выполняла то, что я хочу?

Любые советы приветствуются. Благодаря!

ответ

33

Я работал над проектом, который делает то, что вы ожидаете, смотрите здесь решение, которое я разработал для наших проблем, упрощение будет легче понять:

Отредактировано, добавлена ​​поддержка сервера с несколькими клиентами.

client.h:

#include <QtCore> 
#include <QtNetwork> 

class Client : public QObject 
{ 
    Q_OBJECT 
public: 
    explicit Client(QObject *parent = 0); 

public slots: 
    bool connectToHost(QString host); 
    bool writeData(QByteArray data); 

private: 
    QTcpSocket *socket; 
}; 

client.cpp:

#include "client.h" 

static inline QByteArray IntToArray(qint32 source); 

Client::Client(QObject *parent) : QObject(parent) 
{ 
    socket = new QTcpSocket(this); 
} 

bool Client::connectToHost(QString host) 
{ 
    socket->connectToHost(host, 1024); 
    return socket->waitForConnected(); 
} 

bool Client::writeData(QByteArray data) 
{ 
    if(socket->state() == QAbstractSocket::ConnectedState) 
    { 
     socket->write(IntToArray(data.size())); //write size of data 
     socket->write(data); //write the data itself 
     return socket->waitForBytesWritten(); 
    } 
    else 
     return false; 
} 

QByteArray IntToArray(qint32 source) //Use qint32 to ensure that the number have 4 bytes 
{ 
    //Avoid use of cast, this is the Qt way to serialize objects 
    QByteArray temp; 
    QDataStream data(&temp, QIODevice::ReadWrite); 
    data << source; 
    return temp; 
} 

сервера.ч:

#include <QtCore> 
#include <QtNetwork> 

class Server : public QObject 
{ 
    Q_OBJECT 
public: 
    explicit Server(QObject *parent = 0); 

signals: 
    void dataReceived(QByteArray); 

private slots: 
    void newConnection(); 
    void disconnected(); 
    void readyRead(); 

private: 
    QTcpServer *server; 
    QHash<QTcpSocket*, QByteArray*> buffers; //We need a buffer to store data until block has completely received 
    QHash<QTcpSocket*, qint32*> sizes; //We need to store the size to verify if a block has received completely 
}; 

server.cpp:

#include "server.h" 

static inline qint32 ArrayToInt(QByteArray source); 

Server::Server(QObject *parent) : QObject(parent) 
{ 
    server = new QTcpServer(this); 
    connect(server, SIGNAL(newConnection()), SLOT(newConnection())); 
    qDebug() << "Listening:" << server->listen(QHostAddress::Any, 1024); 
} 

void Server::newConnection() 
{ 
    while (server->hasPendingConnections()) 
    { 
     QTcpSocket *socket = server->nextPendingConnection(); 
     connect(socket, SIGNAL(readyRead()), SLOT(readyRead())); 
     connect(socket, SIGNAL(disconnected()), SLOT(disconnected())); 
     QByteArray *buffer = new QByteArray(); 
     qint32 *s = new qint32(0); 
     buffers.insert(socket, buffer); 
     sizes.insert(socket, s); 
    } 
} 

void Server::disconnected() 
{ 
    QTcpSocket *socket = static_cast<QTcpSocket*>(sender()); 
    QByteArray *buffer = buffers.value(socket); 
    qint32 *s = sizes.value(socket); 
    socket->deleteLater(); 
    delete buffer; 
    delete s; 
} 

void Server::readyRead() 
{ 
    QTcpSocket *socket = static_cast<QTcpSocket*>(sender()); 
    QByteArray *buffer = buffers.value(socket); 
    qint32 *s = sizes.value(socket); 
    qint32 size = *s; 
    while (socket->bytesAvailable() > 0) 
    { 
     buffer->append(socket->readAll()); 
     while ((size == 0 && buffer->size() >= 4) || (size > 0 && buffer->size() >= size)) //While can process data, process it 
     { 
      if (size == 0 && buffer->size() >= 4) //if size of data has received completely, then store it on our global variable 
      { 
       size = ArrayToInt(buffer->mid(0, 4)); 
       *s = size; 
       buffer->remove(0, 4); 
      } 
      if (size > 0 && buffer->size() >= size) // If data has received completely, then emit our SIGNAL with the data 
      { 
       QByteArray data = buffer->mid(0, size); 
       buffer->remove(0, size); 
       size = 0; 
       *s = size; 
       emit dataReceived(data); 
      } 
     } 
    } 
} 

qint32 ArrayToInt(QByteArray source) 
{ 
    qint32 temp; 
    QDataStream data(&source, QIODevice::ReadWrite); 
    data >> temp; 
    return temp; 
} 

Примечание: Не следует использовать этот метод для передачи больших файлов, так как с помощью этого метода всего содержимого сообщения помещаются внутри памяти до отправки и это приводит к высокому использованию памяти. И поскольку 32 бита, подписанные INT, имеют максимальное значение до 2,147,483,647, если ваши входные данные имеют значение выше, чем в байтах, это не сработает. Береги себя.

+0

Большое спасибо! Предыдущий ответ сработал, но это заставило меня пересмотреть мой класс. Все стало немного ... противно. – DiscobarMolokai

+0

Однако быстрый вопрос: когда ваш сервер получает своего первого клиента (newConnection вызывается в первый раз), член сокета содержит указатель на сокет для этого соединения. Но что, если через несколько мгновений встретится новое входящее соединение, а newConnection вызывается снова. Тогда предыдущий сокет потерян, не так ли? Или механизм сигнала/слота по-прежнему работает на этом сокете? – DiscobarMolokai

+0

Сигнал будет излучаться, потому что сокет является указателем, он не будет уничтожен в конце области действия, за исключением случаев, когда вы вызываете 'delete' или используете интеллектуальный указатель, однако вы больше не можете получать данные из этого соединения, метод 'readyRead()' будет работать только с указателем, хранящимся в нашей переменной (последний подключенный клиент). Для работы с несколькими клиентами вам потребуется создать несколько экземпляров класса, содержащего ваш собственный указатель сокета и соответствующий буфер. –

2

Как вы сказали, вам нужно дождаться, что ваш заголовок будет полностью отправлен, прежде чем читать его, а затем прочитайте хорошее количество байтов и испустите сигнал для доступности данных.

Вот пример (непроверенные):

//header file 

class Peer { 
//[...] 
protected: 
    bool m_headerRead; //initialize to false 
    unsigned int m_size_of_data_to_read; 
//[...] 
}; 

//source file 
void QPeer::sendData(QByteArray data) 
{ 
    int size = data.size(); 
    _socket->write((const char*) &size, sizeof(int); 
    //use directly QIODevice::write(QByteArray) 
    _socket->write(data); 
} 

void QPeer::readData() 
{ 
    int bytes = _socket->bytesAvailable(); 
    bool contains_enough_data = true; 

    while (contains_enough_data) { 
     if (! m_headerRead && _socket->bytesAvailable() >= sizeof(int)) { 
     //read header only and update m_size_of_data_to_read 
     m_headerRead = true; 
     } else if (m_headerRead && _socket->bytesAvailable >= m_size_of_data_to_read) { 
      //read data here 
      m_headerRead = false; 
      emit dataAvailable(); 
     } else { 
      contains_enough_data = false; //wait that further data arrived 
     } 
    } 
} 
+0

Итак, вы говорите, что '_socket-> write (data)' гарантирует, что весь QByteArray будет написан сразу? Поскольку документация Qt говорит об этом: _qint64 QIODevice :: write (const QByteArray & byteArray) Это перегруженная функция. Записывает содержимое byteArray на устройство. Возвращает количество байтов, которые были написаны на самом деле, или -1, если произошла ошибка. Итак, он говорит, что возвращает количество фактически записанных байтов. Зачем это делать, если весь массив будет написан все одновременно? – DiscobarMolokai

+0

Хорошо, я всегда использую этот метод, когда у меня есть 'QByteArray' для записи и проверки ошибки на -1. Ваш предыдущий способ сделать это неплохо, вы можете спокойно заменить его этим. Я буду искать, если QByteArray всегда полностью написан. – jbh

+0

Я не проверяю ошибку -1; но я подключил сигнал ошибки сокета к слоту в QPeer, который обрабатывает ошибки. Этого достаточно? Спасибо за помощь! Ваша реализация отлично работает, я не тестировал ее полностью, но это прекрасно, спасибо. – DiscobarMolokai

Смежные вопросы