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.

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 | void Widget::serverStop() { tcpServer->close(); // ... } |
1 2 3 4 5 6 7 8 9 10 | 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/matthewpl/qt7.git. Miłego kodzenia!

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!!!
@filipesq: link jest jak najbardziej poprawny. Kody źródłowe są trzymane w repozytoriach gita.
@Matthew: Nie rozumiesz. Ten link jest nie poprawny. Musisz go zmienić na: http://github.com/pingwinek/qt7
@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.
@Mattew: Może i tak, ale tego linku nie da się otworzyć
@filipesq: Uparty jesteś… po prawej masz taką kategorię jak git. Przeczytaj z niej wszystko, wtedy zrozumiesz o co z tym wszystkim biega.
czesc. mam takie pytanie: dla ustalenia, na jakim ip ma nasluchiwac serwer, sprawdzamy wszystkie allAdreesses, dopiero pozniej jesli nic nie ma, ustawiamy na localHost (127.0.0.1 chyba).
no i moje pytanie dotyczy tego, jakie wady ma ustawienie ip serwera przez localHost, bo przeciez najpierw szukamy kazde inne mozliwe (if(ip!=localHost….), ale jednoczesnie ip to nie jest zakazane, bo jesli nic nie znajdzie, to i tak bierzemy te localHost – a wiec dlaczego od razu nie mozna ustalic na nie? (bo pewnie jakis powod jest)
a drugie pytanie, zbudowalem mini serwer i klienta i czy dobrze jest testowac je na jednym komputerze? (poki co dziala, bo zalaczam serwer, klientem lacze do serwera i serwer potwierdza laczenie – ale maja takie samo ip i nie wiem czy rozbudowywanie kodu bedzie dobre przy sprawdzaniu na jednej maszynie
jak znajdziesz chwilke to licze na odp, dzieki i pozdr.
@dmx: czy Tobie brakuje podstaw wiedzy informatycznej? Localhost to adres komputera, który wskazuje na samego siebie i nie jest dostępny z zewnątrz. W przykładzie sprawdza sie czy mamy jakieś adresy ip które wskazują na internet i jeżeli nie to nasłuchuje na lokalce.
Co do drugiego to w komunikacji ważniejsze są porty od ip. Serwer nasłuchuje na jednym ustalonym porcie, a klienci mają już je przydzielane losowo z pewnej puli.
to prawda, nie ukonczylem zadnej szkoly informatycznej, wiec wszytsko dla mnie jest „nowe” :) mniej wiecej opanowalem (jesli tak mozna powiedziec) programowanie obiektowe w C++, Allegro, no a teraz wzialem sie za QT no i pierwszy raz ucze sie pisac aplikacje sieciowe, qt ma dobrze zrobiona dokumentacje, wiec w miare wszytsko lapie – ale wiadomo, tak jak dla ciebie, tak dla tworcow dokumentacji niektore rzeczy byly oczywiste, a dla mnie nie, dlatego wole zapytac, niz uzywac czegos nie wiedzac dlaczego, mam nadzieje, ze nie stracisz cierpliwosci, no i ze coraz mniej tu bedzie takich pytan :)
a podsumowujac odpowiedz, kazdy komputer ma swoje lokalne ip 127.0.0.1, z ktorym inne komputery nie moga sie polaczyc, ale jesli ma dostep do internetu, wtedy ma juz on takze przydzielone ip – widzialne dla innych komputerow (zgadza sie?)
czesc, to znowu ja :) i znowu pewnie dla ciebie cos oczywistego, ale wole zapytac :
chodzi o przesylanie danych z klienta do serwera, a konkretnie o pewien fragment w serializacji:
mamy QByteArray, QDataStream odczytje z QSocket,
1. tworzymy zmienna quint16 (16bitow)=0 i dopisujemy na poczatek QByteArray
2. dopisujemy do tego to, co chcemy przeslac (stringa) czyli qdatastream<<string
no i przesylamy przez socket.write(qdataarray)
w przypadku twojego przykladu dopisujesz 2 stringi – nick i wiadomosc
czyli stream<<nick, stream<>nick i >>wiadomosc?, bo rozumiem, jakby nick i wiadomosc byly jako jedna wiadomosc i wtedy wczytamy je do jednego stringa, a tak jak je podzielic (czy moze przy zapisywaniu do strumienia osobnych stringow dopisuje sie znak null_terminated?, jesli tak to juz rozumiem, ale wole zapytac)
mam nadzieje, ze rozumiesz, o co mi chodzi, dzieki i pozdr.
no i ciagle mam z tym problem – chodzi o to, ze jak wysylam dane do serwera (oczywiscie z serializacja) a pozniej je odczytuje, to po odczytaniu wciaz w buforze sa dane -
sprawdzam to wyswietlajac w oknie socket->bytesAvaible();
1.wysylam stringa + 16 bitow info o rozmiarze
2.odczytuje te 16 bitow do zmiennej quint16, pozniej reszte do stringa, a po tej operacji wciaz jest bytesAvaible() >0 (np 12 i wielokrotnosci 12)
3. sprawdzam przy wysylaniu wielkosc qytearray itd i zawsze jest duzo mniejsza, nic bytesAvaible przed poczatkiem odczytywania
wstawilbym kod, ale moze dasz rade cos podpowiedziec, bo nie chce za duzo zasmiecac ci wpisow, chociaz i tak chyba duzo miejsca tu zajmuje
ok problem juz rozwiazany, mialem 2 razy podlaczony ten sam sygnal, tzn jako void MainWindow::on_pb_clicked() i przez connect(pb,Signal(clicked())
Stanąłem w miejscu :/
zrobiłem chacika wzorując się na Twoim przykładzie. Jednak nie mogę rozgryźć pewnego problemu. Mianowicie po rozłączeniu się któregoś z klientów, żaden następny nie może się połączyć. Wiesz może w czym może być problem ?
Hey!
Probuje sciagnac te pliki z gira i wywale mi cos takiego:
fatal: remote error:
Could not find Repository pingwinek/qt7
@Woda:
Zamiast git://github.com/pingwinek/qt7.git spróbuj http://github.com/pingwinek/qt7.git
Niestety już nie można dostać się na gita przynajmniej na żaden z powyższych linków czy istnieje możliwość wrzucenia tego ponownie ??
Podpinam się do prośby !
Także jestem za powtórnym wrzuceniem programów. Chciałbym się nauczyć programowanie TCP w QT a tu nie można bo brak kodów źródłowych.
Pisałem do Matthew w sprawie gita. Okazało się, że zmienił nawę konta.
Kod znajdziecie tutaj: git://github.com/matthewpl/qt7.git
z terminala wpisz git clone git://github.com/matthewpl/qt7.git, i pobierzesz repo :)