====== Client-Server mit Threads ====== Während der Programmierung eines **SocketServers** bin ich zu folgenden Schlüssen gekommen: ===== MultiThread SocketServer ===== Bei **MultiThread** SocketServern (und insbesondere bei mehreren gebundenen Ports) den Ablauf auf 4 statt 3 Phasen unterteilen: * **Init** (Requests werden gar nicht angenommen) * **Running** (Requests werden normal beantwortet) * - **FadeOut** (neue Requests werden mit einer "soft"-Fehlermeldung beantwortet) * **Done** (Requests werden gar nicht mehr angenommen, Sockets geschlossen) Die Unterphase FadeOut dauert solange, bis alle Threads komplett abgearbeitet bzw. nach dem TimeOut "gecancelt" sind. ===== Buffers ===== **Buffers** für Request, Response und Steuervariablen als eine ganze Struktur definieren um keine mehreren Speicherfunktionen (etwa malloc(), free(), etc.) behandeln zu müssen. Falls es mehrere Arten von Requests behandelt werden müssen, alle Einzelstrukturen in eine Union zusammenfassen. Somit ist jedesmal nur ein einziger Aufruf zur "Allocierung" und Freigabe vom Speicher nötig. **Hinweis 1:** Bei einem MultiThread-Betrieb diese Struktur nicht als lokale Variablen einer Funktion definieren! Es sei denn, die Struktur ist ganz klein und somit vernachlässigbar. **Grund:** Lokale Variablen werden im Stack zur Laufzeit abgelegt. Im MultiThread-Betrieb kann es zum Stacküberlauf führen, falls die Anzahl der Threads groß werden sollte. Man sollte die obere Grenze der Anzahl der Threads möglichst im voraus festlegen. Es wird bei 32-Bit CPUs im Stack standardmäßig 1MB Speicher pro Thread reserviert (bei 64-Bit CPUs - 2MB). **Hinweis 2:** Innerhalb der Struktur sollen mehrere Statusfelder definiert werden, die den aktuellen Status der Nachricht widerspiegeln und zwar aus mehreren Sichten: technische (separat für Hard- und Soft-Fehlern) und inhaltliche. **Hard-Fehler** (z.B. statusHFC genannt) sind Fehler, wenn die Kommunikation nicht einwandfrei funktioniert (z.B. Requests ohne Daten, TimeOuts beim Empfang, etc.). **Soft-Fehler** (z.B. statusSFC genannt) sind Fehler, wenn die Kommunikation auf der physischen Ebene einwandfrei funktioniert, die Daten aber nicht zusammen passen, im falschen Format vorliegen, korrupt sind. Mit anderen Worten, die Daten sind zwar vorhanden, können aber nicht ausgewertet bzw. verarbeitet werden. **Inhaltliche** Statusinformationen stellen eine kurze Zusammenfassung des Datenzustandes bzw. des Ergebnisses dar. ===== Pool ===== Alle Threads zu einem **Pool** zusammenfassen (mit zu "pthread_t" zusätzlichen Attributen wie "active", "TimeStamp_Start", "TimeOut") und einige Funktionen zur Pool-Verwaltung definieren. Das wären etwa "getNextFreeThread()", "getActiveThreadsCount()", "setThreadAsActive()", "setThreadAsInactive()", "cleanInactiveThreads()", "cancelTimeoutedThreads()" und "killAllThreads()".\\ Der Funktion "setThreadAsActive()" als Parameter TimeOut übergeben. Die "getNextFreeThread()" soll am Anfang (bei Bedarf) die "cleanInactiveThreads()" aufrufen um erst die inaktiven frei zu geben (sonst stehen sie nicht zur Verfügung). Die "cleanInactiveThreads()" soll am Anfang (bei Bedarf) die "cancelTimeoutedThreads()" aufrufen um erst die inaktiven zu ermitteln. **Hinweis 3:** Die Pool-Verwaltung selbst soll aber nicht in einem Thread laufen, sondern im Mutterprozeß. Auch deren (Pool-Verwaltungs) Variablen dürfen nicht durch die Schreibzugriffe aus den Threads beeinflußt werden. Die Variablen selbst sollen als **volatile** deklariert werden. **Grund:** Die meisten Prozeduren sind nicht atomar, was fatale Folgen haben kann, wenn sie aus den Threads aufgerufen werden. Die Pool-Verwaltung selbst als Thread würde Gefahr eingehen, zu Opfer einer DeadLock-Falle unter den Threads zu fallen. Um die Effizienz der Speicherverwaltung eines SocketServers bedeutend zu erhöhen, ist es ratsam statt für jede einzelne Request Speicher zu reservieren, ein **Pool** für N Requests (soll dynamisch bzw. durch Parameter änderbar sein) zu reservieren (z.B. mit calloc() mit N+1 Elementen, wobei das Element 0 die Steuerstruktur der eigenen Speicherverwaltung selbst ist) und es selbst zu verwalten. **Grund:** malloc() für jeden Requeset separat "kostet" viel Ressourcen. Wenn die Strukturen für alle Requests/Response gleich groß definiert werden (s. oben), lassen sie sich in einem Array zusammenfassen und leicht verwalten. Overhead dabei ist minimal: Block ist belegt / Block ist frei. ===== Senden ===== Beim **Senden** beachten, daß bei einem Sendevorgang nicht unbedingt alle Daten auf einmal gesendet werden müssen/können. Daher ist es ratsam, den Sendevorgang in eine Schleife einzuschließen: int rc; int fdSocketOfPeer; // Socket (Filedescriptor) zur Gegenstelle int bytesZumSenden; int bytesGesendet; char * sendeBuffer; // Daten zum Wegschreiben bytesGesendet = 0; // noch keine Bytes gesendet rc = 0; while ((bytesGesendet < bytesZumSenden) && !(rc == -1)) // solange noch nicht alle Zeichen gesendet { rc = send(fdSocketOfPeer, sendeBuffer + bytesGesendet, bytesZumSenden - bytesGesendet, 0); if (rc > 0) bytesGesendet += rc; } // while An dieser Stelle ist ein **Versuchszähler** oder eine Prüfung auf **TimeOut** natürlich ratsam. ===== Lesen ===== Das gilt auch fürs **Lesen** - nicht unbedingt alle Daten auf einmal gelesen werden müssen/können (es können noch welche unterwegs sein, die zum selben Request gehören). Das **TimeOut** beim Lesen kann man beispielsweise so prüfen: int fdSocketOfPeer; // Socket (Filedescriptor) zur Gegenstelle int timeOutSocket; time_t timeStamp1, timeStamp2; double vorgangsDauer; readCount = 0; // es wurde noch nichts gelesen vorgangsDauer = 0; time(&timeStamp1); // TimeStamp davor ermitteln while ((readCount == 0) && (vorgangsDauer < timeOutSocket)) // nur bis zum TimeOut { readCount = recv(fdSocketOfPeer, tmpSocketBuffer, sizeof(tmpSocketBuffer), 0); // aus dem Socket lesen time(&timeStamp2); // TimeStamp danach ermitteln vorgangsDauer = difftime(timeStamp2, timeStamp1); // Dauer des Vorgangs } // while An dieser Stelle ist ein kurzes **sleep()** für den Fall, daß nicht alle Daten auf einmal gelesen worden sind, natürlich ratsam*. ** *Vorsicht:** Bei sleep() unbedingt darauf zu achten bzw. zu klären, ob nicht der gesamte Prozeß samt aller Threads in den Schlaf versetzt wird. Bei einem sleep(), welches auf die Threads wartet, wäre es sinnlos. **Hinweis 4:** Er ist ratsam, im Hauptprozeß eine allgemeine idle()-Funktion zu definieren, die bei erforderlichen kurzen Wartezeiten aufgerufen wird. Die Funktion selbst besitzt keine Funktionalität, außer sie soll weitere Funktionen aufrufen, die diverse Aufräum-Aktivitäten ausführen, insbesondere im Bezug auf Threads-Verwaltung (Suchen nach beendeten Threads, Beenden von Threads, deren TeimOut abgelaufen ist, Freigeben von Resssourcen, rtc.).\\ Diese Funktion könnte auch regelmäßig in einem extra dafür erzeugten Thread (quasi GarbageCollector) aufgerufen werden. Aber **Vorsicht** (sehe den Hinweis 3)! **Hinweis 5:** Er ist ratsam, bei den Funktionen, deren Laufzeit nicht vorhersehbar bzw. durch externe Ereignisse/Zustände beeinflußbar ist, das TimeOut und/oder den maximalen Versuchszähler als Parameter mit zu übergeben. Beispiel: int initSocket (int port, int backlogs, int * fdSocketOnServer, int maxTimeOut, int maxAttempt, int delayAttempt) wobei: Eingabeparameter:\\ "port" - die zu bindende Portnummer "backlogs" - Anzahl Requests, die gepuffert werden sollen, falls der Socket nicht leer ist "maxTimeOut" - maximale Dauer bis der Listener initialisiert worden sein soll (0 = unendlich) "maxAttempt" - maximale Versuchszahl den Port zu binden (0 = unendlich) "delayAttempt" - Verzögerung zwischen den Versuchen den Port zu binden (0 = keine Verzögerung) Ausgabeparameter:\\ "fdSocketOnServer" - FileDescriptor des geöffneten (Master-)Sockets (-1 = Fehler) **Hinweis 6:** Beim Kompilieren unbedingt Direktiven und Compiler-Parameter für den Multithreadfähigen Code beachten. Stichwort **REENTRANT**, **Reenterable**. **Hinweis 7:** Bei der Verwendung von Copy-Funktionen zwischen den Buffern ist Vorsicht wegen den Sonderzeichen geboten. Die sicherere Funktion ist **memcpy()** und nicht die strncpy(), da die letztere bei z.B. einem 0-Byte abbrechen würde. Beim Ermitteln der Größe des ausgelesenen Strings ist auf strlen() kein Verlaß (aus dem selben Grund). **Weitere Infos:** man poll ===== Vor-/Nachteile: ===== **Vorteil:** Da die Requests parallel abgearbeitet werden, können keine Staus entstehen.\\ **Nachteil:** Die Syncronisation (Semaphore), aber nur falls erforderlich, ist kompliziert. Gefahr von DeadLocks oder durch Zombie-Semaphore (von abgebrochenen Threads/Prozessen). ==== MultiThreads: ==== **Vorteil:** Threads können über gemeinsame Variablen miteinander kommunizieren. Ressourceschonender, da alles unter einer einzigen PID läuft.\\ **Nachteil:** Threads können durch Nutzung von gemeinsamen Variablen in die Quere kommen. Der Hauptprozeß muß sich über die Thread-Verwaltung selber kümmern. Einzelne Threads kann man nicht (direkt) von außen steuern. ==== MultiProzeß: ==== **Vorteil:** Saubere Trennung unter den SohnProzessen. Die Prozeß-Verwaltung übernimmt das Betriebssystem.\\ **Nachteil:** Interprozeßkommunikation ist umständlicher (z.B. per Shared Memory). ===== Weitere Schritte (ToDo): ===== Eine allgemeine SocketServer Vorlage erstellen. Vorhaben: * Mehrere Ports binden. * Mehrere Threads oder Sohn-Prozesse. * Kein Warten durch accept(), sondern poll() oder select() verwenden. Dabei zu beachten, daß select() zwar bequemer ist, kann aber nur eine begrenzte Zahl (1024 :?:) von Sockets verkraften. Es liegt an der Steuerstruktur, die vom select() benutzt wird. * Unterstützung von IPv4 und IPv6. ===== Links zum Thema: ===== * **Netzwerkprogrammierung mit Sockets** (von Ulrich Vogel) - einfach: [[http://www.uvomatik.de/programmierung/sockdoc/]] * **Client-Server Socketprogrammierung** (von Arnold Willemer) - wertvoll: [[http://www.willemer.de/informatik/unix/unprsock.htm]] * **Poll: Lesen von mehreren Filedeskriptoren** (von Prof. Schweiggert und Dr. J.Mayer mit H.Braxmeier und C.Ehrhardt):\\ [[http://www.mathematik.uni-ulm.de/sai/ss04/soft/uebungen/blatt06/html/blatt06.ps]] * **Parallele Programmierung mit Threads** (Jürgen Wolf: Linux/Unix-Systemprogrammierung):\\ [[http://pronix.linuxdelta.de/C/Linuxprogrammierung/Linuxsystemprogrammieren_C_Kurs_Kapitel8.shtml]] * **Die grundlegenden Funktionen zur Thread–Programmierung** (Jürgen Wolf: Linux-UNIX-Programmierung, Das umfassende Handbuch – 2., aktualisierte und erweiterte Auflage 2006):\\ [[http://openbook.galileocomputing.de/linux_unix_programmierung/Kap10-004.htm]] * **Mit den POSIX-Threads programmieren** (Jürgen Wolf: C von A bis Z, Das umfassende Handbuch - 3., aktualisierte und erweiterte Auflage):\\ [[http://openbook.galileocomputing.de/c_von_a_bis_z/026_c_paralleles_rechnen_004.htm]] ---- Stand: 20.04.2007 - **in Arbeit**\\ --- //[[feedback.jk-wiki@kreick.de|: Jürgen Kreick]]// EOF