1.0
Die Vermittlungsschicht (Internet Layer)
übernimmt das Routing
realisiert Ende-zu-Ende-Kommunikation
überträgt Pakete
ist im Internet durch IP realisiert
löst folgende Probleme:
Sender und Empfänger erhalten netzweit eindeutige Bezeichner (⇒ IP-Adressen)
die Pakete werden durch spezielle Geräte (⇒ Router) weitergeleitet
Transmission Control Protocol (TCP), RFC 793
verbindungsorientierte Kommunikation
ebenfalls Konzept der Ports
Verbindungsaufbau zwischen zwei Prozessen (Dreifacher Handshake, Full-Duplex-Kommunikation)
geordnete Kommunikation
zuverlässige Kommunikation
Flusskontrolle
hoher Overhead ⇒ eher langsam
nur Unicasts
User Datagram Protocol (UDP), RFC 768
verbindungslose Kommunikation
unzuverlässig ⇒ keine Fehlerkontrolle
ungeordnet ⇒ beliebige Reihenfolge
wenig Overhead ⇒ schnell
Größe der Nutzdaten ist 65507 Byte
Anwendungsfelder:
Anw. mit vorwiegend kurzen Nachrichten (z. B. NTP, RPC, NIS)
Anw. mit hohem Durchsatz, die gel. Fehler tolerieren (z. B. Multimedia)
Multicasts sowie Broadcasts
UDP nutzt für die Kommunikation "Datagramme" (Pakete). In der Praxis sin die Nutzdaten meist deutlich kleiner und orientieren sich an der Größe für IP-Pakete.
RFC 7230 – 7235: HTTP/1.1 (redigiert im Jahr 2014; urspr. 1999 RFC 2626)
RFC 7540: HTTP/2 (seit Mai 2015 standardisiert)
Eigenschaften:
Client / Server (Browser / Web-Server)
basierend auf TCP, i. d. R. Port 80
Server (meist) zustandslos
seit HTTP/1.1 auch persistente Verbindungen und Pipelining
abgesicherte Übertragung (Verschlüsselung) möglich mittels Secure Socket Layer (SSL) bzw. Transport Layer Security (TLS)
HTTP-Kommandos
(„Verben“)
HEAD
GET
POST
PUT
PATCH
DELETE
OPTIONS
TRACE
CONNECT
...
Aufbau der Dokumentenbezeichner Uniform Resource Locator (URL)
scheme://host[:port][abs_path[?query][#anchor]]
Protokoll (case-insensitive) (z. B. http, https oder ftp)
DNS-Name (oder IP-Adresse) des Servers (case-insensitive)
(optional) falls leer, 80 bei http und 443 bei https
(optional) Pfadausdruck relativ zum Server-Root (case-sensitive)
(optional) direkte Parameterübergabe (case-sensitive) (?from=…&to=…)
(optional) Sprungmarke innerhalb des Dokuments
Uniform Resource Identifier (URI) sind eine Verallgemeinerung von URLs.
definiert in RFC 1630 (im Jahr 1994)
entweder URL (Location) oder URN (Name) (z. B. urn:isbn:1234567890)
Beispiele von URIs, die keine URL sind, sind XML Namespace Iidentifiers
<svg version="1.1" xmlns="http://www.w3.org/2000/svg">...</svg>
Dient dem Anfordern von HTML-Daten vom Server (Request-Methode).
Minimale Anfrage:
1GET <Path> HTTP/1.1
2Host: <Hostname>
3Connection: close
4<Leerzeile (CRLF)>
Client kann zusätzlich weitere Infos über die Anfrage sowie sich selbst senden.
Server sendet Status der Anfrage sowie Infos über sich selbst und ggf. die angeforderte HTML-Datei.
Fehlermeldungen werden ggf. vom Server ebenfalls als HTML-Daten verpackt und als Antwort gesendet.
Beispiel Anfrage des Clients
1GET /web/web.php HTTP/1.1
2Host: archive.org
3**CRLF**
Beispiel Antwort des Servers
1HTTP/1.1 200 OK
2Server: nginx/1.25.1
3Date: Thu, 22 Feb 2024 19:47:11 GMT
4Content-Type: text/html; charset=UTF-8
5Transfer-Encoding: chunked
6Connection: close
7**CRLF**
8<!DOCTYPE html>
9…
10</html>**CRLF**
Sockets sind Kommunikationsendpunkte.
Sockets werden adressiert über die IP-Adresse (InetAddress-Objekt) und eine interne Port-Nummer (int-Wert).
Sockets gibt es bei TCP und auch bei UDP, allerdings mit unterschiedlichen Eigenschaften:
verbindungsorientierte Kommunikation über Streams
verbindungslose Kommunikation mittels Datagrams
Das Empfangen von Daten ist in jedem Fall blockierend, d. h. der empfangende Thread bzw. Prozess wartet, falls keine Daten vorliegen.
Der Server-Prozess wartet an dem bekannten Server-Port.
Der Client-Prozess erzeugt einen privaten Socket.
Der Socket baut zum Server-Prozess eine Verbindung auf – falls der Server die Verbindung akzeptiert.
Die Kommunikation erfolgt Strom-orientiert: Für beide Parteien wird je ein Eingabestrom und ein Ausgabestrom eingerichtet, über den nun Daten ausgetauscht werden können.
Wenn alle Daten ausgetauscht wurden, schließen im Allg. beide Parteien die Verbindung.
1import sys
2import socket
34
def scan_port(host, port):
5try:
6with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
7s.settimeout(0.5) # Set a timeout to avoid hanging connections
8s.connect((host, port))
9print(f"Port {port} is open on {host}")
10except (ConnectionRefusedError, TimeoutError) as e:
11pass # Port is likely closed, expected behavior
1213
def main():
14host = "localhost"
15if len(sys.argv) > 1: host = sys.argv[1]
16for port in range(1, 1024): scan_port(host, port)
1718
if __name__ == "__main__": main()
Nach erfolgtem Verbindungsaufbau können zwischen Client und Server mittels sendall
und recv
Daten ausgetauscht werden.
Wir können blockierend auf Daten warten bzw. blockierend schreiben, indem wir recv
bzw. sendall
aufrufen. (Siehe nächstes Beispiel.)
Sollte die Verbindung abbrechen oder die Gegenseite nicht antworten, kann es „relativ lange dauern“, bis dieser Fehler erkannt bzw. gemeldet wird.
Wir können den Socket auch in den nicht-blockierenden Modus versetzen, indem wir setblocking(False)
aufrufen (ggf. sinnvoll).
1# Client
2import socket
3def receive_all(conn, chunk_size=1024):
4data = b''
5while True:
6part = conn.recv(chunk_size)
7data += part
8if len(part) == 0: break # no more data
9return data
1011
while True:
12the_line = input()
13if the_line == ".": break
14with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
15s.connect(("localhost", 5678)) # Connect to localhost on port 5678
16s.sendall(the_line.encode())
17data = receive_all(s)
18print(data.decode())
1# Server
2import socket
3def receive_all(conn, chunk_size=1024): # see previous example
45
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
6server.bind(("localhost", 5678)) # Bind to localhost on port 5678
7server.listen(1) # queue at most one connection at a time
8while True:
9conn, addr = server.accept()
10with conn:
11print(f"Connection from {addr}.")
12data = receive_all(conn, 1024)
13print(f"Received {data}.")
14if data:
15conn.sendall(data)
Python erlaubt es Sockets zu Wrappen, um sie wie Dateien behandeln zu können.
<Socket>.makefile(mode="r?w?b?" [, encoding="utf-8"])
erzeugt ein Dateiobjekt, das (insbesondere) readline() und write() unterstützt. Dies kann insbesondere bei zeilenorientierter Kommunikation hilfreich sein.
Es können auch ganze Dateien über Sockets basierend übertragen werden (<Socket>.sendfile(<File>)
).
Clientseitig
Datagram-Socket erzeugen und an Zieladresse binden
Nachricht erzeugen (ggf. vorher maximale Länge prüfen)
Datagram absenden
ggf. Antwort empfangen und verarbeiten
Serverseitig
Datagram-Socket auf festem Port erzeugen
(Die Hostangabe bestimmt wer sich mit dem Socket verbinden darf; localhost bedeutet nur lokale Verbindungen sind erlaubt.)
Endlosschleife beginnen
Datagram empfangen (und verarbeiten)
ggf. Antwort erstellen und absenden
1import socket
23
HOST = "localhost"
4PORT = 5678
56
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as server:
7server.bind((HOST, PORT))
89
while True:
10data, addr = server.recvfrom(65507) # buffer size is 65507 bytes
11print(f"received message: {data} from: {addr}")
12server.sendto(data, addr)
Ein einfacher HTTP-Client
Schreiben Sie einen HTTP-Client, der den Server www.michael-eichberg.de kontaktiert, die Datei /index.html anfordert und die Antwort des Servers auf dem Bildschirm ausgibt.
Verwenden Sie HTTP/1.1 und eine Struktur ähnlich dem in der Vorlesung vorgestellten Echo-Client.
Senden Sie das GET-Kommando, die Host-Zeile sowie eine Leerzeile als Strings an den Server.
Erweitern Sie Ihren Client um die Fähigkeit URLs auf der Kommandozeile anzugeben.
Verwenden Sie existierende Funktionalität, um die angegebene URL zu zerlegen (urlparse von urllib.parse).
Speichern Sie die Antwort des Servers in einer lokalen Datei. Prüfen Sie, dass die Datei in einem Browser korrekt angezeigt wird.
Kann Ihr Programm auch Bilddateien (z. B. "http://www.michael-eichberg.de/acm.svg") korrekt speichern? Falls nicht, prüfen Sie ob Sie Antwort des Servers richtig verarbeiten; analysieren Sie ggf. den Header und passen Sie Ihr Programm entsprechend an.
Protokollaggregation
Schreiben Sie einen Python basierten Server und Client, mit dem sich Protokoll-Meldungen auf einem Server zentral anzeigen lassen. Das Programm soll mehrere Clients unterstützen und UDP verwenden. Jeder Client liest von der Tastatur eine Eingabezeile in Form eines Strings ein, validiert die Eingabe und sendet diese dann ggf. sofort zum Server. Der Server wartet auf Port 5678 und empfängt die Meldungen beliebiger Clients, die er dann unmittelbar ausgibt.
Stellen Sie sicher, dass Fehler adäquat behandelt werden.
Sie können den UDP basierten Echo Server als Vorlage für Ihren Server verwenden.