mar 13 2010

Kurs Qt – część 7 – TCP

Category: Kurs Qt,Programowanie,Qt,TechblogMatthew @ 05:26

Z racji zaspania na dzisiejsze zajęcia (cała jedna godzina wykładu!) oraz pozwolenia używania na laborkach z wirtualnych zespołów roboczych (sponsorowanych przez prof. dr hab. inż. Bogdana Wiszniewskiego i dr inż. Jerzego Dembskiego) własnej technologii (czyli wcale nie trzeba pisać pod Windowsem w WinAPI (albo czymś takim), jednak robiąc przy okazji spory narzut godzinny na sklecenie własnego programu) macie możliwość poczytania jak wykorzystywać Qt do przesyłania danych przez sieć z wykorzystaniem protokołu TCP/IP. No… koniec tej wazeliny, czas wziąć się za coś bardziej efektywnego (i efektownego). Zrobimy prosty chat (nie, mój projekt na te laborki polegał na czymś innym).

Odmiennie, w stosunku do poprzednich części kursu, nie będę wrzucał całego kodu źródłowego (jest on umieszczony w moim repozytorium gita, adres pod koniec tej części kursu), ale fragmenty, które są nowe i dotyczą zagadnienia TCP/IP.

Chat

Zaczniemy od przyjrzenia się klasie serwera (jego zadaniem jest zbieranie danych od jednego klienta i rozsyłanie ich pozostałym):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class QTcpServer;
class QTcpSocket;

class Widget : public QWidget
{
   Q_OBJECT

public:
   Widget(QWidget *parent = 0);
   ~Widget();

private slots:
   void serverStart();
   void serverStop();
   void serverQuit();
   void addClient();
   void removeClient();
   void sendForward();

private:
   QTcpServer *tcpServer;

   QLabel *portLabel;
   QLabel *statusLabel;
   QLineEdit *portLineEdit;
   QPushButton *startServerButton;
   QPushButton *stopServerButton;
   QPushButton *quitServerButton;

   QList<qtcpsocket *> *clientsList;
   QList<quint16> *blockSizeList;
};

Dwie nowości to klasy QTcpServer, której zadaniem jest zbieranie połączeń przychodzących do serwera, oraz QTcpSocket, która reprezentuje socket. Dodatkowo mamy kilka slotów, których zadaniem będzie obsługa tego co się dzieje z serwerem, czyli reakcja na nawiązywanie i kończenie połączenia , odbieranie i przesyłanie dalej wiadomości oraz uruchamianie i zatrzymywanie nasłuchiwania serwera.

1
2
3
4
5
6
7
8
9
10
11
12
13
Widget::Widget(QWidget *parent) : QWidget(parent)
{
   tcpServer = new QTcpServer(this);
   connect(tcpServer, SIGNAL(newConnection()), this, SLOT(addClient()));

   portLineEdit = new QLineEdit(this);
   portLineEdit->setValidator(new QIntValidator(1, 65535, this));

   portLabel = new QLabel(tr("&Port:"), this);
   portLabel->setBuddy(portLineEdit);

   ...
}

W konstruktorze, jedyną operacją (poza przydzieleniem pamięci dla odpowiednich zmiennych), którą zrobimy w celu uruchomienia naszego serwera, jest połączenie sygnału nawiązania nowego połączenia z serwerem (newConnection()) ze slotem dodawania klientów do listy (addClient()).
Dodatkowo, warto jeszcze wspomnieć o dwóch przydatnych metodach związanych z interfejsem. Pierwsza to ustawianie walidatora dla pola tekstowego. W tym wypadku określamy jaki typ liczb może zostać wklepany w to pole (całkowite), oraz ich zakres (od 1 do 65535). Następną rzeczą jest przekazanie focusa do pola tekstowego w przypadku gdy wciśniemy kombinację Alt-P. Pola tekstowe nie mają etykiet, więc aby była możliwość przenoszenia im focusa poprzez takie skróty musimy się posiłkować pomocą etykiet.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void Widget::serverStart()
{
   if (!tcpServer->listen(QHostAddress::Any, portLineEdit->text().toInt()))
   {
      QMessageBox::critical(this, tr("Błąd!"),
         tr("Serwer nie może zostać poprawnie uruchomionym"));
      return;
   }
   else
   {
      QString ipAddress;
      QList<qhostaddress> ipAddressesList =
         QNetworkInterface::allAddresses();

      for (int i = 0; i < ipAddressesList.size(); ++i)
      {
         if (ipAddressesList.at(i) != QHostAddress::LocalHost &&
            ipAddressesList.at(i).toIPv4Address())
         {
            ipAddress = ipAddressesList.at(i).toString();
            break;
         }
      }

      if (ipAddress.isEmpty())
      {
         ipAddress =
            QHostAddress(QHostAddress::LocalHost).toString();
      }

      ...
}

Najpierw nakazujemy serwerowi nasłuchiwania połączeń na wybranych IP oraz porcie. W przypadku gdyby ktoś nie podał portu zostanie on wylosowany. W przypadku błędu, zostanie wyświetlony komunikat o niepowodzeniu tej operacji.
Dalej próbujemy się dowiedzieć na jakim IP oraz porcie nasłuchuje serwer. Ponieważ wcześniej powiedzieliśmy serwerowi, że ma nasłuchiwać na dowolnym IP, teraz musimy wiedzieć jakie konkretnie ono jest (żebyśmy wiedzieli co wklepać w klienta ;) ). Bierzemy pierwsze z brzegu, chyba że nie ma żadnego to wybieramy localhosta.
Aby zatrzymać serwer wystarczy wywołać metodę close(). Powoduje ona zakończenie nasłuchiwania przez serwer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Widget::serverStop()
{
   tcpServer->close();
   ...
}{/geshi}{geshi lang=cpp-qt}void Widget::addClient()
{
   QTcpSocket *client = tcpServer->nextPendingConnection();
   clientsList->push_back(client);
   blockSizeList->push_back(0);

   connect(client, SIGNAL(disconnected()), this, SLOT(removeClient()));
   connect(client, SIGNAL(disconnected()), client, SLOT(deleteLater()));
   connect(client, SIGNAL(readyRead()), this, SLOT(sendForward()));
}

Aby dodać klienta do usługi, trzeba by wiedzieć skąd nadsyła dane. Informacje te trzyma klasa QTcpSocket, a wydobyć je możemy z QTcpServer poprzez wywołanie metody nextPendingConnection(). Musimy jeszcze połączyć dwa sygnały od socketu. disconnected() poinformuje nas, kiedy klient się rozłączył. A ponieważ taki socket nie jest już nam potrzebny, możemy go skasować poprzez wywołanie slotu deleteLater(). Drugim ważnym sygnałem jest readtRead(), który mówi nam o tym, że są już zbuforowane dane, które możemy odczytać i obrobić. W tym wypadku w naszym własnym slocie sendForward(), będziemy je przesyłać dalej.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
void Widget::sendForward()
{
   QTcpSocket *client = (QTcpSocket*) sender();
   int index = clientsList->indexOf(client);

   QDataStream in(client);
   in.setVersion(QDataStream::Qt_4_6);

   if (blockSizeList->at(index) == 0)
   {
      if (client->bytesAvailable() < (int)sizeof(quint16))
      {
         return;
      }

      quint16 sizeOfBlock;
      in >> sizeOfBlock;
      (*blockSizeList)[index] = sizeOfBlock;
   }

   if (client->bytesAvailable() < blockSizeList->at(index))
   {
      return;
   }

   QString nick;
   in >> nick;

   QString message;
   in >> message;

   QByteArray data;
   QDataStream out(&data, QIODevice::WriteOnly);
   out.setVersion(QDataStream::Qt_4_6);

   out < < blockSizeList->at(index);
   out < < nick;
   out << message;

   for (int i = 0; i < clientsList->size(); ++i)
   {
      if (i != index)
      {
         clientsList->at(i)->write(data);
      }
   }

   (*blockSizeList)[index] = (quint16)0;
}

Samo przesyłanie danych od jednego klienta do pozostałych składa się z dwóch części: odbierania danych oraz wysyłania ich (oczywiste, nieprawdaż? ;) ). Aby tego dokonać, musimy znać socket, który wywołał metodę przesłania. Tego dowiadujemy się dzięki metodzie sender(), która zwraca wskaźnik na obiekt, który wysłał sygnał do naszego slota. Dalej następuje deserializacja z wykorzystaniem klasy QDataStream. Używa się jej na podobnej zasadzie co standardowe strumienie wyjścia i wejścia z C++. Musimy dodatkowo określić wersję Qt, wg. której będzie następowała serializacja. Zaleca się używanie wcześniejszych wersji, aby niezależnie od tego, na jakiej wersji Qt skompilujemy nasz program nie było jakiś problemów z komunikacją. Wyjaśnienia wymaga jeszcze dziwna liczba na początku. Jest nią liczba bajtów, które odbieramy/wysyłamy w jednym “pakiecie” danych. Zabezpiecza nas to przed sytuacją, w której część danych już dotarła a na pozostałe jeszcze musimy czekać z powodu losowych opóźnień w sieci. Tak pobrane dane wysyłamy do pozostałych klientów.

Teraz przejdziemy do kodu klienta naszego chata:

1
2
3
4
5
6
7
8
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
{
   ...
   connect(tcpSocket, SIGNAL(connected()), this, SLOT(connectedToServer()));
   connect(tcpSocket, SIGNAL(error(QAbstractSocket::SocketError)), this,
      SLOT(displayError(QAbstractSocket::SocketError)));
   ...
}

W konstruktorze łączymy sygnał, który jest emitowany po nawiązany połączenia, do slota, którego zadaniem jest informowanie nas, że takie połączenie zakończyło się sukcesem. Oraz drugie połączenie, od sygnału, który poinformuje nas o ew. błędach do slotu, który te błędy wyświetli.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void MainWindow::sendMessage()
{
   chat->append(nick + ": " + messageLine->text());

   QByteArray data;
   QDataStream out(&data, QIODevice::WriteOnly);

   out.setVersion(QDataStream::Qt_4_6);
   out < < (quint16) 0;

   out << nick;

   out << messageLine->text();

   out.device()->seek(0);

   out < < (quint16) (data.size() - sizeof(quint16));

   tcpSocket->write(data);

   messageLine->clear();
}

Mamy tutaj przykład tego jak się serializuje dane do wysłania. Do przechowania danych z QDataStream (której obiekt ustawiamy w trybie tylko do zapisu) wykorzystamy obiekt klasy QByteArray. Po ustawieniu wersji, zaczynamy zapis do strumienia. Podajemy 0 (które później zastąpimy przez wielkość paczki), nick którym się posługujemy na chatcie oraz wiadomość. Następnie przechodzimy na początek strumienia i zapisujemy wielkość danych które podaliśmy do niego (wcześniej podane zero zostanie w tym momencie nadpisane). Teraz wystarczy tylko zapisać naszą tablicę bajtów do socketu, a Qt już zadba o to, żeby dane zostały wysłane. Dane które przychodzą do klienta odczytuje się praktycznie tak samo jak to zostało przedstawione w kodzie serwera.

1
2
3
4
5
void MainWindow::disconnectFromServer()
{
   tcpSocket->close();
   ...
}

Jeżeli chcemy się rozłączyć z serwerem możemy to wykonać na dwa sposoby: wywołanie metody close(), gdzie bieżące połączenie zostanie w “delikatny” sposób zakończone, poprzez przesyłanie danych jakie jeszcze zostały i skończenie nasłuchiwania/wysyłania dalszych danych (w przeciwieństwie do abort() które kończy nasłuchiwanie bez zwracania uwagi na cokolwiek).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void MainWindow::displayError(QAbstractSocket::SocketError socketError)
{
   switch (socketError)
   {

   ...

   case QAbstractSocket::HostNotFoundError:
      QMessageBox::information(this, tr("Fortune Client"),
         tr("The host was not found. Please check the "
         "host name and port settings."));
      break;
   ...
}

Jeżeli z jakiegoś powodu zdarzy się jakiś błąd, nie jest problemem aby wyświetlić komunikat z dokładną informacją co się stało. Rodzaje błędów są przechowywane w enumie QAbstractSocket::SocketError, wystarczy wyświetlić QMessageBox z odpowiednim komunikatem

Mam nadzieję, że część o TCP/IP wam się podobała. Jeżeli jakieś błędy się znalazły, piszcie o tym w komentarzach (kończę ten tekst o 5:30, więc nie zdziwię się jeżeli coś jednak się znadzie ;) ). Jako pracę domową możecie dorobić listę użytkowników chata oraz powiadomienia o przyjściu i wyjściu klienta. Oczywiście zamieszczam również adres do repozytorium z gotowym kodem przykładowym użytym w tej części kursu: git://github.com/pingwinek/qt7.git. Miłego kodzenia!

6 komentarzy dla “Kurs Qt – część 7 – TCP”

  1. filipesq, dnia napisał(a):

    Link do repozytorium nie działa: “Firefox nie jest w stanie otworzyć tego adresu, ponieważ protokół “git” nie jest przypisany do żadnego programu.”. Jak dałem zamiast ‘git://’, “http://” to nie znaleziono strony… A szkoda… Popraw link, bo oprócz tego te tutoriale są super!!!

  2. Matthew, dnia napisał(a):

    @filipesq: link jest jak najbardziej poprawny. Kody źródłowe są trzymane w repozytoriach gita.

  3. filipesq, dnia napisał(a):

    @Matthew: Nie rozumiesz. Ten link jest nie poprawny. Musisz go zmienić na: http://github.com/pingwinek/qt7

  4. Matthew, dnia napisał(a):

    @filipesq: to Ty nie rozumiesz. W kursie jest wyraźnie napisane “Oczywiście zamieszczam również adres do *repozytorium* z gotowym kodem przykładowym użytym w tej części kursu: git://github.com/pingwinek/qt7.git”. Ja podaję adresy do repozytoriów skąd można ściągnąć kody źródłowe przy pomocy gita. WWW githuba, to tylko frontend do podglądu tego repozytorium z posiomu strony oraz wspomagające komunikację między deweloperami.

  5. filipesq, dnia napisał(a):

    @Mattew: Może i tak, ale tego linku nie da się otworzyć

  6. Matthew, dnia napisał(a):

    @filipesq: Uparty jesteś… po prawej masz taką kategorię jak git. Przeczytaj z niej wszystko, wtedy zrozumiesz o co z tym wszystkim biega.

Zostaw odpowiedź

Subskrybuj bez komentowania.