Programmieren in Rust

Netzwerk-Programmierung

Inhaltsverzeichnis

  1. Sockets
    1. Ein minimaler Client
    2. Ein minimaler Server
  2. IP-Adressen
  3. Literatur

Sockets

Ein minimaler Client

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.

Ein minimaler Server

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.

IP-Adressen

IPv4

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);

IPv6

Jede IPv4-Adresse ist auch eine gültige IPv6-Adresse. Die IPv4-Adresse 0xhhhhhhhh wird so eingebettet:

0000:0000:0000:0000:0000:ffff:hhhh:hhhh
bzw. 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);

Literatur

  1. »hyper.rs«. Dokumentation zur HTTP-Bibliothek hyper.
  2. Amos Wenger: »Request coalescing in async Rust. Blog, 6. März 2022.
  3. »Hypertext Transfer Protocol«. Englische Wikipedia.
  4. »List of TCP and UDP port numbers«. Englische Wikipedia.
  5. Bastian Ballmann: »Network Hacks«. Springer-Verlag, Berlin & Heidelberg 2012.