PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : Qt Socket Tutorial



anda_skoa
05-06-2003, 23:58
Dieses Tutorial gibt ein Beispiel für die Benutzung der eventloopbasierten Qt Socket Klassen.
Eventloopbasiert bedeutet, dass man ohne Threads arbeiten kann und dennoch nichtblockieren Ein-/Ausgabe hat.

Wie immer bei der Arbeit mit Eventloop muss man darauf achten, dass man keine Funktion, die von der Eventloop aufgerufen wird, blockieren auslegt.
Im Falle von QSocket bedeutet das, dass man Slots, die auf QSocket Signale verbunden sind, nur für kurze Arbeiten heranziehen sollte.

Es bedeutet weiter, dass QSocketmethoden meist sofort beenden und die eigentlich Aufgabe zeitversetzt bearbeitet wird, so dass man das Resultat der Aufgabe nur über Signale erkennen kann.

Erzeugung
Die Beispiel Programme im Anhang sind mit qmake-Projektdateien versehen. Zum Erzeugen des Server und des Client Programms muss also, wie im Grundlagen Tutorial beschrieben, QMAKESPEC und QTDIR gesetzt sein.
Dann erfolgt die Erzeugung mit einem einfachen
#> make

Das Hauptmakefile erzeugt dann die beiden Teilmakefiles und damit dann die beiden Programme.

Client Code
Der Client besitzt eine einfache, im Designer konstruierte, GUI.
Der erste interessante Teil des Codes befindet sich im Konstruktor der Klasse ClientMainWindow


m_socket = new QSocket(this);
QObject::connect(m_socket, SIGNAL(connected()), this, SLOT(slotConnected()));
QObject::connect(m_socket, SIGNAL(connectionClosed()), this, SLOT(slotDisconnected()));
QObject::connect(m_socket, SIGNAL(error(int)), this, SLOT(slotError(int)));
QObject::connect(m_socket, SIGNAL(readyRead()), this, SLOT(slotRead()));

Hier wird eine QSocket Instanz erzeugt und ihre Signale werden mit Slots der Fensterklasse verbunden.

Wie oben angedeutet, wird der Erfolg einer QSocketmethode durch ein Signal bekannt gemacht.
Ist zum Beispiel ein durch QSocket::connectToHost initierter Connect erfolgreich, sendet der Socket sein connected() Signal aus.
Schläg der Connectversuch fehl, wid error(int) ausgesandt, wobei der Integerparameter einen Hinweis auf den Grund des Fehlschlags gibt (siehe die damit verbundene Methode ClientMainWindow::slotError).


Zu einem Server Verbindung aufzunehmen ist sehr einfach:


QString host = m_host->text();
int port = m_port->value();

m_socket->connectToHost(host, port);

Die Methode QSocket::connectToHost initiert einen Verbindungsaufbau und kehrt unmittelbar danach zurück.
Als ersten Parameter kann eine IP Adresse oder ein Hostname angegeben werden, letzterer wird dann intern mit Hilfe von QDns aufgelöst.

Hier ist zu beachten, dass nach dem Aufruf die Eventloop nicht blockiert wird, also in dem man zB danach eine Endlosschleife hat.


Ein QSocket ist ein QIODevice und kann daher zum Beispiel als Ziel eines Streams verwendet werden.


QString input = m_input->text();
if (input.isEmpty()) return;

if (m_socket->state() == QSocket::Connected)
{
QTextStream stream(m_socket);
stream << input << endl;
}

Hier wird überprüft, ob der Socket verbunden ist und, wenn ja, eine Textzeile über den Socket geschickt.
Das Endline ist in diesem Fall wichtig, denn das Protokoll dieses einfachen Beispiels berucht darauf, dass eine Übertragung immer mir einem Newline endet, also aus vollständing Zeilen besteht.

Im allgemeinen Fall, wenn beliebige Daten übertragen werden, ist es besser immer nur kleinere Happen zu übertragen und den Rest in einem Puffer zwischen zu speichern.
Man kann dann mittel des Signals bytesWritten verfolgen, wann alle Daten des aktuellen Happens übetragen wurden und man den nächsten Teil senden kann.

Lesen vom Socket kann man nun wieder über QIODevice Funktionalität machen oder wie im Beispiel über Funktion des Sockets selbst.


QString text;
while (m_socket->canReadLine())
text += m_socket->readLine();

Dieser Code ist aus einem Slot, der mit dem readyRead Signal des Sockets verbunden ist. Dieses Signal wird immer ausgesandt, wenn neue Daten im Socket ankommen.
Da wir wissen, dass immer ein abschliessendes Newline kommen wird, tun wir erst etwas, wenn eine ganze Zeile gelesen werden kann.

Im allgemeine Fall wird man mit QSocket::bytesAvialable nachsehen, wieviele Daten eingetroffen sind und diese dann in einen Puffer lesen.

Server
Der Server besitzt ebenfalls eine einfache GUI.

Der zentrale Teil der Server Funktionalität, also das Annehmen von Clientverbindungen, erledigt die Klasse ServerSocket, die von QServerSocket abgeleitet ist.
Dabei wird die abstrakte Methode (pure virtual) newConnection(int) implementiert.


void ServerSocket::newConnection(int socketfd)
{
QSocket* socket = new QSocket(parent());
socket->setSocket(socketfd);

emit newClient(socket);
}

Der Integer-Parameter ist der Filedescriptor des Datensockets der neuen Verbindung.
Wir erzeugen eine neue QSocket Instanz und lassen sie mit diesem Descriptor arbeiten.
Damit wir mit dieser Verbindung etwas anfangen können, müssen wir sie unserem Server Programm irgendwie zugänglich machen.
Das geschieht über ein selbstdefiniertes Signal newClient(QSocket*), dass als Parameter unseren neuen Socket transportiert.

Der erste Teil der Datei servermainwindow.cpp ist die Datenklasse Client


class Client
{
public:
Client(QSocket* socket, QListViewItem* item)
: m_socket(socket), m_item(item), m_num(++m_count) {};

~Client() {
delete m_socket;
delete m_item;
}

inline QSocket* socket() { return m_socket; }

inline QListViewItem* item() { return m_item; }

inline int number() { return m_num; }

protected:
static int m_count;
QSocket* m_socket;
QListViewItem* m_item;
int m_num;
};

Die Klasse ist mehr oder weniger nur eine Gruppierung von Daten, die mit einem Client assoziert sind: der Socket der Datenverbindung zum Client, das ListView Element in der Anzeige und die Nummer des Clients.
Die Nummer ist eine fortlaufende Zahl, die hier der Einfachheit halber zur Benennung der Clients benutzt wird.

Die Klasse ist in der CPP Datei deklariert, da sie extern nicht von Belang ist. Sie ist nur für die Serverklasse von Bedeutung.

Im Konstruktor der ServerMainWindow Klasse nutzen wir ein Feature der Qt Container aus


m_clients.setAutoDelete(true);

Das AutoDelete weißt den Container an, alle seine Einträge zu löschen, wenn sie aus ihm entfernt werden.
Das nutzen wir später aus, um mit einem einfachen clear() alle Cleint Instanzen zu löschen.

Unser Server verhält sich wie ein einfacher Chatserver. Er schickt einkommende Zeilen Text an alle Clients weiter.
Dieses Verschicken passsiert wie beim Client mit einem QTextStream, der die Sockets der Verbindungen als QIODevice behandelt.


void ServerMainWindow::sendToClients(const QString& text)
{
if (text.isNull()) return;

// iterate over all clients and send them the text
QPtrDictIterator<Client> iter(m_clients);
for (; iter.current() != 0; ++iter)
{
QSocket* sock = iter.current()->socket();
QTextStream stream(sock);
stream << text;
}
}

Wir benutzen eine Iterator, um alle Einträge des Dictionaries anzusprechen.
Die Einträge im Dictionary sind Client Instanzen und haben daher die Methode socket(), die den Socket der Verbindung zurück gibt.

Um überhaupt Verbindungen annehmen zu können, müssen wir einen ServerSocket erzeugen und dessen newClient verbinden.


m_server = new ServerSocket(m_port->value(), this);
if (!m_server->ok())
{
delete m_server;
m_server = 0;
return;
}

QObject::connect(m_server, SIGNAL(newClient(QSocket*)), this, SLOT(slotNewClient(QSocket*)));

Wenn das bind(), dass der QSockerSocket intern machen muss um den Port zu reservieren, klappt, gibt die Methode QServerSocket::ok() "true" zurück, wenn es nicht geklappt hat "false".

Sollte es nicht geklappt haben löschen wir die unbrauchbare SocketServer Instant und beende die Methode.
Sonst verbinden wir das newClient Signal mit einem Slot, der dann die neue Verbindung verarbeitet.
Diese Methode sieht so aus


QListViewItem* item = new QListViewItem(m_list, socket->peerAddress().toString(),
QString::number(socket->peerPort()));
Client* client = new Client(socket, item);

// notify all others about the newcomer
sendToClients(QString("Server: Client %0 connected\n").arg(client->number()));

m_clients.insert(socket, client);

QObject::connect(socket, SIGNAL(connectionClosed()), this, SLOT(slotClientDisconnected()));
QObject::connect(socket, SIGNAL(readyRead()), this, SLOT(slotSocketRead()));

// great the newcomer
QTextStream stream(socket);
stream << "Server: Hi\nYou are client " << client->number() << endl;

Zuerst erzeugen wir den ListView Eintrag, der die Verbindung im der GUI anzeigt. Dann erzeugen wir die Client Instanz, die wir intern zu Verwaltung der Verbindung benutzen und tragen sie in unser Dictionary ein, nachdem wir allen bisher eingetragenen Clients die Ankunft des neuen Teilnehmers gemeldet haben.

Wie im Clientprogramm verbinden wir QSocket Signale mit entsprechende Slots, um eingehende Daten und den Verbindungsabbau behandeln zu können.

Wenn der Verbindungspartner von sich aus die Verbindung beendet, sendet der Socket an unserem Ende das connectionClosed Signal aus.
Das haben wir auf einen Slot verbunden, der folgenden Code enthält


QObject* sender = const_cast<QObject*>(QObject::sender());
QSocket* socket = static_cast<QSocket*>(sender);

qDebug("client disconnected");

//disconnect signals
socket->disconnect();

// remove from dict
Client* client = m_clients.take(socket);

sendToClients(QString("Server: Client %0 disconnected\n").arg(client->number()));

delete client;

Da wir das Signal aller Client Sockets mit einem einzigen Slot verbunden haben, müssen wir zuerst rausfinden, welcher Socket das Signal ausgesandt hat.
Das machen wir über QObject::sender(). Dabei ist zu beachten, dass dieser Slot auf keine Fall wie eine Methode aufgerufen werden darf, denn in diesem Fall ist der Wert, den diese Methode liefert, nicht definiert!.

Das take entfernt den Eintrag aus den Dictionary ohne die automatische Löschung auszuführen, die wir mit AutoDelete erlaubt haben.
Wenn wir die Client Instanz nicht mehr benötigen würden, könnte wir statt take und delete gleich remove benutzen.

Eingehende Daten behandeln wir ähnlich wie im Clientprogramm


QObject* sender = const_cast<QObject*>(QObject::sender());
QSocket* socket = static_cast<QSocket*>(sender);

Client* client = m_clients.find(socket);

while (socket->canReadLine())
{
QString line = socket->readLine(); // read the line of text

// and send it to everone including the sender
sendToClients(QString("Client %0: %1").arg(client->number()).arg(line));

client->item()->setText(2, line.left(line.length()-1));
m_list->update();
}

Wie in der vorigen Methode ermitteln wir zuerst, welcher Socket neue Daten hat. Dann benutzen wir diesen Pointer, um in unserem Dictionary nach der zugehörigen Client Instanz zu suchen.
Dann lesen wir alle verfügbaren Zeilen und senden sie an alle Clients, einschliesslich dem aktuellen.
Am Ende aktualisieren wir noch die Anzeige in der GUI des Servers.

Wenn das Serverprogramm seinen Dienst einstellen soll, müssen wir alle Clients loswerden.


QPtrDictIterator<Client> iter(m_clients);
for (; iter.current() != 0; ++iter)
{
// disconnect the socket's signals from any slots or signals
iter.current()->socket()->disconnect();
}

m_clients.clear(); // delete all clients (m_clients has autodelete ON)

Zuerst iterieren wir über unser Dictionary und löschen die Verbindungen der Socket Signale damit uns kein connectionClosed dazwischen pfuscht, während wir iterieren.
Dann erstören wir alle Client Instanzen, in dem wir einfach das Dictionary leeren. Da es AutoDelete erlaubt hatte, löscht es alle Einträge.
Der Client Destruktor wiederum löscht den Socket der Verbindung, der dabei geschlossen wird.