Skip to content

Instantly share code, notes, and snippets.

@pascalj
Last active May 12, 2023 15:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pascalj/446072ad352729e5b7dccfb5c4211efe to your computer and use it in GitHub Desktop.
Save pascalj/446072ad352729e5b7dccfb5c4211efe to your computer and use it in GitHub Desktop.
Schichtenkonzept: Beispiel
Display the source blob
Display the rendered blob
Raw
{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3 (ipykernel)","language":"python"},"language_info":{"name":"python","version":"3.7.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Ein einfaches Beispiel in Python\n\nAlice möchte eine Nachricht an Bob verschicken. Dazu nutzt sie eine Funktion `send_message`, die eine Nachricht an den Empfänger übermittelt:\n\n```python\nsend_message(\"Bob\", \"Hallo, Bob!\")\n// => True\n```\n\nDie Funktion gibt zurück, ob die Nachricht erfolgreich an den Empfänger übermittelt wurde.\n\nIn diesem Beispiel werden wir sehen, wie das Schichtenkonzept praktisch funktioniert. Wir nutzen aus der Anwendung heraus ein Protokoll der Anwendungsschicht, das wiederum Daten der Transportschicht mittels Sockets verschickt. Die Beispiele verzichten zur besseren Lesbarkeit auf jegliche Fehlerbehandlung.\n\n\n## 1. Alices Implementierung von `send_message`\n\nAls erstes müssen Alice und Bob sich auf ein *Protokoll* der Anwendungsschicht einigen, über das sie die Nachricht übertragen können. Dieses Protokoll müssen dann beide implementieren.\n\n### HTTP\n\nHier benutzen sie das [Hypertext Transfer Protocol (HTTP)](https://de.wikipedia.org/wiki/Hypertext_Transfer_Protocol), mit dem üblicherweise Webseiten übertragen werden. Es wird aber auch für viele andere Dienste genutzt, da es vielseitig und sehr einfach ist. Es gibt auch Protokolle, die besser für den Austausch von Nachrichten geeignet sind, jedoch eignen sie sich nicht für ein so einfaches Beispiel.\n\nAlice schreibt also die Funktion `send_message` und nutzt HTTP zum verschicken der Nachricht.","metadata":{}},{"cell_type":"code","source":"def send_message(to, message):\n # Wie genau wir den Host aus dem \"Benutzernamen\" (to) ermitteln, hängt von unserer Applikation ab.\n # Der Einfachheit halber legen wir 'www.nm.ifi.lmu.de' fest, aber\n # das könnte z.B. auch in einer Datenbank gespeichert sein.\n host = \"www.nm.ifi.lmu.de\"\n \n # Verschicken der Nachricht über HTTP POST\n status = post_http(host, \"/teaching/Vorlesungen/2023ss/rn/message/\", message)\n \n # Gebe True zurück, wenn der Response-Status 200 ist\n return status == 200","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"HTTP folgt der Client-Server-Architektur. Die PDU in HTTP nennt sich *Nachricht*, wird durch Absetzen einer HTTP-Anfrage (Request) durch den Client initialisiert. Dieser Request enthält neben unserer Nachricht als Nutzlast bzw. Nutzdaten einen HTTP-Header oder kurz Header, der Steuerinformationen enthält. Der HTTP-Server quittiert den Request mit einer Antwort (Response), die u.a. anzeigt, ob der Request erfolgreich empfangen wurde.\n\nEin HTTP-Request hat die Form: `[METHODE] [PFAD] HTTP/1.1\\r\\n[FELDER]\\r\\n\\r\\n[NUTZLAST]`, also z.B.\n\n```\nPOST /messages HTTP/1.1\nHost: bob.example.com\nContent-Length: 11\n\nHallo, Bob!\n```\n\nDie Zeichenfolge `\\r\\n` steht für eine neue Zeile. Alice möchte aus der Nachricht (Nutzdaten) und dem Empfänger (Steuerdaten) einen HTTP-Request erzeugen, diesen übertragen und die Antwort des HTTP-Server erhalten. Dazu implementiert sie eine Funktion `post_http(target, body)`.","metadata":{}},{"cell_type":"code","source":"def post_http(host, path, payload):\n # Erzeugen eines Requests\n request = create_http_request(\"POST\", path, host, payload)\n \n # Verschicken des Requests über TCP\n http_port = 443\n response = transmit_with_tcp(host, http_port, request)\n \n # Interpretieren der erhaltenen Antwort als String\n response_string = response.decode('utf-8')\n\n # Extrahieren des \"Status\" der Antwort via Regex\n # Dieser extrahiert aus \"HTTP/1.1 xxx The Status Message\" die xxx\n match = re.search(r\"^HTTP/1\\.1 (\\d{3}) .+\", response_string)\n status = int(match.group(1))\n\n # Antwort zurückgeben\n return status","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Die Funktion erzeugt also aus aus einer Nachricht und einigen Steuerdaten einen Request, verschickt diesen und gibt den *status* der Antwort zurück. Zuerst muss Alice die Funktion schreiben, die den Request erstellt:","metadata":{}},{"cell_type":"code","source":"def create_http_request(method, path, host, body = \"\"):\n content_length = len(body)\n header = \"%s %s HTTP/1.1\\r\\nHost: %s\\r\\nContent-Length: %s\" %(method, path, host, content_length)\n request = \"%s\\r\\n\\r\\n%s\" % (header, body)\n return request.encode('utf-8')","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Diese Funktion gibt die Zeichenkette als UTF-8 enkodierte Binärdaten zurück. Der Request ist - wie oben zu sehen - einfach nur Text, jedoch können wir auf unterliegenden Schichten nur Bytes also Binärdaten übertragen. Man kann die Bytes wieder dekodieren (`decode`) und die Zeichenkette ausgeben:","metadata":{}},{"cell_type":"code","source":"print(create_http_request(\"GET\", \"/\", \"www.nm.ifi.lmu.de\").decode('utf-8'))","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Alice nutzt die Methode `POST` und den Pfad `/teaching/Vorlesungen/2023s/rn/message/`. Über die genaue Semantik dieser beiden Parameter des HTTP-Requests muss zwischen Alice und Bob Einigkeit bestehen. Vom Standard werden nur einige Vorgaben gemacht, z.B. dass ein `GET`-Request die mit dem Pfad beschriebenen Resource in der Antwort zurückliefert.\n\nDie Funktion `transmit_with_tcp` soll nun den Request übertragen. Dafür ist die Transportschicht zuständig. Jemand, der die Funktion `post_http` aufruft, muss nichts über das gewählte Protokoll dieser Schicht wissen. Hier wird das **Schichtenprinzip** deutlich: wie `transmit_with_tcp` die Übertragung gewährleistet, ist für den Aufrufer der Funktion **gänzlich transparent**.\n\nFür HTTP ist als Transportschichtprotokoll TCP im RFC festgeschrieben. `transmit_with_tcp` nimmt den HTTP-Request als Nutzdaten entgegen und überträgt ihn via TCP.","metadata":{}},{"cell_type":"markdown","source":"### TCP Sockets\n\nZum zuverlässigen übertragen von Datenströmen wurde TCP entworfen. Da TCP *verbindungsorientiert* ist, muss erst eine Verbindung aufgebaut werden, bevor Datenströme verschickt bzw. empfangen werden können.\n\nEin TCP-Socket besteht aus einem paar von *IP-Adresse* und einem *Port*. Die IP-Adresse beschreibt dabei den Host und der Port dient zum Multiplexing der Applikationen auf dem Host. Viele Protokolle haben typische Ports zugewiesen, unter denen die Server erreichbar sind, damit nicht explizit ein Port zwischen Server und Client vereinbart werden muss.\n\nAlice ist in diesem Fall der Client, da sie eine Verbindung zu Bob bzw. seinem Server-Prozess aufbaut. Als Client hat sie vier Schritte zu vollziehen:\n\n1. Verbindung aufbauen (connect)\n2. Daten senden (send)\n3. Daten empfangen (recv)\n4. Verbindung schließen (close)\n\nAll das passiert in `transmit_with_tcp`:","metadata":{}},{"cell_type":"code","source":"import socket\nimport re\nimport ssl\n\ncontext = ssl.create_default_context()\n\ndef transmit_with_tcp(host, port, data):\n # Erzeugt in \"s\" einen TCP-Socket (SOCK_STREAM) über IPv4 (AF_INET)\n with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:\n with context.wrap_socket(s, server_hostname=host) as ss:\n # 1. Verbindung zum Server-Socket (host, port) wird aufgebaut\n ss.connect((host, port))\n\n # 2. Die Daten werden verschickt\n ss.sendall(data)\n\n # 3. Die Antwort wird empfangen (bis zu 4096 Bytes)\n response_bytes = ss.recv(4096) \n\n # 4. Verbindung schließen (geschieht automatisch beim Verlassen der with-Umgebung)\n return response_bytes","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Hier kann man die vier Phasen der Verbindung gut erkennen. Wollte Alice viele Nachrichten hintereinander verschicken, würde sie die Verbindung länger offen halten und `send[all]` und `recv` mehrfach auf dem einmal geöffneten Socket `s` aufrufen. `s.close()` würde sie in dem Fall natürlich erst nach dem Versenden aller Nachrichten aufrufen.\n\n### Verschicken der Nachricht\n\nNun kann Alice `send_message` aus ihrer Applikation heraus aufrufen. Aus Sicht der Anwendung sind Aufgrund der Trennung der Schichten die dabei benutzten Protokolle transparent.","metadata":{}},{"cell_type":"code","source":"if send_message(\"Bob\", \"Hallo, Bob!\") == True:\n print(\"Nachricht erfolgreich verschickt.\")\nelse:\n print(\"Verschicken fehlgeschlagen.\") ","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Alice hat die Nachricht erfolgreich übertragen und dafür eine Bestätigung erhalten.\n\n## 2. Bobs Implementierung von `receive_message`\n\nWegen der Client-Server-Architektur muss Bob einen Server betreiben und auf Nachrichten warten. Er muss also die eine Funktion `receive_message` bereitstellen, die genau dies tut.","metadata":{}},{"cell_type":"code","source":"def receive_message():\n payload = handle_http_request()\n return payload","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Das ist einfach: `handle_http_request` blockiert so lange, bis ein HTTP-Request von einem Client eingegangen ist, extrahiert die Nutzlast (`payload`) und gibt eine Antwort zurück.\n\n### TCP Server\n\nBob muss nun das passende Gegenstück zu Alices Client-Implementierung bereitstellen. In `handle_http_request` sieht man jetzt wieder den Übergang von Anwendungs- zu Transportschicht.\n\nBob muss jetzt einen Socket erstellen, auf dem er auf eingehende Verbindungen von Clients lauscht, diese entgegennimmt und ggf. antwortet. Das passiert ebenfalls in mehreren Schritten:\n\n1. Socket an ein Host/Port-Tupel *binden* (`bind`)\n2. Auf dem Socket auf einkommende Verbindungen *lauschen*\n3. Einkommende Verbindungen annehmen, was einen neuen Socket für den Verbundnen Client erzeugt (`accept`)\n4. Socket Schließen (`close`)","metadata":{}},{"cell_type":"code","source":"import socket\n\ndef handle_http_request():\n # HTTP benutzt TCP\n host = b'' # Lauschen auf allen möglichen Adressen, die auf diesem Host zur Verfügung stehen\n port = 8080 # Muss mit Client übereinstimmen\n \n # Erzeugen eines Puffers zum Empfangen eines Requests\n request_bytes = b''\n \n # Erzeugen eines Sockets analog zum Client\n with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:\n # Binden des Sockets an Port \"port\"\n s.bind((host, port))\n \n # Lauschen auf eine Verbindung\n s.listen(1)\n \n # Akzeptieren der Verbindung, erzeug einen neuen Socket \"conn\"\n conn, addr = s.accept()\n\n with conn:\n # Empfangen des HTTP-Requests von bis zu 4096 Bytes\n request_bytes = conn.recv(4096)\n\n # Request erhalten: antworten mit einer einfachen HTTP-Response\n http_response = b'HTTP/1.1 200 OK\\r\\nConnection: close\\r\\n'\n conn.sendall(http_response)\n \n # Erzeugen einer Zeichenkette aus den Binärdaten\n request = request_bytes.decode('utf-8')\n \n # Payload auslesen: zur Erinnerung ein Request hat die Form 'HEADER\\r\\n\\r\\nPAYLOAD'\n # Man kann also alles nach \\r\\n\\r\\n als Payload ansehen\n payload = request.split(\"\\r\\n\\r\\n\")[1]\n \n return payload","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"`handle_http_request` öffnet also einen Socket, lauscht auf eingehende Verbindungen, nimmt einen Bytestrom entgegen, interpretiert ihn als HTTP-Request und Antwortet mit einer einfachen `200 OK` Response. Außerdem extrahiert es noch den Payload (Alices Nachricht), die im Request mitgeschickt wurde.\n\nHier sei noch einmal darauf hingewiesen, dass diese Implementierung einige Vereinfachungen macht. Zum Beispiel nimmt sie immer nur eine Anfrage entgegen und die Anfrage darf nicht größer als 4096 Bytes sein. In der Praxis arbeitet man deshalb mit while-Schleifen, die so lange Inhalt lesen, bis der Request komplett empfangen wurde. Dafür nutzt man das `Content-Length`-Headerfeld, um so lange Daten in `request_bytes` zu lesen, bis alle Bytes empfangen wurden.\n\n### Testen des Servers\n\nWenn man dieses Notebook auf dem eigenen Rechner ausführt, kann man die folgende Zeile einkommentieren (das `#` entfernen) und ausführen. Dann wird ein HTTP-Server gestartet, der genau eine Nachricht entgegen nimmt.","metadata":{}},{"cell_type":"code","source":"# receive_message()","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Ist der Server gestartet (links erscheint `In [*]`), so kann man per\n\n```\ncurl -4 -v -d 'Hallo, Bob!' localhost:8080\n```\n\nIn einer Shell die Nachricht verschicken, sie wird dann hier angezeigt. Alternativ kann man das Notebook ein zweites Mal starten und den `host` im Client auf `localhost` anpassen.\n\n## Fazit\n\nAn diesem Beispiel kann man das Schichtenkonzept in der Praxis sehen.\n\n- Normalerweise schreibt man einen HTTP-Client/Server nicht selbst, sondern nutzt fertige Bibliotheken. Damit reduziert sich auch die Komplexität deutlich: man ruft das Equivalent zu `post_http` in der Bibliothek auf und muss sich um den Rest nicht kümmern.\n- Die Applikation nutzt Protokolle der Applikationsschicht (HTTP in diesem Fall), die wiederum Protokolle der Transportschicht nutzen (TCP). Das geht natürlich weiter bis auf die Bitübertragungsschicht herunter, was den [Rahmen eines einfachen Beispiels](https://tools.ietf.org/html/rfc1180) sprengen würde.\n- Für den Anwendungsprogrammierer sind die Details der unteren Schichten transparent. Würde `send_message` statt HTTP ein komplett anderes Protokoll nutzen, müsste sich der Funktionsaufruf nicht ändern. Gleiches gilt auch jeweils für die tieferen Schichten!","metadata":{}},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]}]}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment