Das Linux-API-Buch. Umfassender Leitfaden»


Das Linux-API-Buch. Umfassender Leitfaden»

Guten Tag! Ich mache Sie auf das Buch „Linux API. Umfassender Leitfaden“ (Übersetzung des Buches Die Linux-Programmierschnittstelle). Es kann auf der Website des Herausgebers bestellt werden und wenn Sie den Promo-Code anwenden LinuxAPI Sie erhalten 30 % Rabatt.

Ein Auszug aus dem Buch zur Rezension:

Sockets: Serverarchitektur

In diesem Kapitel besprechen wir die Grundlagen des Entwurfs iterativer und paralleler Server sowie einen speziellen inetd-Daemon, der die Erstellung serverseitiger Internetanwendungen erleichtert.

Iteration und parallele Server

Es gibt zwei gängige Socket-basierte Netzwerkserverarchitekturen:

  • iterativ: Der Server bedient die Clients einzeln, indem er zunächst eine Anfrage (oder mehrere Anfragen) von einem Client verarbeitet und dann zum nächsten übergeht.

  • parallel: Der Server ist darauf ausgelegt, mehrere Clients gleichzeitig zu bedienen.

Abschnitt 44.8 hat bereits ein Beispiel für einen Iterationsserver basierend auf FIFO-Warteschlangen bereitgestellt.

Iterationsserver eignen sich normalerweise nur in Situationen, in denen Clientanfragen relativ schnell verarbeitet werden können, da jeder Client warten muss, bis alle anderen Clients vor ihm bedient wurden. Ein häufiger Anwendungsfall für diesen Ansatz ist der Austausch einzelner Anfragen und Antworten zwischen einem Client und einem Server.

Parallele Server eignen sich in Fällen, in denen die Verarbeitung jeder Anfrage viel Zeit in Anspruch nimmt oder der Client und der Server einen langen Nachrichtenaustausch haben. In diesem Kapitel konzentrieren wir uns hauptsächlich auf die traditionelle (und einfachste) Methode zum Entwerfen paralleler Server, die darin besteht, für jeden neuen Client einen separaten untergeordneten Prozess zu erstellen. Ein solcher Prozess übernimmt die gesamte Arbeit zur Betreuung des Kunden und endet dann. Da jeder dieser Prozesse unabhängig voneinander abläuft, ist es möglich, mehrere Kunden gleichzeitig zu bedienen. Die Hauptaufgabe des Hauptserverprozesses (übergeordneter Prozess) besteht darin, für jeden neuen Client einen separaten untergeordneten Prozess zu erstellen (optional können Sie Ausführungsthreads anstelle von Prozessen erstellen).

In den folgenden Abschnitten betrachten wir Beispiele für iterative und parallele Server, die auf Internet-Domain-Sockets basieren. Diese beiden Server implementieren eine vereinfachte Version des Echo-Dienstes (RFC 862), der eine Kopie aller vom Client an ihn gesendeten Nachrichten zurückgibt.

Echo-Iteration-UDP-Server

In diesem und dem nächsten Abschnitt stellen wir Server für den Echo-Dienst vor. Es ist auf Port Nummer 7 verfügbar und funktioniert sowohl über UDP als auch über TCP (dieser Port ist reserviert und daher muss der Echo-Server mit Administratorrechten ausgeführt werden).

Der Echo-UDP-Server liest ständig Datagramme und sendet eine Kopie davon an den Absender zurück. Da der Server jeweils nur eine Nachricht verarbeiten muss, reicht hier eine iterative Architektur aus. Die Header-Datei für Server ist in Listing 56.1-XNUMX dargestellt.

Auflistung 56.1. Header-Datei für die Programme id_echo_sv.c und id_echo_cl.c

#include "inet_sockets.h" /* Deklariert unsere Socket-Funktionen */
#include „tlpi_hdr.h“

#define SERVICE "echo" /* UDP-Dienstname */

#define BUF_SIZE 500 /* Maximale Größe von Datagrammen, die
kann von Client und Server gelesen werden */
____________________________________________________________________________ sockets/id_echo.h

Listing 56.2-XNUMX zeigt die Serverimplementierung. Folgende Punkte sind zu beachten:

  • Um den Server in den Daemon-Modus zu versetzen, verwenden wir die Funktion beenDaemon() aus Abschnitt 37.2;

  • Um das Programm kompakter zu machen, verwenden wir die in Abschnitt 55.12 entwickelte Internet-Domain-Socket-Bibliothek.

  • Wenn der Server keine Antwort an den Client zurückgeben kann, schreibt er mithilfe des syslog()-Aufrufs eine Nachricht in das Protokoll.

In einer realen Anwendung würden wir höchstwahrscheinlich eine bestimmte Grenze für die Häufigkeit der Protokollierung von Nachrichten mithilfe von syslog() festlegen. Dies würde die Möglichkeit ausschließen, dass ein Angreifer das Systemprotokoll überläuft. Bedenken Sie außerdem, dass jeder syslog()-Aufruf recht teuer ist, da standardmäßig fsync() verwendet wird.

Auflistung 56.2. Ein Iterationsserver, der den Echo-UDP-Dienst implementiert

_________________________________________________________________sockets/id_echo_sv.c
#enthalten
#include „id_echo.h“
#include „become_daemon.h“

int
main(int argc, char *argv[])
{
int sfd;
ssize_t numRead;
socklen_tlen;
struct sockaddr_storage claddr;
charbuf[BUF_SIZE];
char addrStr[IS_ADDR_STR_LEN];

if (becomeDaemon(0) == -1)
errExit("becomeDaemon");

sfd = inetBind(SERVICE, SOCK_DGRAM, NULL);
if (sfd == -1) {
syslog(LOG_ERR, „Server-Socket (%s) konnte nicht erstellt werden“,
strerror(errno));
Ausfahrt (EXIT_FAILURE);

/* Datagramme empfangen und Kopien an Absender zurücksenden */
}
für (;;) {
len = sizeof(struct sockaddr_storage);
numRead = recvfrom(sfd, buf, BUF_SIZE, 0, (struct sockaddr *) &claddr, &len);

if (numRead == -1)
errExit("recvfrom");
if (sendto(sfd, buf, numRead, 0, (struct sockaddr *) &claddr, len)
!=numRead)
syslog(LOG_WARNING, „Fehler beim Echo der Antwort auf %s (%s)“,
inetAddressStr((struct sockaddr *) &claddr, len,
addrStr, IS_ADDR_STR_LEN),
strerror(errno));
}
}
_________________________________________________________________sockets/id_echo_sv.c

Wir verwenden das Programm aus Listing 56.3, um den Server zu testen. Es verwendet auch die in Abschnitt 55.12 entwickelte Internet-Domain-Socket-Bibliothek. Das Client-Programm verwendet den Namen des Netzwerkhosts, auf dem sich der Server befindet, als erstes Argument in der Befehlszeile. Der Client tritt in eine Schleife ein, in der er jedes der verbleibenden Argumente als separate Datagramme an den Server sendet und dann die vom Server als Antwort empfangenen Datagramme liest und ausgibt.

Auflistung 56.3. Client für den Echo-UDP-Dienst

#include „id_echo.h“

int
main(int argc, char *argv[])
{
int sfd,j;
size_tlen;
ssize_t numRead;
charbuf[BUF_SIZE];

if (argc < 2 || strcmp(argv[1], "--help") == 0)
useErr("%s host msg…n", argv[0]);

/* Bilden Sie die Serveradresse basierend auf dem ersten Befehlszeilenargument */
sfd = inetConnect(argv[1], SERVICE, SOCK_DGRAM);
if (sfd == -1)
fatal("Verbindung zum Server-Socket konnte nicht hergestellt werden");

/* Die restlichen Argumente als separate Datagramme an den Server senden */
für (j = 2; j < argc; j++) {
len = strlen(argv[j]);
if (write(sfd, argv[j], len) != len)
fatal("teilweiser/fehlgeschlagener Schreibvorgang");

numRead = read(sfd, buf, BUF_SIZE);
if (numRead == -1)
errExit("read");
printf("[%ld bytes] %.*sn", (long) numRead, (int) numRead, buf);
}
Beenden (EXIT_SUCCESS);
}
_________________________________________________________________sockets/id_echo_cl.c

Das Folgende ist ein Beispiel dafür, was wir sehen werden, wenn wir den Server und zwei Client-Instanzen starten:

$ su // Erfordert Berechtigungen zum Binden an einen reservierten Port
Passwort:
# ./id_echo_sv // Der Server geht in den Hintergrund
#exit // Admin-Rechte aufgeben
$ ./id_echo_cl localhost hello world // Dieser Client sendet zwei Datagramme
[5 Bytes] hallo // Der Client gibt die vom Server empfangene Antwort aus
[5 Bytes] Welt
$ ./id_echo_cl localhost goodbye // Dieser Client sendet ein Datagramm
[7 Bytes] Auf Wiedersehen

Ich wünsche Ihnen eine angenehme Lektüre)

Source: linux.org.ru