Narses´ Netzwerk-Tutorials - Navigation
- FAQ-Beitrag: Socket-Komponenten nachinstallieren (ab D7)
- hier Netzwerk-Basics - Minimaler Chat für Anfänger
- Terminatorzeichen-Protokoll - Grundlagen
- Terminatorzeichen-Protokoll - Erweiterungen
- Binär-Protokoll - Für Fortgeschrittene
- Netzwerk-Spiel - Multiplayer TicTacToe
- UDP LAN-Chat - Der Chat ohne Server
Wie macht man einen LAN/Internet-Chat mit TServerSocket und TClientSocket?
Hier ist ein Anfänger-Tutorial für einen (wirklich minimalen) Netzwerk-Chat auf Basis der Socket-Komponenten (
TServerSocket und
TClientSocket). Falls die Sockets nicht in der Komponenten-Palette verfügbar sein sollten, ist im oben erwähnten FAQ-Beitrag eine ausführliche Anleitung zur Nachinstallation. Das Tutorial ist mit Delphi 7 Pro erstellt worden (ich sehe aber keine Probleme mit anderen Delphi-Versionen, solange die Socket-Komponenten installiert sind). In den Personal Editions von Delphi sind die Socket-Komponenten leider nicht in der IDE verfügbar, können aber trotzdem dynamisch verwendet werden. Mehr dazu ganz am Ende des Textes bei den Anhängen.
Wir werden für unseren Chat zwei Programme schreiben, einen
Client, den die Chat-Teilnehmer verwenden werden und einen
Server, der die Clients miteinander verbindet. Nochmal deutlich: nur einmal das Server-Programm starten und dann benutzt jeder einen Client, um am Chat teilzunehmen (auch der, der das Server-Programm gestartet hat!).
Die Clients verbinden sich dann mit dem einen Server-Programm (wie das genau geht, kommt gleich noch, bitte weiterlesen).
Zunächst das Server-Programm. Wir starten die IDE und legen ein neues Projekt an. Damit wir nicht ganz so viel an der Oberfläche herumbasteln müssen, hier das Formular in der Textdarstellung. Zum Übernehmen einfach das leere Formular anklicken, ALT+F12 drücken, dann wird in die Textdarstellung gewechselt. Jetzt den Text unten markieren, kopieren und dann den Inhalt im IDE-Fenster für das Formular komplett ersetzen (alles markieren und den kopierten Text von hier einfügen):
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:
| object Form1: TForm1 Left = 226 Top = 121 Width = 209 Height = 132 Caption = 'Server' Color = clBtnFace Constraints.MinHeight = 132 Constraints.MinWidth = 209 Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -11 Font.Name = 'MS Sans Serif' Font.Style = [] OldCreateOrder = False Position = poDesktopCenter OnCreate = FormCreate OnDestroy = FormDestroy PixelsPerInch = 96 TextHeight = 13 object Log: TMemo Left = 8 Top = 8 Width = 185 Height = 89 Anchors = [akLeft, akTop, akRight, akBottom] TabOrder = 0 end object ServerSocket1: TServerSocket Active = False Port = 0 ServerType = stNonBlocking OnClientRead = ServerSocket1ClientRead Left = 88 Top = 40 end end |
Mit ALT+F12 schalten wir wieder in die grafische Darstellung des Formulars zurück. Jetzt können wir den Code übernehmen, dabei ersetzen wir den Standard-Quelltext in der IDE einfach komplett:
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: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60:
| unit Unit1;
interface
uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ScktComp;
const MY_PORT = 12345; type TForm1 = class(TForm) Log: TMemo; ServerSocket1: TServerSocket; procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure ServerSocket1ClientRead(Sender: TObject; Socket: TCustomWinSocket); private public end;
var Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject); begin ServerSocket1.Port := MY_PORT; ServerSocket1.Open; Log.Lines.Add('Server online.'); end;
procedure TForm1.FormDestroy(Sender: TObject); begin ServerSocket1.Close; end;
procedure TForm1.ServerSocket1ClientRead(Sender: TObject; Socket: TCustomWinSocket); var MyMessage: AnsiString; i: Integer; begin MyMessage := Socket.RemoteAddress+': '+Socket.ReceiveText; Log.Lines.Add(MyMessage); for i := 0 to ServerSocket1.Socket.ActiveConnections-1 do ServerSocket1.Socket.Connections[i].SendText(MyMessage); end;
end. |
Hier ein paar kurze Erläuterungen zum Quelltext:
Ganz am Anfang definieren wir eine Konstante für den Port, auf dem die Kommunikation stattfinden soll:
Delphi-Quelltext
1: 2:
| const MY_PORT = 12345; |
Einen Port kann man sich als virtuelle Steckdose an einem PC vorstellen. Damit ist es möglich, gleichzeitig mehrere Verbindungen über die gleiche IP-Adresse abzuwickeln. Dabei gilt es allerdings zu beachten, dass es bestimmte vordefinierte Ports gibt (sog. "well known ports"), die man nicht für eigene Anwendungen verwenden sollte. Dieser reservierte Bereich geht von 1-1024 (TCP). Fazit: Für eigene Anwendungen sollte man Portnummern oberhalb 1024, noch besser fünfstellige, verwenden.
Im FormCreate-Ereignis weisen wir den Port zu und aktivieren dann den Serverdienst. Ab jetzt können sich Clients mit dem Server verbinden. Dazu braucht der Client die IP-Adresse des PCs, auf dem der Server läuft und die Portnummer, an der der Server auf Clients wartet. Dazu später beim Client mehr.
Im FormDestroy-Ereignis schließen wir noch den Serverdienst, das Programm soll ja beendet werden. Dabei werden eventuell noch verbundene Clients zwangsgetrennt.
Bleibt nur noch das OnClientRead-Ereignis des Servers, dass wir uns jetzt ganz genau ansehen werden:
Delphi-Quelltext
1: 2:
| procedure TForm1.ServerSocket1ClientRead(Sender: TObject; Socket: TCustomWinSocket); |
Dieses Ereignis tritt ein, wenn eine Verbindung zu einem Client Daten empfangen, oder anders gesagt, wenn ein Client Daten gesendet hat. Dabei wird in
Sender die Komponente geliefert, die das Ereignis ausgelöst hat (hier:
ServerSocket1) und in
Socket die Verbindung, die Daten empfangen hat (hier: ein Element des
ServerSocket1.Socket.Connections[]-Array).
Delphi-Quelltext
1: 2:
| MyMessage := Socket.RemoteAddress+': '+Socket.ReceiveText; |
Wir bauen uns hier aus
Socket.RemoteAddress, das die IP-Adresse des Clients enthält, und aus
Socket.ReceiveText, was die gesendeten Daten liefert, ein eigenes Nachrichtenformat zusammen. Auf diese Weise können wir die Nachrichten wenigstens grob unterscheiden. Warum nehmen wir keine Nicknames? Das geht zwar theoretisch, allerdings wird es an dieser Stelle schon schwer, ohne ein Protokoll auszukommen. Wir wollen den Rahmen dieses Tutorial nicht sprengen und verzichten deshalb auf Nicknames. Wer an dieser Stelle mehr wissen möchte, sollte mal
hier nachsehen.
Delphi-Quelltext
1:
| Log.Lines.Add(MyMessage); |
Damit schreiben wir die Nachricht in das Protokoll-Fenster des Servers.
Delphi-Quelltext
1: 2: 3:
| for i := 0 to ServerSocket1.Socket.ActiveConnections-1 do ServerSocket1.Socket.Connections[i].SendText(MyMessage); |
Das sind eigentlich die entscheidenden Zeilen des Server-Codes: Die Schleife läuft über die Anzahl der aktuell verbundenen Clients (startet mit 0, deshalb -1 am Ende) und sendet an jede Verbindung den empfangenen Text (IP+Nachricht). Auf diese Weise erhalten alle Clients den gesendeten Text, der nur von einem Client kam.
Jetzt zum Client. Wir starten eine weitere IDE und legen ein neues Projekt an (wir wollen ja auch zwei Programme schreiben). Hier die Formulardaten in Textform, wie beim Server einfach mit ALT+F12 ersetzen:
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: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100:
| object Form1: TForm1 Left = 212 Top = 118 Width = 227 Height = 235 Caption = 'Client' Color = clBtnFace Constraints.MinHeight = 235 Constraints.MinWidth = 227 Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -11 Font.Name = 'MS Sans Serif' Font.Style = [] OldCreateOrder = False Position = poDesktopCenter OnCreate = FormCreate OnDestroy = FormDestroy PixelsPerInch = 96 TextHeight = 13 object Label1: TLabel Left = 8 Top = 8 Width = 71 Height = 13 Caption = 'Serveradresse:' end object Label2: TLabel Left = 8 Top = 48 Width = 41 Height = 13 Caption = 'Protokoll' end object Label3: TLabel Left = 8 Top = 160 Width = 111 Height = 13 Anchors = [akLeft, akBottom] Caption = 'Nachricht zum Senden:' end object ServerAdress: TEdit Left = 8 Top = 24 Width = 121 Height = 21 Anchors = [akLeft, akTop, akRight] TabOrder = 0 Text = 'localhost' end object BtnSend: TButton Left = 136 Top = 174 Width = 75 Height = 25 Anchors = [akRight, akBottom] Caption = '&Senden' TabOrder = 1 OnClick = BtnSendClick end object Log: TMemo Left = 8 Top = 64 Width = 201 Height = 89 Anchors = [akLeft, akTop, akRight, akBottom] TabOrder = 2 end object Online: TCheckBox Left = 136 Top = 26 Width = 73 Height = 17 Anchors = [akTop, akRight] Caption = '&Online' TabOrder = 3 OnClick = OnlineClick end object Input: TEdit Left = 8 Top = 176 Width = 121 Height = 21 Anchors = [akLeft, akRight, akBottom] TabOrder = 4 Text = 'Hallo' end object ClientSocket1: TClientSocket Active = False ClientType = ctNonBlocking Port = 0 OnConnect = ClientSocket1Connect OnDisconnect = ClientSocket1Disconnect OnRead = ClientSocket1Read OnError = ClientSocket1Error Left = 96 Top = 96 end end |
Hier der Quelltext, ebenfalls einfach den Standard-Code in der IDE komplett ersetzen:
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: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105:
| unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, ScktComp, StdCtrls;
const MY_PORT = 12345; type TForm1 = class(TForm) ServerAdress: TEdit; Online: TCheckBox; Log: TMemo; Input: TEdit; BtnSend: TButton; ClientSocket1: TClientSocket; Label1: TLabel; Label2: TLabel; Label3: TLabel; procedure FormCreate(Sender: TObject); procedure OnlineClick(Sender: TObject); procedure ClientSocket1Connect(Sender: TObject; Socket: TCustomWinSocket); procedure ClientSocket1Disconnect(Sender: TObject; Socket: TCustomWinSocket); procedure ClientSocket1Error(Sender: TObject; Socket: TCustomWinSocket; ErrorEvent: TErrorEvent; var ErrorCode: Integer); procedure BtnSendClick(Sender: TObject); procedure ClientSocket1Read(Sender: TObject; Socket: TCustomWinSocket); procedure FormDestroy(Sender: TObject); private public end;
var Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject); begin ClientSocket1.Port := MY_PORT; end;
procedure TForm1.OnlineClick(Sender: TObject); begin if (Online.Checked) then ClientSocket1.Host := ServerAdress.Text; ClientSocket1.Active := Online.Checked; end;
procedure TForm1.ClientSocket1Connect(Sender: TObject; Socket: TCustomWinSocket); begin Log.Lines.Add('Verbunden mit '+ServerAdress.Text); end;
procedure TForm1.ClientSocket1Disconnect(Sender: TObject; Socket: TCustomWinSocket); begin Log.Lines.Add('Verbindung getrennt.'); Online.Checked := FALSE; end;
procedure TForm1.ClientSocket1Error(Sender: TObject; Socket: TCustomWinSocket; ErrorEvent: TErrorEvent; var ErrorCode: Integer); begin Log.Lines.Add('Fehler '+IntToStr(ErrorCode)); Online.Checked := FALSE; ErrorCode := 0; end;
procedure TForm1.BtnSendClick(Sender: TObject); begin if (ClientSocket1.Active) then ClientSocket1.Socket.SendText(Input.Text) else Log.Lines.Add('Nicht verbunden!'); end;
procedure TForm1.ClientSocket1Read(Sender: TObject; Socket: TCustomWinSocket); begin Log.Lines.Add(Socket.ReceiveText); end;
procedure TForm1.FormDestroy(Sender: TObject); begin if (ClientSocket1.Active) then ClientSocket1.Close; end;
end. |
Erläuterungen zum Code (die konstante Portdefinition kennen wir jetzt schon):
Beim Programmstart tragen wir zunächst nur die Portnummer ein. Die Adresse des Servers brauchen wir erst dann, wenn wir eine Verbindung herstellen wollen. Dazu ist die CheckBox "Online" da, deren OnClick-Ereignis entsprechende Aktionen durchführt:
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9:
| procedure TForm1.OnlineClick(Sender: TObject);
begin if (Online.Checked) then ClientSocket1.Host := ServerAdress.Text; ClientSocket1.Active := Online.Checked; end; |
Wenn wir von offline->online wechseln (das fragt die if-Bedingung ab), tragen wir vorher noch die Adresse des Servers ein. Dann weisen wir einfach den Zustand der CheckBox an die
.Active-Eigenschaft des ClientSockets zu, so dass entweder eine Verbindung aufgebaut (Haken gesetzt) oder die Verbindung getrennt wird (Haken entfernt).
Wir gehen in unserem Beispiel mal davon aus, dass das Projekt nur auf dem lokalen PC getestet wird. Die eigene IP-Adresse ist immer "127.0.0.1" oder "localhost". Diese (virtuelle) Netzwerkschnittstelle ist auf jedem PC vorhanden und bezeichnet einfach "sich selbst". Deshalb ist "localhost" bereits voreingestellt, damit auch Benutzer ohne LAN das Tutorial auf dem eigenen PC nachvollziehen können. Das Projekt läuft selbstverständlich im LAN (dann die entsprechenden lokalen IP-Adressen verwenden) und ist durchaus auch über das Internet nutzbar. Allerdings braucht man hier dann die
öffentliche IP-Adresse des PCs, auf dem das Server-Programm läuft. Diese lässt sich z.B. leicht in einem Browser über die Seite
www.whatismyip.com abfragen. Vorsicht Falle: Wenn ein (DSL-)Router im Einsatz ist, dann ist noch ein
Portforwarding (heißt auch manchmal "virtual Server") notwendig, sonst können sich Clients aus dem Internet nicht zum Server verbinden. Genaueres kann ich hier nicht allgemeingültig beschreiben, bitte gegebenenfalls im Forum nachfragen bzw. die Suchfunktion benutzen.
Nochmal Falle: Wird ein Client auf dem selben PC wie der Server gestartet, bleibt es für diesen Client weiterhin bei "localhost" als Serveradresse (ist ja auch auf dem selben PC)! Nur Clients, die von ausserhalb des LANs eine Verbindung aufbauen wollen, brauchen die öffentliche IP des Routers.
Wenn eine Verbindung aufgebaut werden konnte, tritt das Ereignis OnConnect ein, in dem wir die Verbindung melden.
Wird die Verbindung getrennt, tritt das Ereignis OnDisconnect ein. Auch hier melden wir Entsprechendes, allerdings setzen wir noch zusätzlich die CheckBox zurück, da - falls der Server die Verbindung getrennt hat - der Haken sonst stehen bleiben würde.
Konnte keine Verbindung zum Server hergestellt werden oder tritt in einer Verbindung ein Fehler auf, wird das Ereignis OnError ausgelöst. Wir protokollieren einfach den Fehler mit Nummer, trennen sicherheitshalber die Verbindung (falls das nicht schon geschehen sein sollte -> Haken-Problem) und setzen die Fehlervariable zurück, damit keine Exception ausgelöst wird.
Kommen wir jetzt aber endlich zu den interessanten Prozeduren: Wenn wir einen Text eingegeben haben und diesen nun senden wollen, müssen wir im Button-Ereignishandler nur noch den Text senden:
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8:
| procedure TForm1.BtnSendClick(Sender: TObject); begin if (ClientSocket1.Active) then ClientSocket1.Socket.SendText(Input.Text) else Log.Lines.Add('Nicht verbunden!'); end; |
Aber nur dann, wenn auch eine Verbindung besteht (-> if-Abfrage). Haben wir keine Verbindung, gibts nur eine Fehlermeldung, die wir statt dessen ins Protokoll schreiben.
Wenn wir die Textnachricht gesendet haben, erscheint allerdings noch nichts in unserem Protokoll. Das passiert erst dann, wenn der Server die Daten verarbeitet und uns eine entsprechende Nachricht zurückgesendet hat. In diesem Fall tritt das Ereignis OnRead ein, in dem wir die Daten vom Server lesen und ins Protokoll schreiben:
Delphi-Quelltext
1: 2: 3: 4: 5:
| procedure TForm1.ClientSocket1Read(Sender: TObject; Socket: TCustomWinSocket); begin Log.Lines.Add(Socket.ReceiveText); end; |
Kurz zusammengefasst: Text eintippen -> wird an den Server gesendet -> dieser verteilt die Nachricht an alle Clients -> Nachricht vom Server trifft ein -> ins Protokoll schreiben.
Dieses Konzept nennt man auch
Client-Server-Architektur.
Beim Programmende wird natürlich noch eine eventuell bestehende Verbindung getrennt, wir beenden unsere Programme schließlich sauber.
Wer mit diesem Tutorial erfolgreich den Einstieg ins "Netzwerken" geschafft hat und jetzt schon gespannt auf MEHR ist, sollte sich mal das
Terminatorzeichen-Protokoll-Tutorial ansehen. Da werden sie geholfen.
cu
Narses
Hinweis zu Delphi-Versionen >=D2009 (Unicode-Problem):
Der Code ist Unicode-Save, es sollte keine Probleme mit Delphi-Versionen >=D2009 geben.
Hinweis zu den Anhängen:
In der Personal-Edition von Delphi stehen die Socket-Komponenten leider nicht in der IDE zur Verfügung (
dclsockets70.bpl wird nicht mitgeliefert). Allerdings ist die
ScktComp.dcu ja auch bei der Personal-Edition vorhanden, so dass trotzdem (ganz legal!) mit den Socket-Komponenten gearbeitet werden kann, wenn man diese dynamisch erzeugt.
Da die Frage aufgekommen ist, wie man das denn konkret macht, gibt es zur Demonstration der Vorgehensweise die beiden Programme jetzt auch in einer Version, in der der
TClientSocket und der
TServerSocket dynamisch erzeugt werden.
There are 10 types of people - those who understand binary and those who don´t.