Home » Tutorials » Netzwerk und Internet » Sockets mit WinAPI

Sockets mit WinAPI

UDP-Socket zum Empfangen (Server)

Jetzt brauchen wir noch eine Funktion, die auf Port 6000 lauscht.

Nebenbei möchte ich noch auf IANA hinweisen. Wer sich dafür interessiert welche Portnummern wann zu verwenden sind, kann es dort erfahren. Grundsätzlich ist alles möglich. Der Port ist ein 16bit-Wert, also kann man von 0 bis 65535 alle Werte benutzen (die 0 eigentlich ausgenommen) und Windows stört sich auch nicht daran. Ein Fehler gibt es nur, wenn eine Portnummer einer Verbindung bereits belegt ist (auch wieder mit Ausnahmen, aber das führt zu weit). Als Faustregel gilt: Portnummern vergeben wir oberhalb der 5000. Und von Portnummern unterhalb von 1024 lassen wir prinzipiell die Finger. Und den Bereich dazwischen nutzt Windows.
Zum Beispiel in Kapitel 5 haben wir eigentlich keine Portnummer vergeben. Die 6000 war ja unser RemotePort. Auch wenn er (durch die Adresse) am selben Rechner war, es war ja nicht zwingend unser Programm. Hätte aber auch sein können. Es war aber definitiv nicht unser Socket. Unser Socket in Kapitel 5 hatte auch eine IP-Adresse und eine Portnummer, quasi unsere Absenderadresse. Diese hätten wir vorgeben können. Haben wir aber nicht, und dadurch bekamen wir sie von Windows bei sendto

Socket binden

Wenn wir aber auf UDP-Pakete warten wollen, wie in diesem Kapitel beabsichtigt, dann sollten wir besser die Portnummer selber festlegen. Anders ausgedrückt, wir müssen unser Socket an eine Adresse binden. Das ist praktisch, damit man auch vorher weis, an welche Adresse man die Pakete später schicken soll. Dazu gibt es die Funktion Bind. Und genauso wie bei SendTo übergeben wir den Adressrecord. Diesmal nehmen wir allerdings nicht die Zieladresse, sondern unsere lokale Adresse (unsere gewünschte Absenderadresse). Da wir allerdings noch nichts senden, gibt es bei Bind auch keinen Buffer. Was nehmen wir nun für eine Adresse?
Die Adressfamilie ist nach wie vor vorgegeben (AF_INET).
Den Port legen wir einfach fest (oder wir nehmen als Port 0, dann gibt uns Windows einen freien Port). Wie gesagt, es ist besser eine Nummer größer 5000 zu nehmen. Und da unser Programmteil in Kapitel 5 schon mal an Port 6000 gesendet hat, versuchen wir auch gleich mal uns an diesen Port zu binden.
Als IP-Adresse gibt es mindestens 3 Möglichkeiten (insofern eine Netzwerkkarte samt TCP/IP installiert ist). Hier können wir auswählen, welche Verbindung wir nutzen wollen. Entweder wir wollen uns nur mit unserem eigenen Rechner unterhalten, dann nehmen wir die 127.0.0.1. Oder wir wollen uns nur über eine LAN-Verbindung unterhalten, dann nehmen wir dessen IP-Adresse (bei mir ist es die 192.168.2.100). Oder wir nehmen alle Verbindungen (0.0.0.0) die auf dem Rechner eingerichtet sind. Ich habe mich jetzt für letzteres entschieden. Wir binden unser Socket also an 0.0.0.0: 6000. Dadurch werden wir alle Nachrichten bekommen, egal ob von außen über LAN oder vom eigenen Rechner.

 FSocket := createSocket_UDP;

  AddrLen := sizeof(SockAddr);

  SockAddr.sin_family := AF_INET;
  SockAddr.sin_Port := htons(6000);
  SockAddr.sin_Addr.S_Addr := inet_addr('0.0.0.0');

  if Bind( FSocket, SockAddr, AddrLen) = SOCKET_ERROR then
    HandleError;

Und wie schon gesagt, diese Funktion ist in SendTo auch mit drin. Solange der Socket nicht gebunden ist, wird unser Socket auch über diese Funktion an 0.0.0.0 und einen Port kleiner 5000 gebunden. Von einem bereits gebundenem Socket, versendet man genauso Daten wie in Kapitel 5 beschrieben. Wir haben eben uns nur eine Absenderadresse vorgegeben und nicht aus der Lostrommel eine gezogen. Das Socket aus Kapitel 5 und unser Socket sind jetzt gleich eingestellt.. Aus diesem Grund (und weil man auch keine Verbindung hat) können wir ein UDP-Socket nicht in Client und Server unterscheiden. Ich habe die Begriffe deswegen in der Kapitelüberschrift in Klammern gesetzt. Man kann mit einem UDP-Socket halt immer senden und empfangen auf gleiche Art und Weise und aus allen Richtungen.

Auf Datenpaket warten

Kommen wir nun zum Empfangen. Egal ob wir nun beim ersten Aufruf von SendTo oder über Bind unserem Socket eine Adresse zugewiesen haben. Jetzt können wir Daten an unser Socket per UDP senden. Wir müssen dem anderen Socket nur verraten, wie unsere Portnummer ist und welche IP-Adresse unser Rechner hat. Die Portnummer kennen wir, aber die IP-Adresse nicht (außer von und zum eigenen Rechner: 127.0.0.1). Wie wir die erfahren (und eine uns von SendTo zugeloste Portnummer) lesen wir noch in Kapitel 9. Jetzt setzen wir dies als gegeben (bzw. nehmen eben 127.0.0.1).
Wenn ein Datenpaket an unserem Socket ankommt, landet dies erst einmal im Buffer. Den können wir bei Gelegenheit auch einmal auslesen. Dies geht mit den Befehlen recv und recvfrom. Wobei für eine UDP-Socket recvfrom besser ist, da wir hierüber noch den Absender erfahren (recv ist eher für TCP gedacht, da man da den Absender sowieso kennt). Recvfrom wird genauso aufgerufen wie SendTo:

RecvFrom(FSocket: TSocket;
         var buf;
         buflen: integer;
         Flags: integer;
         var SockAddr: TSockAddrIn;
         var AddrLen: integer);

Dabei ist FSocket unser Socket-Handle, buf ein Speicherplatz, wo die Daten hineinkommen (z.B. ein statisches Array), buflen ist die Größe des Buffers, Das Flag ist wieder auf 0 gesetzt, und in SockAddr landet natürlich nicht die Zieladresse (wir senden ja nicht) sondern die Absenderadresse des Datenpaketes. AddrLen ist nach wie vor sizeof(SockAddr). Und auch bei dieser Funktion können wir den Rückgabewert auf SOCKET_ERROR testen.
In einer vollständigen Funktion könnte dies so aussehen:

function recieve_UDP(FSocket: TSocket; out IP: String; out Port: Word): String;
var AddrLen: Integer;
    SockAddr: TSockAddrIn;
    Buf: array[0..127] of char;
begin
  Addrlen := SizeOf(SockAddr);

  FillChar(buf, length(buf), 0); //Besser ist, falls keine terminierende
                                 //0 mitgeliefert wird

  if Recvfrom(FSocket, buf, length(buf), 0, SockAddr, AddrLen) = SOCKET_ERROR then
    HandleError;
  result := buf;

  IP := inet_ntoa(SocketAddr.sin_addr);
  Port := ntohs(SocketAddr.sin_port);
end;

Der Rückgabewert der Funktion recieve_UDP ist der empfangene Buffer (hier als String interpretiert). Auch RecvFrom gibt als Rückgabewert nicht nur SOCKET_ERROR, sondern auch die Anzahl der empfangenen Bytes. Die sollten wir gerade bei nullterminierten Strings wie hier auswerten. Ich spar mir das jetzt mal wieder. Stattdessen gehe ich besser noch mal auf zwei weitere Funktionen ein: inet_ntoa und ntohs. Wie sicherlich schon jeder festgestellt hat, sind dies die „Umkehrfunktionen“ zu inet_addr und htons. Und das ist es auch und mehr ist dazu nicht zu sagen.
Wenn wir diese Funktion ausprobieren, werden wir ein großes Problem feststellen (je nach Konzept des Programms muss es sich nicht zwingend als Problem darstellen). Das Problem besteht darin, das jedes Socket nach dem initialisieren „blockierend“ ist. Und genau das macht unser Programm auch. Wenn wir recvfrom (oder auch recv) aufrufen und es liegen keine Daten im Socketcontainer, dann bleibt unser Programm an dieser Stelle stehen und macht nix mehr. Im Taskmanager steht dann so etwas wie „Keine Rückmeldung“. Erst wenn Daten kommen, erwacht unser Programm zu neuem Leben, bis es wieder an dieselbe Stelle kommt und Recvfrom erneut aufruft.

Socket Messages

Gegen das Blockieren des Sockets können wir verschiedene Sachen unternehmen:

  1. Die blockierenden Funktionen in einen eigenen Thread stecken
  2. Das Socket auf „Nicht-blockierend“ schalten
  3. Das Socket zuerst fragen, ob etwas anliegt.
  4. Das Socket uns benachrichtigen lassen, wenn es etwas zu tun gibt

Abgesehen, dass es natürlich auch ins Programmkonzept passen könnte, wenn das Socket blockiert, kann es eben auch in einem separaten Thread von Nutzen sein. Dann ist natürlich Variante 1 zu wählen.
Variante 2 geht über den Befehl ioctlsocket. Der geneigte Leser kann sich den Befehl selber zu Gemüte führen. Nur so viel: Man schaltet einfach den Socket auf „nicht-blockierend“ und wenn beim Aufruf von (zum Beispiel) recvfrom keine Daten im Socket liegen wird ein SOCKET_ERROR mit WSAEWOULDBLOCK ausgelöst. Bei dieser Variante müsste man dann pollen. Das ist aber nicht Windows-typisch.
Genauso wenig Windows-typisch ist Variante 3. Das Socket ist im blockierenden Zustand und wir können mit dem Befehl select den Zustand abfragen. Also in unserem Fall, ob neue Daten angekommen sind. Das führt auf genau denselben Polling-Modus (kann aber günstig sein, wenn jemand viele Sockets verwaltet)
Windows-typisch ist Variante 4. Wir setzen ein Ereignis und/oder schicken Messages. Ich stell hier mal die Messages vor. Durch einen einzigen Befehl können wir dem Socket sagen, dass es bei einem bestimmten Ereignis an ein von uns definiertes Window eine Nachricht schicken soll. Benachrichtigt werden können wir bei folgenden Ereignissen:

  • Das Socket ist bereit um Daten zu senden (FD_WRITE)
  • Am Socket sind Daten von außen angekommen (FD_READ)

Die Konstanten in den Klammern brauchen wir nachher noch. Für verbindungsorientierte Sockettypen (Stream-Socket für TCP) gibt es dann noch weitere Ereignisse:

  • Ein neuer Client will sich am Server-Socket verbinden (FD_ACCEPT)
  • Das andere Socket hat die Verbindung beendet (FD_CLOSE)

Die beiden Ereignisse benötigen wir in den nächsten Kapiteln noch. Für UDP sind sie irrelevant, da keine Verbindungen aufgebaut werden können. Es gibt neben den 4 Ereignissen noch ein paar mehr, auf die ich jetzt nicht eingehe.
Jetzt können wir unserem Socket mal erzählen, was wir eigentlich wollen. Und das geht so:

WSAAsyncSelect(FSocket: TSocket;
               WindowHandle: HWND;
               WM_mySocket: Integer;
               EventMask: Integer);

FSocket, ist wieder unser Sockethandle. Das Windowhandle ist das Handle von dem Fenster (z.B. einem Formular) an das wir die Nachricht schicken wollen, WM_mySocket ist irgendeine Konstante, die wir noch wählen können/müssen. Sie definiert den Nachrichtenidentifier. Und zum Schluss kommen noch die Ereignisse als Maske OR-verknüpft, bei denen wir benachrichtigt werden wollen (z.B.: „FD_READ or FD_WRITE“).
Für einen Beispielcode bräuchten wir jetzt also ein Fenster. Und auch, wenn wir die Sockets ausschließlich mit der WinAPI programmieren, würde ich jetzt der Einfachheit halber (und weil ich eingangs versprochen habe, dass keine weiteren WinAPI-Kenntnisse nötig sind) auf ein VCL-Formular zurückgreifen und unser UDP-Empfangsprogramm in ein Formular betten.
Wir nehmen also ein Formular und setzen ein Memo drauf, um die ankommenden Texte anzuzeigen. Dazu schreiben wir in das FormCreate-Ereignis das Einrichten des Sockets und schreiben noch eine Methode welche, die Messages empfängt.

uses WinSock,Messages, Forms, ...;

const WM_mySocket = WM_APP+1;

type TForm1 = class(TForm)
    ...
    procedure FormCreate(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
  private
    FSocket: TSocket;
    Procedure SocketMessage(var msg: TMessage); message WM_mysocket;
    //zum Empfangen der Messages
    ...
end;

...
 
procedure TForm1.FormCreate(Sender: TObject);
var SockAddr: TSockAddrIn;
  AddrLen: Integer;
begin
  FSocket := socket(AF_Inet, SOCK_DGRAM, IPPROTO_UDP);
  if FSocket=invalid_socket then HandleError;

  AddrLen := SizeOf(SockAddr);
  SockAddr.sin_family := AF_Inet;
  SockAddr.sin_port := htons(6000);
  SockAddr.sin_addr.S_addr := inet_addr('0.0.0.0');
  if bind(FSocket, SockAddr, AddrLen) = Socket_Error then 
    HandleError;

  if WSAAsyncSelect(FSocket, self.Handle, WM_mySocket,
    FD_READ or FD_WRITE) = SOCKET_ERROR then 
    HandleError;
end;

procedure TForm1.SocketMessage(var msg: TMessage);
var buf: array[0..127] of char;
  SockAddr: TSockAddrIn;
  AddrLen: Integer;
begin
  case msg.LParamLo of
    FD_READ: begin
        Memo1.Lines.Add('FD_READ');
        FillChar(buf, length(buf), 0);
        AddrLen := SizeOf(SockAddr);
        if recvFrom(msg.wparam,  // = Socket-Handle
          Buf,
          Length(buf),
          0,
          SockAddr,
          AddrLen) = SOCKET_ERROR then 
          HandleError;
                  
        Memo1.Lines.Add(inet_ntoa(SockAddr.sin_addr)+':'+
          IntToStr(ntohs(SockAddr.sin_port)));
        Memo1.Lines.Add(Buf);
      end;

    FD_WRITE: Memo1.Lines.Add('FD_WRITE');
  end;
end;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  CloseSocket(FSocket);
end;

Das war’s dann auch schon. Nicht zu vergessen ist WSAStartUp und WSACleanup in diesem Programm und die HandleError-Funktion lassen wir mal identisch zum Programm in Kapitel 5.
Klären wir noch was in den beiden Parametern von msg.LParam und msg.WParam steht. In LoWord vom LParam ist wie wir im Programm schon erkennen können das Event welches aufgetreten ist. Im HiWord von LParam liegt ein möglicher Fehlercode (0 bedeutet: kein Fehler). Und schließlich in WParam liegt das Sockethandle, welches die Nachricht geschickt hat. Wir können also von verschiedenen Sockets mit demselben Messageidentifier senden.
Und was machen wir eigentlich mit dem FD_WRITE? Das FD_WRITE sagt uns nur, dass das Socket bereit zum Senden ist. Wir hätten bevor wir sendto aufrufen auch erstmal auf FD_WRITE warten können. War aber im obigen Fall (Kapitel 5) nicht notwendig.
Dieses zweite UDP-Socket Programm hängt wieder als Beispiel dran.

5 Gedanken zu „Sockets mit WinAPI“

  1. Wo findet sich denn der Link, um die Programmbeispiele herunterzuladen?
    Ich suche jetzt schon eine Weile, finde aber nichts.

  2. Eine weitere – aus meiner Sicht sehr wichtige – Antwort auf die Frage, warum man die Sockets selbst über die Windows API programmieren sollte, wäre noch:

    Ich hatte vor ca. 10 Jahren einen „Chat“ über die Komponenten von Delphi programmiert. Wir haben hier in der Firma einen Linux-Server zu stehen. Da die Leute das lustig fanden, ist der Chat heute fester Bestandteil. Linux bedeutet aber insoweit Probleme, weil ich das Programm emulieren lassen muss. Dafür würde sich theoretisch wine anbieten.
    Der Haken ist aber, dass die Komponenten alle fest in der VCL verankert sind und man mit wine nicht weit kommt. Man braucht also eine virtuelle Maschine mit Windows, um den Server laufen zu lassen. Insoweit erhoffe ich mir, dieses Problem eventuell lösen zu können. Wenn es mir gelingt, mit dieser Anleitung eine eigene Klasse ganz ohne jede VCL erstellen zu können, dann kann ich den Server als Konsolen-Programm erstellen und (endlich) ohne den ganzen Overhead unter wine laufen lassen.

    Falls den Seitenadmin dieser Kommentar stört, kann er ihn gern wieder gelöscht werden. Aber auf der Suche nach Sockets ohne VCL, wäre dies als Fund bei Suchmaschinen schon vor langer Zeit sehr hilfreich für mich gewesen. Suchmaschinen finden aber immer nur den Text, der auf den Seiten tatsächlich irgendwo steht und wenn man nach „nonvcl“ im Zusammenhang mit Sockets unter Delphi sucht, sieht es dünn aus mit den Suchergebnissen.

  3. Hallo, ich kann leider auch deine Programmbeispiele als Datei nicht finden. Sind die nicht mehr verlinkt?

    Ansonsten gutes Tutorial 🙂

Kommentare sind geschlossen.