↑Programmieren in Rust
Als erstes Beispiel soll ein Client-Programm geschrieben
werden, welches sich mit dem Server der Domain www.example.com
verbindet und über das HTTP-Protokoll die HTML-Seite
http://www.example.com/
erfragt.
Es gibt natürlich Bibliotheken dafür, auch welche mit denen sich sogleich eine verschlüsselte Kommunikation über HTTPS erstellen lässt. Diese Bibliotheken sollte man auch benutzen. Aus didaktischen Gründen wollen wir das hier aber zunächst einmal händisch machen.
Zunächst wird die IP-Adresse der Domain benötigt. Um an diese
zu gelangen, müsste man sich zunächst mit einem DNS-Server verbinden,
der wiederum eine IP-Adresse besitzt. Wir lassen das zur
Vereinfachung bleiben und tragen die Adresse manuell ein.
IP-Adressen können sich von Zeit zu Zeit ändern. Am 23. Dez. 2019
bekam ich 93.184.216.34
. Das Format ist noch das
alte IPv4, man kann aber genauso gut IPv6 eintragen. Zusätzlich
muss man noch eine Portnummer angeben. Bei HTTP ist dies normalerweise Port 80.
Zum Aufbau einer Verbindung mit dem Server muss man ein Socket eröffnen,
über das man über das Protokoll TCP/IP mit dem Server kommunizieren
kann. Zum Erfragen der HTML-Seite wird eine HTTP-GET-Nachricht
abgeschickt. Das ist eine Zeichenkette, deren Format im HTTP-Protokoll
festgelegt ist. Danach wird auf eine Rückmeldung vom Server
gewartet, die ebenfalls im HTTP-Format ankommt. HTTP-Nachrichten
bestehen aus einem Header und einem Body, welche durch eine
leere Zeile getrennt sind. Die leere Zeile ist kodiert als Sequenz
zweier Zeilenumbrüche, die jeweils als CR LF
kodiert sein sollen, das heißt, die leere Zeile ist die Sequenz
b"\r\n\r\n"
. Bei HTTP-GET ist der Body leer. Die
Rückmeldung sollte aber einen Body mit dem eigentlichen HTML-Text
enthalten.
Das Programm:
use std::net::TcpStream; use std::io::{Write, Read}; fn main() -> Result<(), std::io::Error> { let host = "www.example.com"; let path = "/"; let mut stream = TcpStream::connect("93.184.216.34:80")?; let message = format!( "GET {} HTTP/1.0\r\nHost: {}\r\n\r\n", path, host ); stream.write_all(message.as_bytes())?; let mut response = String::new(); stream.read_to_string(&mut response)?; println!("{}", response); Ok(()) }
Ich bekam die folgende Nachricht:
HTTP/1.0 200 OK Accept-Ranges: bytes Cache-Control: max-age=604800 Content-Type: text/html; charset=UTF-8 Date: Mon, 23 Dec 2019 05:04:22 GMT Etag: "3147526947+ident" Expires: Mon, 30 Dec 2019 05:04:22 GMT Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT Server: ECS (nyb/1DCD) Vary: Accept-Encoding X-Cache: HIT Content-Length: 1256 Connection: close <!doctype html> <html> <head> <title>Example Domain</title> <meta charset="utf-8" /> <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <style type="text/css"> ... </style> </head> <body> <div> <h1>Example Domain</h1> <p>This domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission.</p> <p><a href="https://www.iana.org/domains/example">More information...</a></p> </div> </body> </html>
Obwohl das Programm funktionsfähig ist, ist der dargestellte Ansatz allerdings noch mit gewissen Problemen behaftet. Sollte der Server nicht sofort auf die Nachricht antworten, hängt das Programm. Man kann sich dafür verschiedene Lösungsansätze überlegen. Zum einen ist es zielführend, die Anfrage in einer asynchronen Funktion oder einem extra Thread durchzuführen. Zum anderen lässt sich für die Anfrage ein Timeout festlegen – falls dieses nicht eingehalten wird, fragt man später nochmals nach. Im Endeffekt läuft es darauf hinaus, Server und Clients nebenläufig zu programmieren. Netzwerk-Programmierung ist ein typischer Anwendungsfall für Nebenläufigkeit.
Im nächsten Schritt wollen wir uns mit der Konstruktion eines lokalen Netzwerks befassen. Neben dem Client erfordert es einen Server, mit dem der Client kommunizieren kann. Gezeigt wird, wie man einen minimalen lokalen HTTP-Server erstellt. Wir verzichten dabei erst einmal wieder auf die Nutzung einer HTTP-Bibliothek, indem abermals die auf der tieferliegenden Ebene befindliche TCP-Schnittstelle direkt angesprochen wird.
Die Übergabe der speziellen IP-Adresse 0.0.0.0
an
bind
bewirkt, dass das Socket auf sämtlichen lokalen
IP-Adressen Verbindungsanfragen abhorcht. Weil die Ports von 0
bis einschließlich 1023 lediglich mit Administrator-Rechten errichtet
werden dürfen, muss man einen Port außerhalb dieses Bereichs wählen.
Üblicherweise nimmt man dafür Port 8000 oder 8080 in Anlehnung an
Port 80.
use std::net::{TcpListener, TcpStream}; use std::io::{Read, Write}; type Error = Box<dyn std::error::Error>; fn read_request(stream: &mut TcpStream) -> Result<Vec<u8>, Error> { let mut acc = vec![]; let mut buf = [0u8; 8192]; loop { let count = stream.read(&mut buf)?; acc.extend_from_slice(&buf[..count]); if acc.ends_with(b"\r\n\r\n") {break;} } Ok(acc) } fn main() -> Result<(), Error> { let listener = TcpListener::bind("0.0.0.0:8000")?; loop { let (mut stream, addr) = listener.accept()?; println!("Verbindung mit {} wurde akzeptiert.", addr); let data = read_request(&mut stream)?; let request = std::str::from_utf8(&data)?; println!("== HTTP-Anfrage bekommen ==\n{}", request); stream.write_all(b"HTTP/1.1 200 OK\r\n\r\n")?; stream.write_all(b"Hallo, Welt!\n")?; println!("Verbindung mit {} wurde beendet.\n", addr); } }
Läuft der Server, erhält ein Client die formulierte Antwort auf eine GET-Anfrage. Man kann dies kurzum mit einem der Befehle
curl localhost:8000
curl http://localhost:8000
curl 127.0.0.1:8000
GET http://localhost:8000
GET 127.0.0.1:8000
testen, oder, indem localhost:8000
in die Adress-Zeile
eines Webbrowsers eingegeben wird. Erwartungsgemäß kann dafür ebenso
eine Anpassung des zuvor formulierten Client-Programms genutzt werden.
Die IP-Adresse muss man nicht unbedingt als Zeichenkette übergeben:
use std::net::{TcpStream, SocketAddrV4, Ipv4Addr}; let socket = SocketAddrV4::new(Ipv4Addr::new(93, 184, 216, 34), 80); let mut stream = TcpStream::connect(socket)?;
Die IP-Adresse ist eine u32
-Zahl. Die Angabe
können wir umrechnen:
93*2553 + 184*2552 + 216*255 + 34
Die direkte Angabe als Zahl ist möglich:
let ip: u32 = 0x5db8d822; let socket = SocketAddrV4::new(Ipv4Addr::from(ip), 80);
Oder als Zeichenkette:
let ip = "93.184.216.34".parse::<Ipv4Addr>().unwrap(); let socket = SocketAddrV4::new(Ipv4Addr::from(ip), 80);
Jede IPv4-Adresse ist auch eine gültige IPv6-Adresse.
Die IPv4-Adresse 0xhhhhhhhh
wird so eingebettet:
0000:0000:0000:0000:0000:ffff:hhhh:hhhhbzw. kurz:
::ffff:hhhh:hhhh
Das Programm ist dann von folgender Form:
let ip = Ipv6Addr::new(0, 0, 0, 0, 0, 0xffff, 0x5db8, 0xd822); let socket = SocketAddrV6::new(ip, 80, 0, 0);
Entsprechend gibt es auch hier die Angabe als Zahl:
let ip: u128 = 0xffff5db8d822; let socket = SocketAddrV6::new(Ipv6Addr::from(ip), 80, 0, 0);
Oder als Zeichenkette:
let ip = "::ffff:5db8:d822".parse::<Ipv6Addr>().unwrap(); let socket = SocketAddrV6::new(ip, 80, 0, 0);