Kurs Qt – część 7 – TCP

Matthew @ 2010-03-13 — Kategorie: Kurs Qt, Programowanie, Qt, Techblog

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):

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.

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.

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:

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.

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:

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.

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 klasyQByteArray. 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.

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).

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!

Komentarze:

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 :)

@Mattew: Możesz mi wyjaśnić, bo dopiero zaczynem z gitem, dlaczego adres do repozytorium podałeś jako link skoro i tak trzeba przepisać go do terminala git basha?

@Synek: bo taki miałem kaprys. Ładnie to wygląda i jeżeli ktoś ma tak ustawiony system że rozpoznaje protokół i uruchamia odpowiedniego klienta (nie zdziwiłbym się jakby Tortoise tak robił) to zawsze lepsze to niż przepisywanie.

Shub-Niggurath @ 16 maja 2012 o 09:05:

Super tutorial!
Dla wszystkich malkontentów, że link jest do repo a nie do np. zipa:
Linczor przez stronkę – https://github.com/matthewpl/qt7

Można podglądać, zassać, co tylko się chce.

świetna robota Matthew, kawał dobrej roboty :)

Witam. Przepisałem praktycznie kod tylko pozmieniałem gabaryty przycisków, etykiet i innych obiektów i po odpaleniu serwera i klienta, serwer nasłuchuje poprawnie, i po wpisaniu danych do logowania w kliencie, wyświetla mi na QTextBrowser, że nawiązano połączenie a program z serwerem kończy pracę i się zamyka samoczynnie. Co jest nie tak?

@Maniek: nie mam kryształowej kuli żeby powiedzieć Ci co jeszcze zrobiłeś (a o czym nie powiedziałeś) co mogłoby spowodować że program Ci nie działa tak jakbyś chciał. Mogę za to poradzić żebyś odpalił debuger i zaczął śledzić krok po kroku co program robi.

Witam

Rozumie, że ten algorytm sprawdzi się tylko w przypadku wyślij i czekaj na odpowiedź.
Jeśli nie wiem kiedy nastąpi odpowiedź lub cały czas chce nasłuchiwać na jakimś porcie to domyślam się że trzeba otworzyć nowy wątek do nasłuchu, dobrze myślę?

miło, że autor oprócz zaznajamiania nas z tematyką qt, znajduje czas na dostarczanie swoistej rozrywki jaką jest prowadzenie dyskusji z niejakim @filipesq. chociaż fakt, że osoby tak zaparte w swej niewiedzy mogą zajmować się programowaniem wydaje się co najmniej niepokojący. ‚ale tego linku nie da się otworzyć’ doprowadziło mnie do łez.
w każdym razie dzięki za poradnik :)

Bardzo dobry kurs. Mam jednak problem. Napisałem taką aplikację klient-serwer, ale nie wiem jak zrealizować wyświetlanie listy użytkowników. Nie mogę sobie poradzić z wyłuskaniem nazwy podłączonego użytkownika, aby wpisać ją na listę. Z góry dzięki za odpowiedź.

@Maniek,
w powyższym kodzie jest błąd, jest zainicjowana lista QTcpSocket jako wskaźnik, ale nie jest utworzony jej obiekt, utwórz go w konstruktorze i nie będzie crashowało.

Witaj, po próbie kompilacji twojego kodu, wyskakuje taki problem.

QtGui/QApplication: No such file or directory main.cpp 1

Dodaj komentarz:

 

Subscribe without commenting