203 lines
8.4 KiB
TeX
203 lines
8.4 KiB
TeX
\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}
|