Pull to refresh

Работа с сокетами в Qt

Reading time5 min
Views127K

Введение


image
Как-то несколько лет назад, на одном из форумов, я нашел такую замечательную фразу — «Каждый уважающий себя программист в жизни должен написать свой чат-клиент». Тогда мои знания не позволяли сделать это. Я просто улыбнулся и прошел мимо этой фразы. Но вот совсем недавно я столкнулся именно с данной проблемой — нужно было написать свой чат. Ну а так как последнее время мой интерес был направлен на изучение и разработку Qt-приложений, на чем будет сделан он, решилось само собой.

Работа с сетью в Qt осуществляется через QtNetwork. А для того чтобы проект начал поддерживать его, в .pro файле нужно дописать QT += network. Qt поддерживает несколько типов сокетного соединения. Основные это: QUdpSocket и QTcpSocket. QUdpSocket — это дейтаграммный сокет, для осуществления обмена пакетами данных. С помощью этого сокета данные отправляются без проверки дошли ли данные или нет. QTcpSocket же устанавливает связь точка-точка и предоставляет дополнительные механизмы, направленные против искажения и потери данных.

Оба класса наследованы от класса QAbstractSocket. Также есть класс QSslSocket, но мне он на тот момент был не нужен, поэтому затрагивать его не буду.

Чат


Когда начал проектировать архитектуру приложения, стал вопрос: «А собственно как будут взаимодействовать пользователи чата между собой?». Просто сам чат должен был поддерживать как общее окно чата, так и приват-сообщения с возможностью передачи файлов. Если с передачей файлов было все ясно — тут только TCP, так как должен быть контроль и проверка пришли ли пакеты, то вот с основным чатом были трудности. Ведь в чате нет четко выраженного сервера. Да и если взаимодействие их сделать через TCP, то для каждого пользователя нужно будет выделить порт, что немного накладно. Ну а раз не TCP, значит UDP! Он полностью подходит для этого. Прослушивая определенный порт, мы получаем сообщения от пользователя, который отправил их на тот же порт всем. И если какое-то сообщение чата потеряется, это будет не так страшно.

QUdpSocket


Как оказалось, работать с классом QUdpSocket не просто, а очень просто. Создаем объект класса, биндим на указанный порт, соединяем с сигналом класса QIODevice и ждем прихода сообщений на этот порт.
UdpChat(QString nick, int port) {
  nickname = nick;
  socket = new QUdpSocket(this);
  _port = port;
  socket->bind(QHostAddress::Any, port);
  // тут еще какой то код конструктора //
  connect(socket, SIGNAL(readyRead()), SLOT(read()));
}

* This source code was highlighted with Source Code Highlighter.

Еще хотелось бы сказать перед следующим блоком кода, что в нашем случае чат не знает количество людей в сети. Мы будем делать просто широковещательный запрос. Но для того, чтобы сформировать список людей, будем отправлять что-то типа «Кто в сети?», на что получившие данное сообщение отправят «Я!». Таким образом у нас будет 3 типа сообщения:
  1. обычное сообщения от пользователя
  2. сообщения — пользователь в сети
  3. сообщение — кто есть в сети?

В итоге получаем:
void send(QString str, qint8 type) {
  QByteArray data;
  QDataStream out(&data, QIODevice::WriteOnly);
  out << qint64(0);
  out << qint8(type);
  out << str;
  out.device()->seek(qint64(0));
  out << qint64(data.size() - sizeof(qint64));
  socket->writeDatagram(data, QHostAddress::Broadcast, _port);
}

void read() {
  QByteArray datagram;
  datagram.resize(socket->pendingDatagramSize());
  QHostAddress *address = new QHostAddress();
  socket->readDatagram(datagram.data(), datagram.size(), address);

  QDataStream in(&datagram, QIODevice::ReadOnly);

  qint64 size = -1;
  if(in.device()->size() > sizeof(qint64)) {
    in >> size;
  } else return;
  if (in.device()->size() - sizeof(qint64) < size) return;

  qint8 type = 0;
  in >> type;

  if (type == USUAL_MESSAGE) {
    QString str;
    in >> str;
    // код по перенаправке сообщения в классы выше //
  } else if (type == PERSON_ONLINE) {
    // Добавление пользователя с считанным QHostAddress //
  } else if (type == WHO_IS_ONLINE) {
    sending(nickname, qint8(PERSON_ONLINE));
  }
}


* This source code was highlighted with Source Code Highlighter.

Все прозрачно и ясно. Осталось прикрутить интерфейс, соединить все сигналы и основное окно чата готово.

QTcpSocket


Что же касается этого сокета, то он реализует модель «клиент-сервер». То есть в нашем случае каждый пользователь может стать как сервером, если он начнет кому-то первый писать приват-сообщение или отсылать файл. Или же клиентом, в случае если ему написали первому в приват.

С отправкой сообщения нет никаких сложностей. А вот для отправки файла сделаем классический трюк. Так как весь файл засунуть в поток не получится, то будет считывать данные из файла и порционно писать в поток, а затем отправлять их.
void sendFile(QString fileName) {
  if(sendFile != NULL){
    return;
  }
  sendFile = new QFile(fileName);
  if(sendFile->open(QFile::ReadOnly)){
  QByteArray data;
  QDataStream out(&data, QIODevice::WriteOnly);
  // подготовка данных для записи //
  clientSocket->write(data);
  clientSocket->waitForBytesWritten();
  connect(clientSocket, SIGNAL(bytesWritten(qint64)), this, SLOT(sendPartOfFile()));
  sendPartOfFile();
  } else{
    emit this->errorSendFile(QString("File not can open for read"));
    return;
  }
}

void sendPartOfFile() {
  char block[SIZE_BLOCK_FOR_SEND_FILE];
  if(!sendFile->atEnd()){
    qint64 in = sendFile->read(block, sizeof(block));
    qint64 send = clientSocket->write(block, in);
  } else{
    sendFile->close();
    sendFile = NULL;
    disconnect(clientSocket, SIGNAL(bytesWritten(qint64)), this, SLOT(sendPartOfFile()));
    emit endSendFile();
  }
}


* This source code was highlighted with Source Code Highlighter.

Осталось рассмотреть чтение потока. Так как приходит всего 2 типа данных — MESSAGE и SENDFILE, то написать обычный парсер входного потока не составит никакого труда. Он идентичен методу класса реализующий UDP сокет. Интерес представляет чтение из потока передающегося файла.
void receiveFile(QString fileName) {
  QString savePath = "Downloads/";
  QDir dir;
  dir.mkpath(savePath);
  receiveFile = new QFile(savePath + fileName);
  sizeReceivedData = 0;
  receiveFile();
}

void receiveFile() {
  QDataStream in(clientSocket);
   if(!bufferForUnreadData.isEmpty()){
    receiveFile->write(bufferForUnreadData);
    sizeReceivedData += bufferForUnreadData.size();
    bufferForUnreadData.clear();
  }
  char block[SIZE_BLOCK_FOR_SEND_FILE];
  while(!in.atEnd()){
    qint64 toFile = in.readRawData(block, sizeof(block));
    sizeReceivedData += toFile;
    receiveFile->write(block, toFile);
  }
  if(sizeReceivedData == sizeReceiveFile){
    receiveFile->close();
    receiveFile = NULL;
    sizeReceiveFile = 0;
    sizeReceivedData = 0;
  }
}

* This source code was highlighted with Source Code Highlighter.

Это суть работы приват-сообщения. Конечно здесь не хватает части сигналов, коннектов и специфичного кода. Например send- и recieve-файл — это только общий вид. Многое пропущено. Но если все добавить, прикрутить интерфейс, то можно получить вполне рабочий, написанный вами чат-клиент.

Целью же статьи было показать на примере, что работать с сокетами просто и легко. Надеюсь, что кому то данная статья поможет в будущем. Удачи.
Tags:
Hubs:
+24
Comments8

Articles

Change theme settings