lecture-notes/inputs/network_programming/server_structure.tex

204 lines
8.4 KiB
TeX
Raw Normal View History

2022-02-15 20:57:33 +01:00
\subsection{Mögliche Eigenschaften von Servern}
Es soll ein Kommunikationsserver entworfen und programmiert werden, der in der Lage ist, viele Anfragen entgegenzunehmen.
Hierzu sind verschiedene Eigenschaften auszuwählen und zu realisieren:\footnote{Die Kombination aus concurrent und UDP wurde in der Vorlesung nicht behandelt.}
\begin{itemize}
\item iterativ (nacheinander) vs. concurrent (gleichzeitig, nebenläufig)
\item verbindungsorientiert (TCP) vs. verbindungslos (UDP)
\item stateless vs. stateful
\end{itemize}
Welche Konzepte sich eignen, hängt von verschiedenen Faktoren der jeweiligen Anwendung ab:
\begin{itemize}
\item erwartete Anzahl gleichzeitiger Anfragen
\item Größe der Anfrage
\item Schwankungen der Anfrage-Größe
\item verfügbare System-Ressourcen
\end{itemize}
\subsubsection{Iterativ vs. nebenläufig}
\begin{definition}[Iterativer Server]
Ein \vocab[Server!iterativ]{iterativer Server} bearbeitet zu einem Zeitpunkt genau eine Client-Anfrage, d.h.~alle Anfragen werden hintereinander bearbeitet.
\end{definition}
\begin{definition}[Nebenläufiger Server]
Ein \vocab[Server!nebenläufig]{nebenläufiger} (\vocab[Server!concurrent]{concurrent}) Server kann mehrere Client-Anfragen gleichzeitig bearbeiten.\footnote{Es ist auch möglich, dies auf mehrere Rechner zu verteilen, das wurde in der Vorlesung allerdings nicht behandelt.}
\end{definition}
Nebenläufige Server werden häufig eingesetzt, wenn Anfragen lang sind, oder variable Länge haben. Ein nebenläufiger Server besitzt ferner eine höhere Komplexität und benötigt i.d.R.~mehr Systemressourcen.
Iterative Server werden eingesetzt, wenn Anfragen kurz sind bzw. feste Länge besitzen.
\subsubsection{Verbindungslos vs. verbindungsorientiert}
\begin{minipage}[t]{0.49\textwidth}
\textbf{Verbindungslos}
\begin{itemize}
\item Weniger Overhead
\item Keine Begrenzung der Anzahl von Clients
\end{itemize}
\end{minipage}
\begin{minipage}[t]{0.49\textwidth}
\textbf{Verbindungsorientiert}
\begin{itemize}
\item Einfacher zu programmieren, TCP sorgt für zuverlässige Übertragung
\item Benötigt getrennte Sockets für jede aktive Verbindung. Hierdurch ist die Anzahl an Clients begrenzt.
\end{itemize}
\end{minipage}
\subsubsection{Stateless vs. stateful}
\begin{definition}[State]
Die Information, die ein Server über den Status einer aktuellen Client-Interaktion aufrecht erhält, wird als \vocab[Server!State]{State} bezeichnet.
Ein Server, welcher State besitzt, wird als \vocab[Server!stateful]{stateful} bezeichnet (einfacher mit verbindungsorientierten Servern realisierbar), ein Server ohne State heißt \vocab[Server!stateless]{stateless} (typisch für iterativ und verbindungslos).
\end{definition}
\begin{warning}
Verbindungslose Server mit State müssen besonders vorsichtig und sorgfältig konzipiert werden:
\begin{itemize}
\item Ein Client kann jederzeit ausfallen
\item Ein Client kann jederzeit neu starten (d.h. aus der Sicht des Servers mehrmals)
\item Nachrichten können verloren gehen
\item Nachrichten können dupliziert werden
\end{itemize}
\end{warning}
\subsection{Design-Ansätze für Server}
% TODO Slide 122
\subsubsection{UDP Server, iterativ}
Der Server blockiert, bis eine Anfrage empfangen wird und verarbeitet diese dann. Anschließend wird eine Antwort gesendet und auf die nächste Anfrage gewartet.
\todoimg{3 S.123}
Typischerweise wird keine Zustandsinformation gespeichert. Der Server ist somit in dieser Hinsicht unabhängig von der Anzahl der Clients.
Allerdings müssen Clients warten, während andere Anfragen bearbeitet werden. Ankommende Anfragen werden im \vocab{Socket Receive Buffer} zwischengespeichert. Dies ist problematisch, wenn Anfragen sehr lange dauern, da der Socket Receive Buffer nur eine begrenzte Zahl von Anfragen puffern kann und ggf. Anfragen verloren gehen können.
Anfragen, die nicht in ein UDP/IP Datagramm passen, können von diesem Server-Typ nicht unterstützt werden.
\begin{warning}
Sehr große UDP Datagramme sind u.U.~problematisch, da die Wahrscheinlichkeit einer korrekten Übertragung mit der große das Datagrammes abnimmt. Geht ein Teil eines Datagrammes verloren, so wird das gesamte Datagramm verworfen.
\end{warning}
\subsubsection{TCP Server, iterativ}
\todoimg{S. 128}
\begin{lstlisting}
int sock_listen_fd, newfd;
/* initialize server */
while (1) {
newfd = accept(sock_listen_fd, ...);
//handle request
close(newfd);
}
\end{lstlisting}
Der iterative TCP-Server funktioniert analog zum iterativen UDP-Server. Eine Client-Verbindung wird komplett bearbeitet, bevor die nächste Verbindung angenommen wird. Dadurch kann nur eine Verbindung gleichzeitig aktiv sein.
Die Anzahl an wartenden Verbindungen ist begrenzt, daher müssen bei langer Bearbeitungszeit einer Anfrage ggf. Verbindungsaufbauwünsche abgewiesen werden.
Möglicherweise wird der Server nicht gut ausgelastet, wenn Anfragen lange auf andere Ressource (z.B. Festplatte) warten.
Daher ist ein nebenläufiger Server hier i.d.R.~zu bevorzugen.
\subsubsection{TCP Server, nebenläufig}
Für jede Verbindung zu einer Client-Anfrage wird mit \code{fork()}\footnote{Siehe \ref{processeslinux}} in neuer Prozess generiert, der sich um die Bearbeitung der Anfrage kümmert.
Die Verbindung läuft nach der Rückkehr von \code{accept()} auf einem neuen Socket. Der Parent-Prozess schließt die neue Verbindung und wartet sofort wieder auf weitere Anfragen.
\todoimg{S. 131, 132}
\begin{lstlisting}
int main() {
int sock_listen_fd, newfd, child_pid;
/* initialize server... */
while (1) {
newfd = accept(sock_listen_fd, ...);
if (newfd < 0) { /* error */ }
child_pid = fork();
if(child_pid < 0) {
/* error */
} else if (child_pid == 0) { // child
close(sock_listen_fd);
// handle request ...
close(newfd);
exit(0);
} else { // parent
close(newfd);
}
}
\end{lstlisting}
\begin{observe}
Der Aufruf von \code{close()} dekrementiert die Anzahl aktiver Descriptoren und schließt noch nicht direkt den Socket! % Siehe \ref{closefile}.
\end{observe}
\begin{warning}
Nach dem Bearbeiten der Anfrage werden die Child-Prozesse zu Zombies. Der Parent muss sich darum kümmern, diese aufzuräumen.
\end{warning}
\begin{warning}
Wenn bei der Bearbeitung der Anfrage auf shared state zugegriffen werden soll, muss entsprechende IPC verwendet werden.
\end{warning}
Der Systemaufruf \code{fork()} ist relativ aufwendig. Ein möglicher anderer Ansatz wäre es, Threads einzusetzen. Ferner existieren Preforked Server.
\begin{definition}[Preforked Server]
Ein \vocab{Preforked Server} erstellt bereits bevor Anfragen eingehen eine Menge von Child-Prozessen. Jeder Child-Prozess ruft \code{accept()} auf, um auf Verbindungen zu warten. Das Betriebssystem entscheidet bei einer eingehenden Anfrage, welcher der Clients die Verbindung entgegennimmt.
\end{definition}
\begin{example}[Preforked Server]
~
\begin{lstlisting}
#define NB_PROC 10 // number of preforked children
// iterative server
void recv_requests(int fd) {
int f;
while(1) {
f=accept(fd, ...);
// handle request
close(f);
}
}
int main () {
int fd;
// initialize server ...
for (int i = 0; i <NB_PROC; ++i) {
if(fork() == 0){
recv_requests(fd);
}
}
while (1) {
pause();
}
}
\end{lstlisting}
\end{example}
\begin{warning}
Unter manchen Betriebssystemen funktioniert der gleichzeitige Zugriff auf \code{accept} nicht. Dann müsste dies mit einem Mutex geschützt werden.
\end{warning}
Alternativ kann der Parent-Prozess dynamisch die Anzahl von Preforked Children steuern. Statt Prozessen können natürlich auch hier Threads eingesetzt werden (Prethreading).
\begin{remark}
Der Apache Web Server verwendet ab Version 2.0 Preforking.
\end{remark}
\subsubsection{Select Loop}
Ein einziger Prozess kann auch alle Sockets mit Hilfe von \code{select()} bedienen. Dies wird als \vocab{Select Loop} bezeichnet.
Es ist extrem schwierig, dies korrekt zu implementieren, da Client-Anfragen, die ggf. mehrere \code{read() write()}-Zyklen benötigen, korrekt verarbeitet werden müssen.
Hierzu werden komplexe Datenstrukturen benötigt.
\begin{remark}
NGINX, Node.js und Squid WWW Cache verwenden Select Loops.
\end{remark}