Warning: Trying to access array offset on value of type null in /homepages/2/d202998501/htdocs/www/Mihahome/Core/Benutzer.php on line 346

Warning: Trying to access array offset on value of type null in /homepages/2/d202998501/htdocs/www/Mihahome/Core/ShowTut.php on line 57

Warning: Trying to access array offset on value of type null in /homepages/2/d202998501/htdocs/www/Mihahome/Core/ShowTut.php on line 57

Warning: Trying to access array offset on value of type null in /homepages/2/d202998501/htdocs/www/Mihahome/Core/ShowTut.php on line 57
Mihahome Revolution 2.0 - Tutorials - C++ Chat Tutorial
Links
Mihahome Revolution 2.0 - Tutorials - C++ Chat Tutorial
Kopfdaten

Warning: Trying to access array offset on value of type null in /homepages/2/d202998501/htdocs/www/Mihahome/Core/ShowTut.php on line 75

Warning: Trying to access array offset on value of type null in /homepages/2/d202998501/htdocs/www/Mihahome/Core/ShowTut.php on line 75

Warning: Trying to access array offset on value of type null in /homepages/2/d202998501/htdocs/www/Mihahome/Core/ShowTut.php on line 78

Warning: Trying to access array offset on value of type null in /homepages/2/d202998501/htdocs/www/Mihahome/Core/ShowTut.php on line 78
Name des Tutorials:C++ Chat Tutorial
Verfasser:
Erstellt am:05.05.2003
Kurzbeschreibung:Dieses Tutorial beschreibt, wie man einen einfachen Chat über TCP/IP in der Win32-Konsole schreibt.

Inhalt

1. Vorwort: C++, TCP/IP, STL


Das ist mein erstes Tutorial und ich hoffe, dass es nicht allzu schlecht geschrieben ist.
Hier werde ich versuchen zu zeigen, wie man einen einfachen Chat mit der Hilfe von WinSock aufbaut.
Der cpp ist für den Microsoft Visual C++ 6.0 Compiler geschrieben, sollte aber auch unter anderen Compilern laufen und um das Ganze unter Linux zu schreiben, benötigt es auch nur geringfügige Änderungen.
Außerdem läuft dieser simple Chat in der Win32 Konsole, was aber durchaus ausreichend sein sollte, da es hier nicht um grafisches Design geht.
ich verwende hier das TCP/IP Protokoll.
Nach diesem Tutroail sollte es für jeden möglich sein, den Serve und den Clienten nach Bedarf zu erweitern.
Für dieses Tutorial wird ein Server und ein Client geschrieben und wenn man erst einmal einen fähigen Clienten und Server geschrieben hat, ist es nicht mehr schwer zu lernen, wie man z.B. ein E-Mail Programm oder einen IRC Clienten schreibt; dafür benötigt man dann nur noch die jeweiligen Infos.
Das Tutorial wird soweit wie möglich auf Basis von C++ geschrieben, d.h. es wird die STL und auch Singletons verwendet.
Ich hoffe, dass das Tutorial Gefallen finden wird und bin um Verbesserungsvorschläge und Kritik bedürftig.

2. Der TCP/IP-Client



Ich habe mich dazu entschlossen, mit dem Clienten zu beginnen, weil der cpp dafür kürzer ist.
Wir benötigen in Windows folgende Dateien:
winsock.h
wsock32.lib
Die wsock32.lib wird gebunden und die winsock.h einfach inkludiert.

2.1. Die Client-Klasse:


Wir starten an dieser Stelle, indem wir eine SingleTon Klasse schreiben.
Solch eine Klasse kann nur eine einzige Instanz haben, welche in der Klasse selber als statische Variable angelegt ist.
// client.h

#ifndef _CHATCLIENT_
#define _CHATCLIENT_

#include <windows.h>
#include <winsock.h>
#include <iostream>

class Client
{
private: // Variablen
static Client m_Instance;
SOCKET s;
SOCKADDR_IN addr;
private: // Funktionen

bool StartUp();
bool CreateConnectSocket(char* argv[2]);
public: // Methoden
static Client* Instance() {return &m_Instance;}
int ClientMain(char* argv[2]);

};

#endif

So, jetzt gibt es einiges zu sagen.
Wie man sieht, habe ich gleich die winsock.h eingebunden, da wir sie für das SOCKET und SOCKADDR_IN Objekt benötigen.
static Client m_Instance;
ist in diesem Fall die statische Instanz der Klasse selber.
Diese benötiegn wir für den Zugriff auf die Klasse.
SOCKET ist eigentlich nur eine andere Bezecihnung für int und wird in Linux deswegen auch dadurch ersetzt, aber ich denke, dass es in Windows bessert ist, wenn man SOCKET schreibt, damit klar wird, dass es auch ein solches ist.
Ein Socket ist ein Objekt, dass Verbindungen aufnehmen kann und durch das man auch Pakete empfängt oder versendet.
Ein Paket ist in diesem Fall eine Datenmenge, die man durch einen Funktionsaufruf abschickt.
SOCKADDR_IN ist ein Objekt, dass die relevanten Eigenschaften, wie z.B. IP, Port und Adressenfamilie, aber dazu später...
Schließlich haben wir noch die privat gebrauchten Funktionen, ich mache immer lieber zwei private-Blöcke, einmal für die privaten Funktionen und einmal für die privaten Variablen, da ich das übersichtlicher finde.
Zurück zum Thema:
int ClientMain(char* argv[2]);
ist die Hauptfunktion, die von der main aus aufgerufen wird, das ganze Geschehen wird nämlich in der Klasse ablaufen.
char* argv[2];
beinhaltet die IP Adresse und den Port. Diese Daten werden am Programmstart mitgeliefert.
// Main.cpp

#include "client.h"

int main(int argc, char* argv[])
{
if(argc < 3)
{
std::cout << "Sie muessen das Programm folgendermassen starten: Client <ServerIP> <Port>\n";
return 1;
}

return Client::Instance()->ClientMain(argv + 1);
}

Hier wird die Header-Datei des Clienten inkludiert und in der main wird die Paramteranzahl überprüft.
Wenn unter 2 Paramtern übergeben werden, wird eien kurze Ausgabe gemacht und das Programm beendet.
Hierbei ist zu beachten, dass argc die Anzahl der Argumente + 1 ist.
argv ist ein Array auf char*, hier liegt also ein Array auf eine Zeichenkette vor, wobei argv[1] der erste Paramter und argv[2] der zweite Paramter ist (argv[0] ist übrigens der Dateipfad + Dateiname)
Aus diesem Grunde übergebe ich an dieser Stelle auch argv + 1, was bedeutet, dass das Array ab der Position 1 übergeben wird.
Somit landen die IP und der Port bei ClientMain.
Es wird gleich return geschrieben, das bedeutet, dass der Rückgabewert von ClientMain(...) als Exitcpp übergeben wird.
Da sich aber in ClientMain() eine Schleife befindet und der Rückgabewert erst nach Beendigung der Funktion zurückgegeben wird, wird das Programm trotzdem laufen, bis es der Benutzer beendet.

2.2. Der wirkliche Client:


Nach dem ganzen Gerede über Singletons und Paramterübergaben, wollen wir usn jetzt endlich auf die Socketprogrammierung konzentrieren.
Als Erstes müssen wir die Sockets aktivieren.
Dieser Vorgang ist nur bei Windows nötig, bei Linux ist er nicht erforderlich:
// Die bisherige client.cpp:

#include "client.h"

#include <conio.h>

Client Client::m_Instance;

// =========================================================================

bool Client::StartUp()
{
WSADATA wsa;
int rc = WSAStartup(MAKEWORD(2, 0), &wsa);
if(rc == SOCKET_ERROR)
{
std::cout << "Fehler beim Starten des Servers..." << std::endl;
return false;
}
return true;
}

// =========================================================================

Wir inkludieren als Erstes unsere client.h und noch die Datei <conio.h>, die wird später für eine Unblockierte Texteingabe benötigen werden.
In der Funktion StartUp initialsieren wir nun also die Sockets, dabei ist MAKEWORD(2, 0) nur für die Version wichtig und wsa ist für die Speicherung der Versionsdaten, die ich aber an dieser Stelle nicht auslese, da wir sie nicht benötigen.
Der Rückgabetyp von WSAStartup wird gespeichert und überprüft, schlägt es also fehl, wird die Funktion mit false beendet, andernfalls mit true.

Nun müssen wir unser Socket, das wir ja in der Klassendefinition (Kapitel 2.1) definiert haben, erst einmal richtig erstellen und danach schon an den noch-nicht-vorhandenen Server binden.
bool Client::CreateConnectSocket(char* argv[2])
{
int rc;

s = socket(AF_INET, SOCK_STREAM, 0);
if(s == INVALID_SOCKET)
{
std::cout << "Socket konnte nicht erstellt werden.\n";
return false;
}

addr.sin_addr.s_addr = inet_addr(argv[0]);
addr.sin_port = htons(atoi(argv[1]));
addr.sin_family = AF_INET;

std::cout << argv[0] << ' ' << atoi(argv[1]) << '\n';

rc = connect(s, (SOCKADDR*)&addr, sizeof(SOCKADDR));
if(rc == SOCKET_ERROR)
{
std::cout << "Verbindung fehlgeschlagen.\n";
return false;
}

return true;
}

// =========================================================================

char* argv[2] ist unser Array, dass wir am Anfang übergeben haben.
Wenn wir alles richtig gemacht haben, dann befindet sich in argv[0] die IP-Adresse und in argv[1] der Port.
Die Funktion socket erstellt für uns ein solches udn hat folgden Funktionskopf:
SOCKET socket(int af, int type, int protocol);
int af ist die Adressenfamilie, wir verwenden hier AF_INET.
Bei type kommt es darauf an, ob wir z.B. TCP/IP oder UDP verwenden.
Wir verwenden hier, wie bereits erwähnt TCP/IP, d.h. SOCK_STREAM
protocol ist das zu verwende Protokoll, für genaue Informationen, wir verwenden hier das Standardprotokoll, also 0.
Der Rückgabetyp ist das Socket, da es sich hier nur um ein int handelt, reicht eine Übergabe ohne Zeiger oder Referenzen völlig aus.
Wenn das Socket allerdings ungültig ist, wird eine Fehlermeldung ausgegeben und die Funktion mit false beendet.

Jetzt füllen wir unsere SOCKADDR_IN Struktur.
Diese Struktur enthält die Daten des Servers, zu dem wir uns jetzt verbinden möchten.
sin_addr.s_addr erwartet die zu verbindene IP-Adresse.
Diese haben wir ja in argv[0] gespeichert, aber da das ein char* ist, müssen wir diesen in ein Format verwandeln, mit dem WinSock arbeiten kann.
inet_addr ist unsere Funktion! Wir müssen ihr nur den char* übergeben und der Rückgabetyp ist ein unsigned long, der sogleich an die Struktur übergeben wird.
sin_family erwartet hier wieder unsere übliche Adressenfamilie, AF_INET.
sin_zero ist ein ziemlich dämliches Ding, ich weiß nicht, was es bewirkt und will es auch gar nicht wissen, wir lassen es einfach leer.
Nun haben wir unsere Daten zusammen und können uns endlich zum Server verbinden.
Dafür nutzen wir die Funktion connect, die folgerndermaßen definiert ist:
int connect (SOCKET s, const struct sockaddr FAR* name, int namelen);
Der erste Paramter ist das Socket, das sich verbinden soll.
Der zweite Paramter ist unsere Struktur, aber da SOCKADDR* erwartet wird, müssen wir das kurz casten, was natürlich keinerlei Probleme darstellt.
Der dritte Paramter ist dann die Größe der Struktur.
Ob man sizeof(SOCKADDR_IN) nimmt oder sizeof(SOCKADDR)... Bei mir machte es keinen Unterschied.
Wenn ein Firewall am Laufen ist, wird dieser jetzt hier ausschlagen und fragen, ob die Verbindung erlaubt ist.
Im cpp wird angehalten, bis die Vebrindung steht, oder klar ist, dass es nicht klappt.
Wir überprüfen, was von Beidem der Fall ist, und machen bei Erfolg munter weiter.

Nun widmen wir uns der letzten Funktion des Clienten, die aber auch am größten ist.
Es ist die ClientMain, sie enthält eine Hauptschleife und ruft unsere anderen beiden Funktionen auf.
Am Besten ist es, wenn ich erst einmal den gesamten cpp zeige:
 
int Client::ClientMain(char* argv[2])
{
if(!StartUp())
return 1;

if(!CreateConnectSocket(argv))
return 2;

int rc = 0;
fd_set fdSetRead;
char buffer[1024];
char buffera[1024];
char c;
int bufpos = 0;
TIMEVAL timeout;
bool mainloop = true;

std::cout << "Alles erfolgreich gestartet und verbunden.\n";

while(rc != SOCKET_ERROR && mainloop)
{
while(kbhit())
{
c = getch();
if(c==13)
{
if(strcmp(buffer, "exit" == 0)
{
mainloop = false;
break;
}
rc = send(s,buffer,bufpos, 0);
bufpos = 0;
}
else
buffer[bufpos++]=c;
}
buffer[bufpos] = '\0';

FD_ZERO(&fdSetRead);
FD_SET(s, &fdSetRead);

timeout.tv_sec = 0;
timeout.tv_usec = 0;

while((rc = select(0, &fdSetRead, 0, 0, &timeout)) > 0)
{
rc = recv(s, buffera, 1023, 0);

if(rc == 0)
{
std::cout << "Der Server hat die Verbindung unterbrochen.\n";
break;
}
else if(rc == SOCKET_ERROR)
{
std::cout << "Empfangen der Nachricht fehlgeschlagen: " << WSAGetLastError() << '\n';
break;
}

buffera[rc] = '\0';
std::cout << buffera << '\n';
}

Sleep(1);
}

std::cout << "Client wird beendet.\n";

closesocket(s);
WSACleanup();

return 0;
}

Uff, ein ganz schönes Stück, aber das kriegen wir schon hin.
Als Erstes werden natürlich unsere beiden Funktionen für die Initialisierung und den Verbindungsaufbau aufgerufen.
Schlagen diese fehl, wird 1 oder 2 zurückgeliefert, durch unseren Aufbau werden diese Werte dann direkt als Rückgabewert in der main verwendet.

Jetzt werden eine ganze Menge Variablen definiert:
rc ist einfach nur eine Variable, in die wir die Rückgabetypen der Funktionen stopfen, damit wir erfahren, ob diese erfolgreich waren.
fdSetRead ist ein fd_set. Dazu später...
buffer ist ein Buffer, der für das Versenden von Paketen zu verwenden ist.
buffera hingegen ist der Buffer, der die Pakete empfängt.
Versucht man hingegen für beide Vorgänge nur einen Buffer zu verwenden, artet das in einem Chaos aus.
c ist nur ein Zeichen, das wir für das Lesen mit getch() benötigen und nur temporär verwenden.
bufpos ist sozusagen die Schreibpositonen in dem buffer, damit wir wissen, wo das nächste Zeichen c hinkommen soll.
TIMEVAL ist die Struktur in dem wir das Timeout angeben.

Nun startet unsere Hauptschleife.
Diese läuft entweder so lange, bis sie vom Benutzer abgebrochen wird oder, wenn ein Fehler beim Schreiben und Lesen auftritt.
Nun das Einlesen der Daten vom Benutzer, ohne Blockade:
		while(kbhit())
{
c = getch();
if(c==13)
{
if(strcmp(buffer, "!exit" == 0)
{
mainloop = false;
break;
}
rc = send(s,buffer,bufpos, 0);
bufpos = 0;
}
else
buffer[bufpos++]=c;
}
buffer[bufpos] = '\0';

Die Schleife mit der Bedingung kbhit() bedeutet, dass die Schleife solange ausgeführt wird, wie Zeichen auf der tastatur gedrückt werden.
Sie liefert also nicht Null zurück, wenn wir eine Taste drücken, sonst 0.
Durch getch() erfahren wir dann welche taste gedrückt wurde, oder eher welches Zeichen durch die Kombination von Zeichen erreicht wurde (Shift+a = A).
Dieses Zeichen müssen wir nun zwischenspeichern, und zwar in c.
Jetzt überprüfen wir, ob die letzte Eingabe das Zeichen \n == ENTER war.
Wenn dieses der Fall ist, wird überprüft ob der buffer !exit ist, was die Hauptschleife beenden würde, oder sendet, wenn der Buffer nicht !exit, die Daten an den Server und setzt den Buffer zurück.
Wenn nicht ENTER gedrückt wurde, wird das eingegebene Zeichen an das Char-Array angehängt, sodass sich der Text erweitert.
Nach der Schleife wird das aktuelle Ende mit \0 gekennzeichnet, damit jeder weiß: Da ist der C-String zu Ende.

Nun wollen wir uns noch einmal kurz die Funktion send() ansehen:
int send (SOCKET s, const char FAR * buf, int len, int flags);

Die Funktion ist eigentlich verhältnismäßig einfach.
Der erste Paramter ist unser übliches Socket, der zweite die zu übermittelnden Daten (werden in char* angegeben, weil char netterweise genau 1 Byte groß ist), der dritte die Länge der Daten (bei eienm C-String bedeutet das auch die Anzahl der Zeichen + 1) und dann als vierten Paramter noch ein spezieller ndikator, wie die daten gehen sollen, oder so...
Den können wir einfach 0 lassen.

So, wir können nun also völlig frei Daten an den Server senden, wenn wir gerade nichts senden, durchläuft die Schleife sich etliche Male selber und hat sonst nichts zu tun.
Nun wollen wir aber auch etwas empfangen, was uns der Server sendet.
Dafür sehen wir uns diesen cppteil an:
		FD_ZERO(&fdSetRead);
FD_SET(s, &fdSetRead);

timeout.tv_sec = 0;
timeout.tv_usec = 0;

while((rc = select(0, &fdSetRead, 0, 0, &timeout)) > 0)
{
rc = recv(s, buffera, 1023, 0);

if(rc == 0)
{
std::cout << "Der Server hat die Verbindung unterbrochen.\n";
break;
}
else if(rc == SOCKET_ERROR)
{
std::cout << "Empfangen der Nachricht fehlgeschlagen: " << WSAGetLastError() << '\n';
break;
}

buffera[rc] = '\0';
std::cout << buffera << '\n';
}

So, nun muss ich wohl endlich mal fdSetRead erklären:
typedef struct fd_set
{
u_int fd_count; // Anzahl der Sockets
SOCKET fd_array[FD_SETSIZE]; // Socket-Array
}fd_set;

Diese Struktur beinhaltet alle Sockets die benötigt werden.
Später kann der Socket dann leicht überprüfen, ob von einer der angegebenen etwas Empfangen wird.
Natürlich wäre es Schwachsinn das zu tun, da wir ja nur einen Verbindungspartner, den Server, haben (ganz im Gegensatz zum Server, der natürlich mehrere Clienten unterstützen muss), wenn fd_set nicht noch einen anderen Vorteil hätte.
Dazu gleich!
FD_ZERO löscht den Inhalt des fd_sets udn FD_SET füllt etwas rein.
In diesem Fall müssen wir natürlich unser eigenes Socket hinzufügen, der die Verbindung von uns zu dem Server darstellt.
Als mächstes setzen wir den Timeout auf 0 Sekunden und 0 Millisekunden.
Jetzt kommt eine sehr wichtiger cppteil:

while((rc = select(0, &fdSetRead, 0, 0, &timeout)) > 0)

Die Funktion select ist folgendermaßen definiert:

int select (int nfds, fd_set FAR * readfds, fd_set FAR * writefds, fd_set FAR * exceptfds, const struct timeval FAR * timeout);

Riesenvieh, aber das kriegen wir schon...
nfds wird ignoriert, denn es ist laut MSDN nur für eine Kompatibilität mit den Berkeley Sockets nötig (was auch immer das sein soll). Wir können hier also 0 übergeben (guter Anfang).
Jetzt kommt unsere fd_set Struktur, damit select weiß, von welchen Verbindungen es etwas erwarten muss.
In diesem Fall ist das nur der Server...
writefds ist ein fd_set, das man optional angeben kann, wenn man ein paar Sockets auf Schreibfähigkeit testen möchte.
Können wir hier ebenfalls mit 0 beantworten.
excecptfds ist für einen Satz von Sockets zum Testen auf Fehler.
Hier können wir zur Abwechslung mal 0 schreiben.
Schließlich übergeben wir unser timeout.
Wir haben 0 Sekunden und 0 Millisekunden festgelegt, wenn es also nichts zu empfangen gibt, dann wird die Schleife sofort beendet und geht wieder am Anfang weiter.
Wenn allerdings der Server etwas Schicken will, wird das schnell ausgegeben.
Auf diese Weise, wird die Schleife ständig durchlaufen und wir haben ständiog die Möglichkeit zu empfangen UND zu schreiben.

Sehen wir uns nun aber genauer den Schleifeninhalt an:
			rc = recv(s, buffera, 1023, 0);

if(rc == 0)
{
std::cout << "Der Server hat die Verbindung unterbrochen.\n";
break;
}
else if(rc == SOCKET_ERROR)
{
std::cout << "Empfangen der Nachricht fehlgeschlagen: " << WSAGetLastError() << '\n';
break;
}

buffera[rc] = '\0';
std::cout << buffera << '\n';

recv ist die analoge Funktion zu send, mit ihr können wir Daten empfangen.
Der Rückgabewert ist wieder ein int, der hier die Anzahl der empfangenen Zeichen bedeutet.
Der erste Paramter ist der Socket, der zweite der Buffer, in dem das Empfangene gespeichert wird, der dritte die maximale Anzahl der Zeichen, und der vierte wieder unsere flags, die wir 0 lassen.
Danach geht es einfach weiter, wir testen entweder ob 0 Zeichen an uns gesendet wurden...
Ist dieses der Fall fühlen wir uns erstmal betrogen, da man ja mindestens ein char-Zeichen empfangen möchte. (wenn er nichts zu senden hat, dann sendet er nämlich auch nichts)
Wir sehen das als Zeichen, dass uns der Server nicht mehr haben will und die Verbindung mit uns beendet hat.
Alternativ kann es auch sein, dass der Server offline gegangen ist, ohne sich richtig zu beenden, dann gibt es einen Fehler beim Empfangen der Daten und wir beenden wieder.
Gibt es keine Probleme mit der nachricht, setzen wir ans Ende des Buffers ein \0, damit das danach folgende cout weiß, dass der Buffer dort endet.
Jetzt steht da noch ein Sleep... (nur in Windows, aber in Linux heißt es glaub ich sleep, oder so ähnlich)
Bei diesen sehr schnellen Schleifendurchläufen, weil wir kein timeOut gesetzt haben und alles unblockierend gemacht haben, blockiert das Programm den gesamten Prozesspeicher bzw. soviel es kann.
Bei Windows 2000 oder XP kann man das schön betrachten wenn man im Taskmanager auf Systemleistung schaut.
Sleep lässt den Prozess also eine Runde ,,schlafen'', in diesem Fall reicht sogar eine einzige Millisekunde und schon ist die Belastung weg.
Zu guter Letzt beenden wir nach der Schleife das einzige Socket und rufen WSACleanup auf, damit WinSock wieder lahmgelegt wird.
In Linux ist WSACleanup unnötig und statt closesocket sagt man da einfach nur close(s).


So, das wärs zu dem Clienten, das Praktische ist, dass man ihn, so wie er jetzt ist (ohne jede indung an einen Chat), ein sehr allgemeiner Client ist, ähnlich wie telnet (wer's kennt).
Zum Beispiel kann man sich zu einem FTP Server verbinden und dann die jeweiligen Befehle ausführen. (dafür muss man allerdings an den zu sendenen buffer noch "\r\n" anhängen)
Viel Spaß mit dem Clienten jedenfalls...
Ich mach jetzt erstmal ne' Pause und dann geht's an den Server.

3. Der Server:



Ein Client verbindet sich... und zwar zu einem Server.
Natürlich gibt es etliche Server, die man nutzen kann, wie z.B. IRC Server, falls man einen IRC Client schreibt, oder auch einen FTP Server...
Aber da wir ja unseren tollen (?) Chat machen wollen, benötigen wir einen eigenen und sowas programmieren wir uns jetzt natürlich selber.
Wir starten erst einmal mit der main.cpp:
#include "server.h"

int main(int argc, char* argv[])
{
if(argc < 2)
{
std::cout << "Sie muessen das Programm folgendermassen starten: Server <Port>\n";
return 1;
}

return Server::Instance()->ServerMain(argv[1]);
}

Wir verwenden wieder eine Singleton-Klasse und überprüfen, ob genug Parameter übergeben werden.
Ich denke hierzu muss ich ansonsten nichts sagen, das habe ich ja im letzten kapitel schon ausführlich beschrieben.
Villeicht ist noch erwähnenswert, dass wir hier keine IP benötigen, sondern nur den Port und dass wir deswegen auch nur das erste Element übergeben.
Schreiten wir also gleich in die server.h:
#ifndef _CHATSERVER_
#define _CHATSERVER_

#include <winsock.h>
#include <iostream>
#include <string>
#include <vector>

struct CConnection
{
SOCKET s;
SOCKADDR_IN addr;
std::string username;
};

typedef std::vector <CConnection> CVec;
typedef std::vector <CConnection>::iterator CVec_it;

class Server
{
private: // Variablen
static Server m_instance;
SOCKET s;
SOCKADDR_IN addr;
CVec m_clients;
int clientcount;
private: // Funktionen
bool StartUp();
bool CreateBindSocket(char* argc);
void SendToAllClients(std::string);
void SendToClient(SOCKET s, std::string str);
public: // Methoden
static Server* Instance() {return &m_instance;}
int ServerMain(char* argc);
};

#endif

Villeicht ist es nicht so ganz elegant, gleich den ganzen cpp zu posten.
Also die #ifndef-#define-#endif Konstruktion sorgt wie im letzten Kapitel dafür, dass es bei Mehrfachinkludierung der Datei keine Probleme gibt.
In diesem Kapitel nutzen wir die beliebte STL von C++.
Wir benötigen den Vector, deswegen inkludieren wir die vector Datei.
Da wir hier auch ein paar Sachen aus den Strings filtern, nutzen wir hier für das Versenden von Strings auch einen STL-String.
Für das Empfangen von Daten ist ein char* allerdings unumgänglich, deswegen verwenden wir hier mehr oder weniger Beides. *brr*
Wie auch immer, zurück zum cpp:
Die Struktur CConnection beinhaltet alle wichtigen Daten, die ein Client haben muss.
Die Clienten bekommen also ein SOCKET, eine eigene SOCKADDR_IN Struktur (damit wir jederzeit die IP und anderen Daten zur Verfügung haben) und sogar einen Benutzernamen (was für ein Luxus).
Den Benutzernamen könnte auch der Client speichern und dann einfach an den Server mitsenden, allerdings sollte man möglichst vieles vom Server berechnen lassen, damit der Client nicht einfach schummelt und Sachen macht, die nicht erlaubt sind.
Es könnte auch z.B. einfach jemand anderes einen Clienten für unseren Server machen und wenn wir einfach erlauben würden, jede Nachricht durchgehen zu lassen, dann könnte derjenige einfach einen Clienten schreiben, der unter dem benutzernamen eines anderen irgendwelche Sachen schreibt.
So etwas wollen wir natürlich verhindern.
Nun kommen wir zu den beiden typedefs.
Hier deklarieren wir unseren Vector.
typedef std::vector <CConnection>		      CVec;
typedef std::vector <CConnection>::iterator CVec_it;

typedef bedeutet, dass wir sozusagen einen neuen Typ deklarieren.
Das tun wir auch, denn vector ist ein Template.
Wir erstellen unseren Vector also auf unsere CConnection Struktur, das bedeutet, dass wir einen vector (erweitertes Array) von Benutzerstrukturen haben.
Wir können dann einfach den Umgang mit den einzelnen Benutzern handhaben.
Man könnte stattdessen auch eine Map machen, dessen Primärschlüssel ein std::string ist, der den Benutzernamen repräsentiert, aber uns soll ein Vector hier genügen.

Dann deklarieren wir noch einen Iterator für unseren Vector, mit dem wir den Vector durchgehen können... Dazu später.

Diese beiden typedefs sind lediglich Deklarationen, keine Definitionen.
Wir können also nicht schreiben: CVec.insert(...), sondern müssen die Variablen dafür erst noch definieren.
Das machen wir in unserer Klasse.
Dort defineiren wir außerdem unser Server SOCKET, die Server SOCKADDR_IN Struktur, unsere übliche statische Instanz, die Anzahl der Clienten und zu guter letzt auch eine Definition von CVec, in der wir alle Clienten speichern können.
Ansonsten ist hier wieder alles unterteilt in private und öffentliche Funktionen.
argv ist hier nur ein einfaches char*, denn wir benötigen beim Server nur den Port.
Kommen wir nun zu der Implementierung unserer Funktionen.
Es läuft wie üblich ab, die ServerMain wird aufgerufen und gibt einen Rückgabetyp zurück, sobald die Hauptschleife beendet ist.
Erst einmal implementieren wir die Funktionen StartUp und CreateBindSocket; dazu mal gleich etwas mehr cpp:
#include "Server.h"

Server Server::m_instance;

// =========================================================================

bool Server::StartUp()
{
WSADATA wsadata;
int rc = WSAStartup(MAKEWORD(2,0), &wsadata);
if(rc == SOCKET_ERROR)
{
std::cout << "Fehler beim Starten des Servers..." << std::endl;
return false;
}
return true;
}

// =========================================================================

bool Server::CreateBindSocket(char* argv)
{
int rc;

s = socket(AF_INET, SOCK_STREAM, 0);
if(s == INVALID_SOCKET)
{
std::cout << "Socket konnte nicht erstellt werden: " << WSAGetLastError() << std::endl;
return false;
}

memset(&addr, 0, sizeof(SOCKADDR_IN));
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argc));
addr.sin_addr.s_addr = INADDR_ANY;
rc = bind(s, (SOCKADDR*)&addr, sizeof(SOCKADDR_IN));
if(rc == SOCKET_ERROR)
{
std::cout << "Socket konnte nicht gebunden werden: " << WSAGetLastError() << std::endl;
return false;
}

return true;
}

// =========================================================================

Mit StartUp ist es wie gehabt...
In CreateBindSocket müssen wir es jetzt etwas anders machen, als beim Clienten.
Als Erstes erstellen wir unser Socket natürlich.
Danach muss es dann aber nicht zu irgendwem vebrunden werden, sondern nur gebunden werden.
Wir müssen natürlich unsere Struktur erstmal schön leeren, was wir mit dieser memset Funktion machen.
memset erwartet als ersten Parameter einen Zeiger auf die zu füllenden Struktur, als zweiten den Füllwert und als dritten die Größe der Struktur.
INADDR_ANY ist eine sehr feine Sache.
Dadurch muss der Server nicht wissen, welche IP der jewielige Client hat, sondern die Clienten melden sich beim Server und der macht dann irgendwas; dazu später...
Die IP-Adresse, die wir bei sin_addr.s_addr angeben, muss natürlich wieder von char* auf unsigned int konvertiert werden.
Sobald das Socket dann gebunden ist, ist der Server für Weiteres bereit.

Auch wenn das Senden zu diesem zeitpunkt noch nicht möglich ist, schreiben wir jetzt schon die Funktionen dafür, damit wir uns danach vollstens der ServerMain zuwenden können:
// =========================================================================

void Server::SendToAllClients(std::string str)
{
int rc;
for(CVec_it it = m_clients.begin(); it != m_clients.end(); ++it)
{
if(it->username != "!undefiniert"
rc = send(it->s, str.c_str(), str.length(), 0);
}
}

// =========================================================================

void Server::SendToClient(SOCKET s, std::string str)
{
int rc;
rc = send(s, str.c_str(), str.length(), 0);
}

// =========================================================================

Erst einmal die SendToClient-Funktion:
Diese sendet an einen Clienten.
Der Rückgabewert wird hier gespeichert aber nicht überprüft, das Senden läuft hier wie üblich.
Als erster Paramter wird der Socket angegeben, an den gesendet werden soll.
Der zweite Paramter ist der String, da die Funktionen einen char* erwartet müssen wir die Funktion c_str() von std::string aufrufen, welche den C-String zurückgibt.
Der dritte Paramter ist wieder einmal die Größe.
Da diese ja gleich der Länge des Strings + 1 ist, ist die Funktion length() optimal, da sie die Länge der Zeichenkette zurückgibt.

Jetzt schauen wir uns noch die Funktion SendToAllClients an.
In dieser Funktion wird der Text an alle Clienten geschickt, die sich im m_clients-vector befinden.
Im m_clients Vector sind ausschließlich gültige Clienten, da sie vorher überprüft werden.
Ich erkläre jetzt erst einmal, wie wir alle Elemente von m_clients durchgehen:

for(CVec_it it = m_clients.begin(); it != m_clients.end(); ++it)

Es wird ein Iterator für unseren Vector erstellt.
Der Vector-Iterator ist ein Zeiger auf die einzelnen Elemente des Vectors.
Wir setzen ihn zu Beginn auf m_clients.begin(), das ist die Anfangsadresse der Clienten.
Und zwar soll die Schleife solange laufen, wie der Iterator nicht an Ende des Vectors ist.
In jedem Schleifendruchlauf, wird der Vector vorgeschoben.
Auf diese Weise durchlaufen wir alle Elemente des Vectors.

Jetzt wird nur gesendet, wenn der Benutzer kein undefinierter ist.
Ein undefinierter Benutzer hat den Benutzernamen !undefiniert, das bedeutet dass er noch keinen Namen angegeben hat.
Unser Server wird, sobald sich ein Client zu ihm verbunden hat, von diesem erwünschen seinen benutzernamen anzugeben.
Der Client wird direkt dann erstellt, wenn sich jemand zum Server verbunden hat.
Der Benutzername bleibt solange auf !undefiniert, bis der Server von dem CLienten den Benutzernamen empfangen hat, den er verwenden möchte.
Diese Verzweigung im cpp bedeutet hier nur, dass an noch undefinierte Benutzer keine Nachrichten geschickt werden sollen, weil sonst einfach Leute hereinkommen, den Benutzernamen weglassen und dann unsichtbar alles mithören können.

So, jetzt kommt die ServerMain, da dürfen wir uns auf einiges gefasst machen!
int  Server::ServerMain(char* argv)
{
int rc;
fd_set fdSetRead;
CConnection tCon;
int addrsize = sizeof(SOCKADDR_IN);
CVec_it it;
char buf[1024];
std::string temp;

if(!StartUp())
return 1;

if(!CreateBindSocket(argc))
return 2;

rc = listen(s, 30);

std::cout << "Server wurde erfolgreich gestartet...\n";

while(true)
{
FD_ZERO(&fdSetRead);

FD_SET(s, &fdSetRead); // Eintragen des Servers!

for(it = m_clients.begin(); it != m_clients.end(); ++it)
{
FD_SET(it->s, &fdSetRead);
}

rc = select(0, &fdSetRead, 0, 0, 0);

if(rc < 1) break;

for(int i = 0; i < fdSetRead.fd_count; ++i)
{
// Will ein Client dazukommen?
if(fdSetRead.fd_array == s)
{
tCon.username = "!undefiniert";
tCon.s = accept(s, (SOCKADDR*)&tCon.addr, &addrsize);

m_clients.push_back(tCon);

std::cout << "Neuer Client hinzugefuegt IP: " << inet_ntoa(tCon.addr.sin_addr) << "\r\n";

rc = send(tCon.s, "Bitte geben Sie hren Benutzernamen ein: ", 10, 0);

break;
}

// Hat ein Client eine andere Bitte?

for(it = m_clients.begin(); it != m_clients.end(); ++it)
{
if(it->s == fdSetRead.fd_array[i])
{
rc = recv(it->s, buf, 1023, 0);
buf[rc] = '\0';

if(rc == 0)
{
if(it->username != "!undefiniert"
{
std::cout << "Der Client hat die Verbindung getrennt.\n";
temp = std::string("Der Benutzer " + it->username + std::string("hat den Chat verlassen.\r\n";
SendToAllClients(temp);
}
closesocket(it->s);
m_clients.erase(it);
break;
}
else if(rc == SOCKET_ERROR)
{
if(it->username != "!undefiniert"
{
std::cout << "Fehler beim Empfangen von " << inet_ntoa(it->addr.sin_addr) << '\n';
std::cout << "Client trennt die Verbindung.\n";
temp = std::string("Der Benutzer " + it->username + std::string("hat den Chat verlassen.\r\n";
SendToAllClients(temp);
}
closesocket(it->s);
m_clients.erase(it);
break;
}
else
{
if(it->username == "!undefiniert"
{
bool ok = true;
for(CVec_it vict = m_clients.begin(); vict != m_clients.end(); ++vict)
{
if(vict->username == buf)
{
SendToClient(it->s, "Fehler: Benutzername bereits vorhanden.";
ok = false;
break;
}
}
if(ok)
{
it->username = buf;
temp = std::string("Der Benutzer " + it->username + std::string(" loggt sich ein.";
SendToAllClients(temp);
}
}
else
{
std::cout << inet_ntoa(it->addr.sin_addr) << "> " << it->username << buf << '\n';
temp = it->username;
temp += ": ";
temp+= buf;
SendToAllClients(temp);
}
}
} // Ende der Bedingung (it -> s == fdSetRead-fd_array[i])

} // Ende (it = m_clients.begin(); it != m_clients.end(); ++it) Schleife

} // Ende (int i = 0; i < fdSetRead.fd_count; ++i) Schleife

} // Ende Hauptschleife


return 0;
}

Keine Sorge, hiernach haben wir es endlich geschafft.
Arbeiten wir hier wieder Stückchen für Stückchen alles ab:
	int         rc;
fd_set fdSetRead;
CConnection tCon;
int addrsize = sizeof(SOCKADDR_IN);
CVec_it it;
char buf[1024];
std::string temp;

if(!StartUp())
return 1;

if(!CreateBindSocket(argc))
return 2;

rc ist wieder für das Überprüfen der Rückgabewerte, fd_set ist wieder unser Satz für alle Clienten die sich zu uns verbinden, der Server muss auch dazu; tCon ist ein temporäres Objekt für unsere Verbindungen.
Wir benötigen es zum Zwischenspeichern von Clientdaten... Dazu später.
Da wir die Größe der SOCKADDR_IN Struktur des Öfteren benötigen, speichern wir den Wert hier gleich am Anfang.
CVec_it ist wieder ein Iterator, den wir für das Durchlaufen unseres m_Client Vectors benötigen.
buf ist unser Buffer, den wir für das Empfangen benötigen und temp ein String, den wir für das Versenden verwenden.
Dann rufen wir wieder die Funktionen zum Initialisieren von WinSock und für das Erstellen und Binden des Sockets auf und beenden die Funktion ggf.

Ich habe hier eine Endlosschleife für die Hauptschleife genommen, das ist nicht besonders gut, da die Sockets dann nicht einzeln beendet werden.
Man kann es aber verkraften und ein guter Server sollte sich sowieso niemals beenden. *g*
	rc = listen(s, 30);

listen versetzt den Server in den Status, wo es auf die Nachrichten der Clienten wartet.
Der erste Paramter reprsänetiert das Server-Socket, der zweite die Anzahl der maximalen Clienten.
30 sollten eigentlich reichen.

Gehen wir nun an das Innere der Schleife.
Ich weiß, es ist ekelerregend, aber da muss man nunmal durch:
		FD_ZERO(&fdSetRead);

FD_SET(s, &fdSetRead); // Eintragen des Servers!

for(it = m_clients.begin(); it != m_clients.end(); ++it)
{
FD_SET(it->s, &fdSetRead);
}

rc = select(0, &fdSetRead, 0, 0, 0);

if(rc < 1) break;

Erst einmal leeren wir wieder den Satz.
Danach tragen wir den Server ein und danach wieder mit einem Vectordurchlauf alle Clienten.
Mich verwunderte es besonders, dass dies in jedem Schleifendurchlauf geschieht, aber das ist schon in Ordnung so, da es auch in jedem Schleifendurchlauf geleert wird.

Danach rufen wir wieder die select-Funktion auf, der Server muss keine Eingaben des Benutzers empfangen und braucht deswegen auch kein imeout.
Folglich wartet er hier immer, bis irgendwas passiert, reagiert dann und wartet dann wieder, bis etwas passiert.
Das belastet auch nicht den Prozesser.
Bei einem Fehler liefert select 0 oder weniger zurück, dann wird eingegriffen und der Server beendet.

Jetzt müssen wir auf die einzelnen Sachen reagieren, die so im Leben eines Servers passieren.
		for(int i = 0; i < fdSetRead.fd_count; ++i)
{
// Will ein Client dazukommen?
if(fdSetRead.fd_array[i] == s)
{
tCon.username = "!undefiniert";
tCon.s = accept(s, (SOCKADDR*)&tCon.addr, &addrsize);

m_clients.push_back(tCon);

std::cout << "Neuer Client hinzugefuegt IP: " << inet_ntoa(tCon.addr.sin_addr) << "\r\n";

rc = send(tCon.s, "Bitte geben Sie hren Benutzernamen ein: ", 10, 0);

break;
}

Wir starten erst einmal eine Schleife durch alle im Satz eingetragenen Clienten durchläuft.
Dei im fd_setRead eingetragenden Clienten sind übrigens nur die, die etwas zu melden haben, deswegen muss auch in jedem Schleifendurchlauf, der Satz neu gefüllt werden.
Wenn jedenfalls einer der Sockets, die etwas zu melden haben, das Serversocket ist, dann bedeutet das, dass sich ein Client zum Server verbunden hat.
Jetzt können wir unsere temporäre Verbindungsstruktur nutzen.
Wir setzen als Erstes den Namen auf !undefiniert, damit kennzeichnen wir, dass der Benutzername noch nicht eingetragen ist.
Die nächste Zeile wird etwas interessanter.
Nachdem wir wissen, dass sich ein Client zu dem Server verbinden möchte, akzeptieren wir das und erstellen das Socket, indem wir accept verwenden:
[i]accept(s, (SOCKADDR*)&tCon.addr, &addrsize);

Hierzu mal der Funktionskopf der Funktion accept:
SOCKET accept (SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen);

Der erste Paramter ist das Serversocket, der zweite Paramter ist die Struktur, in der die Daten des Sockets, das sich zum Server verbunden hat, gespeichert werden sollen und der dritte die Größe der Struktur.
Die Funktion erwartet hier wieder SOCKADDR* und nicht SOCKADDR_IN*, deswegen casten wir hier wieder kurz.

Nachdem wir die daten haben und auch in das temporäre Objekt tCon geschrieben haben, können wir dieses einfach in unseren Vector einfügen und schon ist dieser um ein Element reicher.
Wir geben noch rasch aus, dass sich ein Client verbunden hat...
Die Ausgaben mit cout sind im Server nur für den nützlich, der den Server auch startet, dieser kann dann die IP einsehen und wie der Benutzer aus dem Chat gegangen ist etc.
Der neue Client ist jetzt zwar eingetragen und mit dem Server verbunden, aber öffentlich für alle Clienten, dass es ihn gibt, ist es nicht, niemand weiß von ihm.
Wir senden ihm kurz die Bitte, dass er uns doch den Benutzernamen vorbeischicken soll.

Jetzt müssen wir auf die Nachrichten eingehen, die uns so ein Client schicken kann.
Wir müssen als Spezialfall behandeln, dass der Server evtl. auch noch auf den Benutzernamen eines Clienten wartet.
			for(it = m_clients.begin(); it != m_clients.end(); ++it)
{
if(it->s == fdSetRead.fd_array)
{
rc = recv(it->s, buf, 1023, 0);
buf[rc] = '\0';

if(rc == 0)
{
if(it->username != "!undefiniert"
{
std::cout << "Der Client hat die Verbindung getrennt.\n";
temp = std::string("Der Benutzer " + it->username + std::string("hat den Chat verlassen.\r\n";
SendToAllClients(temp);
}
closesocket(it->s);
m_clients.erase(it);
break;
}
else if(rc == SOCKET_ERROR)
{
if(it->username != "!undefiniert"
{
std::cout << "Fehler beim Empfangen von " << inet_ntoa(it->addr.sin_addr) << '\n';
std::cout << "Client trennt die Verbindung.\n";
temp = std::string("Der Benutzer " + it->username + std::string("hat den Chat verlassen.\r\n";
SendToAllClients(temp);
}
closesocket(it->s);
m_clients.erase(it);
break;
}
else
{
if(it->username == "!undefiniert"
{
bool ok = true;
for(CVec_it vict = m_clients.begin(); vict != m_clients.end(); ++vict)
{
if(vict->username == buf)
{
SendToClient(it->s, "Fehler: Benutzername bereits vorhanden.";
ok = false;
break;
}
}
if(ok)
{
it->username = buf;
temp = std::string("Der Benutzer " + it->username + std::string(" loggt sich ein.";
SendToAllClients(temp);
}
}
else
{
std::cout << inet_ntoa(it->addr.sin_addr) << "> " << it->username << buf << '\n';
temp = it->username;
temp += ": ";
temp+= buf;
SendToAllClients(temp);
}
}
} // Ende der Bedingung (it -> s == fdSetRead-fd_array[i])

} // Ende

[i](it = m_clients.begin(); it != m_clients.end(); ++it) Schleife

Riesen Schleife, kurzer Sinn.
Es werden alle Vectorelemente durchlaufen und mit dem jewiligen Obejkt des Satzes verglichen.
Stimmen diese werte überein, hat genau dieser Client etwas zu melden... oder auch nicht.
Jedenfalls empfangen wir die daten erst einmal und setzen an das Ende des Strings (= der Rückgabewert von recv) ein \0 damit der String ein abgeschlossener ist.
Wenn rc == 0 ist, dann ist irgendwas schief gelaufen...
Nun schließen und löschen wir das Socket wieder sauber ab, indem wir closesocket (unter Windows) oder nur close (unter Linux) aufrufen.
Außerdem kicken wir das CConnection-Objekt des Benutzer aus unserem Vector heraus.
Wenn der Benutzer bereits offiziell online war, dann schreiben wir im Server eine kurze Nachricht und senden an alle Clienten die Botschaft, dass der Client von uns gegangen ist.
Ist der Benutzer aber nur dagewesen ohne seinen Benutzernamen einzugeben und ist dann wieder gegangen, so löschen wir nur sein Socket und seinen Platz im Vector.
Ist ein Fehler beim Empfangen einer Nachricht des Sockets vorgefallen, so wird das gleiche gemacht.

Jetzt kommen wir zu einer weiteren Verzweigung, in der wir überprüfen, ob der Benutezr noch !undefiniert ist, dann nämlich wird seine Eingabe als Benutzername verwendet, wenn sie denn noch nicht da ist:
						if(it->username == "!undefiniert"
{
bool ok = true;
for(CVec_it vict = m_clients.begin(); vict != m_clients.end(); ++vict)
{
if(vict->username == buf)
{
SendToClient(it->s, "Fehler: Benutzername bereits vorhanden.";
ok = false;
break;
}
}
if(ok)
{
it->username = buf;
temp = std::string("Der Benutzer " + it->username + std::string(" loggt sich ein.";
SendToAllClients(temp);
}
}
else
{
std::cout << inet_ntoa(it->addr.sin_addr) << "> " << it->username << buf << '\n';
temp = it->username;
temp += ": ";
temp+= buf;
SendToAllClients(temp);
}

Um zu Überprüfen ob der benutzername schon vorhanden ist, müssen wir alle Vectorelemente durchlaufen und mit dem Benutzernamen vergleichen.
Wenn er schon vorhanden ist, sendet der Server eine Fehlermeldung an den jeweiligen Clienten und wird die nächste Nachricht wieder als Benutzernamen ,,ausprobieren''.
Wenn aber alles ok ist, wird der Benutzername gesetzt und eine Nachricht an alle Clienten gesendet, dass er ab jetzt mit dabei ist.

Der letzte else-Abschnitt:
Wenn kein Benutzername erwartet wird, wird im Server erst einmal die IP der Nachricht, der Benutezrname des Schreibers und natürlich der Text ausgegeben.
Dann wird der String zusammengemixt und an alle Clienten geschickt, sodass jeder die Nachricht des Clienten empfängt.

Da die Schleife sinnigerweise niemals verlassen wird, habe ich dort auch keine Funktionen zum Beenden der Sockets und von WinSock gemacht.
Das kann ja noch ohne Probleme erweitert werden.

4. Nachträgliches und Probleme:


So, das war nun mein erstes Tutorial und ich hoffe, es war gut verständlich.
Eine Klage war z.B., dass man bei dem Clienten nicht sehen kann, was man schreibt, bis man es abgeschickt hat.
Das ist in der Tat etwas dämlich, wäre eine nette Aufgabe das zu verbessern, man könnte z.B. jedes Zeichen via cout ausgeben, aber dann hat man nachher doppelte Nachricht, also müsste man den empfangenen string mit dem geschriebenenen vergleichen und ggf. nicht ausgeben.

Ein weiteres Problem, was mich in letzter Zeit öfters generft hat, war, dass man bei Routern keine Verbindung von außen bekommen kann.
Das liegt daran, dass der Router wie ein Firewall wirkt und nach innen keine Verbindung durchlässt.
Dann muss man ,,Port Forwarding'' einstellen, d.h. den jeweiligen Port, den man verwendet freischalten, das unterstützt fast jeder Router, und danach funktioniert alles.

Ich habe den Konsolenclienten nicht weiter vertieft, aber einen schönen Clienten mit MFC erstellt.
Der Server arbeitet bei mir immernoch in der Konsole, aber hat mehr Funktionen, z.B. arbeitet er mit Userliste und der Client kann diese darstellen und arbeitet auch mit schönen Farben.
Näheres dazu kann man auf meiner Homepage, www.mihahome.de erfahren.
Wenn es Fragen gibt, dann könnt ihr mir diese gerne im Forum über persönliche Nachricht, via Mail oder als Eintrag im Forum stellen.