IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Tutoriel pour apprendre les bases de la programmation en Rust

Image non disponible


précédentsommaire

III. Aller plus loin

III-A. Utiliser du code compilé en C

Rust permet d'exécuter du code compilé en C. Ce chapitre va vous montrer comment faire.

III-A-1. Les bases

La première chose à faire est d'ajouter une dépendance à la bibliothèque libc :

Cargo.toml
Sélectionnez
[dependencies]
libc = "*"
Fichier principal
Sélectionnez
extern crate libc;

Toute fonction que vous voudrez utiliser doit être déclarée ! Par exemple, utilisons la fonction rename :

 
Sélectionnez
extern crate libc;

use std::ffi::CString;

extern "C" {
    fn rename(old: *const libc::c_char, new_p: *const libc::c_char) -> libc::c_int;
}

fn main() {
    if unsafe { rename(CString::new("old").unwrap().as_ptr(),
                       CString::new("new").unwrap().as_ptr()) } != 0 {
        println!("Rename failed");
    } else {
        println!("successfully renamed !");
    }
}

À noter qu'il est tout à fait possible de ne pas passer par les types fournis par la libc :

 
Sélectionnez
extern "C" {
    fn rename(old: *const i8, new_p: *const i8) -> i32;
}

Cependant je vous le déconseille, les types fournis par la libc ont l'avantage d'être plus clairs et surtout de correspondre au type C. Regardons maintenant comment utiliser des fonctions d'une bibliothèque C.

III-A-2. Interfaçage avec une bibliothèque C

Tout d'abord, il va falloir linker notre code avec la bibliothèque C que l'on souhaite utiliser :

Fichier principal
Sélectionnez
#[cfg(target_os = "linux")]
mod platform {
    #[link(name = "nom_de_la_bibliotheque")] extern {}
}

Dans le cas présent j'ai mis linux, mais sachez que vous pouvez aussi mettre win32 ou macos. Il est aussi possible de préciser l'architecture de cette façon :

 
Sélectionnez
#[cfg(target_os = "linux")]
mod platform {
    #[cfg(target_arch="x86")]
    #[link(name = "nom_de_la_bibliotheque_en_32_bits")] extern{}
    #[cfg(target_arch="x86_64")]
    #[link(name = "nom_de_la_bibliotheque_en_64_bits")] extern{}
}

Nous avons donc maintenant les bases.

III-A-3. Interfacer les fonctions

Tout comme je vous l'ai montré précédemment, il va falloir redéclarer les fonctions que vous souhaitez utiliser. Il est recommandé de les déclarer C dans un fichier ffi.rs (c'est ce qui généralement fait). FFI signifie « Foreign Function Interface » (ce qui peut se traduire par « Interface de Fonctions Étrangères »). Vous allez aussi enfin voir l'utilité des structures unitaires !

On va dire que la bibliothèque en C ressemble à ça :

 
Sélectionnez
#define NOT_OK 0
#define OK 1

struct Handler; // on ne sait pas ce que la structure contient

Handler *new();
int do_something(Handler *h);
int add_callback(Handler *h, int (*pointeur_sur_fonction)(int, int););
void destroy(Handler *h);

Nous devons écrire son équivalent en Rust, ce que nous allons faire dans le fichier ffi.rs :

 
Sélectionnez
use libc::{c_int, c_void, c_char};

enum Status {
    NotOk = 0,
    Ok = 1
}

#[repr(C)] // cette metadata n'est pas obligatoire mais c'est "mieux" de la mettre
pub struct FFIHandler;

extern "C" {
    pub fn new() -> *mut FFIHandler;
    pub fn do_something(handler: *mut FFIHandler) -> c_int;
    pub fn add_callback(handler: *mut FFIHandler, fonction: *mut c_void) -> c_int;
    pub fn set_name(handler: *mut FFIHandler, name: *const c_char);
    pub fn get_name(handler: *mut FFIHandler) -> *const c_char;
    pub fn destroy(handler: *mut FFIHandler);
}

Voilà pour les déclarations du code C. Nous pouvons attaquer le portage à proprement parler. Comme l'objet que l'on va binder s'appelle Handler, on va garder le nom en Rust.

handler.rs
Sélectionnez
use libc::{c_int, c_void, c_char};
use ffi::{self, FFIHandler};

pub struct Handler {
    pointer: *mut FFIHandler
}

impl Handler {
    pub fn new() -> Result<Handler, ()> {
        let tmp = unsafe { ffi::new() };

        if tmp.is_null() {
            Ok(Handler { pointer : tmp })
        } else {
            Err(())
        }
    }

    pub fn do_something(&self) -> Status {
        unsafe { ffi::do_something(self.pointer) }
    }

    pub fn add_callback(&self, fonction: fn(isize, isize) -> isize) -> Status {
        unsafe { ffi::add_callback(self.pointer, fonction as *mut c_void) }
    }

    pub fn set_name(&self, name: &str) {
        unsafe { ffi::set_name(self.pointer, name.as_ptr() as *const c_char) }
    }

    pub fn get_name(&self) -> String {
        let tmp unsafe { ffi::get_name(self.pointer) };

        if tmp.is_null() {
            String::new()
        } else {
            unsafe { String::from_utf8_lossy(::std::ffi::CStr::from_ptr(tmp).to_bytes()).to_string() }
        }
    }
}

impl Drop for Handler {
    fn drop(&mut self) {
        if !self.pointer.is_null() {
            unsafe { ffi::destroy(self.pointer); }
            self.pointer = ::std::ptr::null_mut();
        }
    }
}

Voilà, vous devriez maintenant pouvoir vous en sortir avec ces bases. Nous avons vu là comment ajouter un callback, convertir une String entre C et Rust et nous avons surtout vu à quoi servaient les structures unitaires !

III-B. Documentation et rustdoc

En plus du compilateur, Rust possède un générateur de documentation. Toute la documentation en ligne de l'API (disponible ici) a été générée avec cet outil. Vous allez voir qu'il est très facile à mettre en œuvre.

III-B-1. Génération de la documentation

Commençons par le commencement : la génération. Si vous utilisez Cargo, rien de plus simple :

 
Sélectionnez
> cargo doc

Et c'est tout. Votre documentation se trouvera dans le dossier ./target/doc/le_nom_de_votre_programme/. Pour l'afficher, ouvrez le fichier index.html qui s'y trouve avec votre navigateur Internet préféré.

Maintenant si vous souhaitez le faire sans passer par Cargo :

 
Sélectionnez
> rustdoc le_nom_de_votre_fichier_source

Le contenu sera généré dans le dossier ./doc/. Pour consulter la documentation générée, c'est pareil que pour Cargo.

III-B-2. Ajouter des explications

Pour le moment, la documentation que je vous ai fait générer ne contient que du code, sans explication. Pas tip top donc. Au final, ce serait bien qu'on ait une explication, comme ici :

Image non disponible

Pour cela, rien de plus simple, il suffit d'utiliser les « /// » :

 
Sélectionnez
/// Et ici je mets la description
/// que je veux !
fn une_fonction() {}

/// Et le markdown aussi fonctionne :
/// 
/// ```
/// println!("quelque chose");
/// // ou même un exemple d'utilisation de la structure !
/// ```
struct UneStruct {
    /// ce champ sert à faire ceci
    un_champs: 32,
    /// et ce champ sert à faire cela
    un_autre_champs: i32
}

Je vous invite maintenant à essayer cela sur vos codes pour voir le résultat obtenu. Il est cependant important de noter que les « /// » doivent être mis avant l'objet qu'ils doivent décrire. Ce code ne fonctionnera pas :

 
Sélectionnez
enum Option<T> {
    None, /// No value
    Some(T), /// Some value `T`
}

Voilà pour les bases. Il existe encore un autre niveau de commentaire qui sert à décrire le contenu d'un module, le « //! » ou « /*! ». Il doit être mis avant que le code du module ne commence et ne peut être mis qu'une seule fois (par module). Cela donne :

Image non disponible

Petit exemple rapide :

 
Sélectionnez
// copyright
// blablabla

//! Ce module fait ci.
//! Il fait aussi ça.
//!
//! #Titre
//! blabla
//! etc.

// du code...
pub mod un_module {
    //! Encore un module !
    //! Who dares summon the Rust documentation maker ?!
}

Si vous êtes un peu fainéant, vous pouvez aussi l'écrire de cette façon :

 
Sélectionnez
// copyright
// blablabla

/*!
Ce module fait ci.
Il fait aussi ça.

#Titre
blabla
etc.
!*/

Cependant, il est plus rare de la voir dans les codes.

Voilà, vous savez maintenant gérer des documentations en Rust !

III-C. Ajouter des tests

Ce chapitre parlera des tests et de la métadonnée #[test].
En Rust, il est possible d'écrire des tests unitaires directement dans un fichier qui peuvent être lancés directement par Cargo ou le compilateur de Rust.

Avec Cargo :

 
Sélectionnez
> cargo test

Avec rustc :

 
Sélectionnez
> rustc --test votre_fichier_principal.rs
> ./votre_fichier_principal

Regardons maintenant comment créer ces tests unitaires :

III-C-1. La métadonnée #[test]

Pour indiquer au compilateur que cette fonction est un test unique, il faut ajouter #[test]. Exemple :

 
Sélectionnez
fn some_func(valeur1: i32, valeur2: i32) -> i32 {
    valeur1 + valeur2
}

#[test]
fn test_some_func() {
    assert_eq!(3, some_func(1, 2));
}

Plutôt facile, non ? Vous pouvez aussi mettre cette balise sur un module :

 
Sélectionnez
fn some_func(valeur1: i32, valeur2: i32) -> i32 {
    valeur1 + valeur2
}

#[cfg(test)] // on compile si jamais on est en mode "test"
mod tests {
    use super::some_func;

    #[test] // on doit le remettre pour bien spécifier au compilateur que c'est un test
    fn test_some_func() {
        assert_eq!(3, some_func(1, 2));
    }
}

Ça permet de découper un peu le code.

III-C-2. La métadonnée #[should_panic]

Maintenant, si vous voulez vérifier qu'un test échoue, il vous faudra utiliser cette balise :

 
Sélectionnez
fn some_func(valeur1: i32, valeur2: i32) -> i32 {
    valeur1 + valeur2
}

#[test] // c'est un test
#[should_panic] // il est censé paniquer
fn test_some_func() {
    assert_eq!(4, some_func(1, 2)); // 1 + 2 != 4, donc ça doit paniquer
}

Quand vous lancerez l'exécutable, il vous confirmera que le test s'est bien déroulé (parce qu'il a paniqué). Petit bonus : vous pouvez ajouter du texte qui sera affiché lors de l'exécution du test :

 
Sélectionnez
#[test]
#[should_panic(expected = "1 + 2 != 4")]
fn test_some_func() {
    assert_eq!(4, some_func(1, 2));
}

III-C-3. Mettre les tests dans leur propre dossier

Il est aussi possible d'écrire des tests dans un dossier à part. Commencez par créer un dossier tests puis créez un fichier .rs.

Dans ce fichier, il vous faudra importer votre bibliothèque pour pouvoir tester ses fonctions :

 
Sélectionnez
extern crate ma_lib;

#[test]
fn test_some_func() {
    assert_eq!(3, ma_lib::some_func(1, 2));
}

Et voilà ! Encore une fois, rien de bien compliqué.

III-D. Rc et RefCell

Ce chapitre va vous permettre de comprendre encore un peu plus le fonctionnement du borrow-checker de Rust au travers des types RefCell et Rc.

III-D-1. RefCell

Les RefCell sont utiles pour garder un accès mutable sur un objet. Le « borrowing » est alors vérifié au runtime plutôt qu'à la compilation. Imaginons que vous vouliez dessiner une fenêtre contenant plusieurs vues. Ces vues seront mises dans un layout pour faciliter leur agencement dans la fenêtre. Seulement, on ne peut pas s'amuser à créer un vecteur contenant une liste de références mutables sur un objet, ça ne serait pas pratique du tout !

 
Sélectionnez
struct Position {
    x: i32,
    y: i32,
}

impl Position {
    pub fn new() -> Position {
        Position {
            x: 0,
            y: 0,
        }
    }
}

struct Vue {
    pos: Position,
    // plein d'autres champs
}

struct Layout {
    vues: Vec<&mut Vue>,
    layouts: Vec<&mut Layout>,
    pos: Position,
}

impl Layout {
    pub fn update(&mut self) {
        for vue in self.vues {
            vue.pos.x += 1;
        }
        for layout in self.layouts {
            layout.update();
        }
    }
}

fn main() {
    let mut vue1 = Vue { pos: Position::new() };
    let mut vue2 = Vue { pos: Position::new() };
    let mut lay1 = Layout { vues: vec!(), layouts: vec!(), pos: Position::new() };
    let mut lay2 = Layout { vues: vec!(), layouts: vec!(), pos: Position::new() };

    lay1.vues.push(&mut vue1);
    lay2.layouts.push(&mut lay1);
    lay2.vues.push(&mut vue2);
    lay2.update();
}

Si on compile le code précédent, on obtient :

 
Sélectionnez
<anon>:23:15: 23:23 error: missing lifetime specifier [E0106]
<anon>:23     vues: Vec<&mut Vue>,
                        ^~~~~~~~
<anon>:23:15: 23:23 help: see the detailed explanation for E0106
<anon>:24:18: 24:29 error: missing lifetime specifier [E0106]
<anon>:24     layouts: Vec<&mut Layout>,
                           ^~~~~~~~~~~
<anon>:24:18: 24:29 help: see the detailed explanation for E0106
error: aborting due to 2 previous errors

« Arg ! Des lifetimes ! »

En effet. Et réussir à faire tourner ce code sans souci va vite devenir très problématique ! C'est donc là qu'intervient RefCell. Il permet de « balader » une référence mutable et de ne la récupérer que lorsque l'on en a besoin avec les méthodes borrow et borrow_mut. Exemple :

 
Sélectionnez
use std::cell::RefCell;

struct Position {
    x: i32,
    y: i32,
}

impl Position {
    pub fn new() -> Position {
        Position {
            x: 0,
            y: 0,
        }
    }
}

struct Vue {
    pos: Position,
    // plein d'autres champs
}

struct Layout {
    vues: Vec<RefCell<Vue>>,
    layouts: Vec<RefCell<Layout>>,
    pos: Position,
}

impl Layout {
    pub fn update(&mut self) {
        for vue in &mut self.vues { // nous voulons &mut Vue et pas juste Vue
            vue.borrow_mut().pos.x += 1;
        }
        for layout in &mut self.layouts { // pareil que pour la boucle précédente
            layout.borrow_mut().update();
        }
    }
}

fn main() {
    let mut vue1 = Vue { pos: Position::new() };
    let mut vue2 = Vue { pos: Position::new() };
    let mut lay1 = Layout { vues: vec!(), layouts: vec!(), pos: Position::new() };
    let mut lay2 = Layout { vues: vec!(), layouts: vec!(), pos: Position::new() };

    lay1.vues.push(RefCell::new(vue1));
    lay2.layouts.push(RefCell::new(lay1));
    lay2.vues.push(RefCell::new(vue2));
    lay2.update();
}

III-D-2. Rc

Pour faire simple, le type Rc est un compteur de référence d'un objet constant. Exemple :

 
Sélectionnez
use std::rc::Rc;

let r = Rc::new(5);
println!("{}", *r);

Juste là, rien de problématique. Maintenant, que se passe-t-il si on clone ce Rc ?

 
Sélectionnez
use std::rc::Rc;

let r = Rc::new(5);
let r2 = r.clone();
println!("{}", *r2);

Rien de particulier, r et r2 pointent vers la même valeur. Et si on modifie la valeur de l'un des deux ?

 
Sélectionnez
let mut r = Rc::new(5);
println!("{:?} = {}", (&*r) as *const i32, *r);
let r2 = r.clone();
*Rc::make_mut(&mut r) = 10;
println!("{:?} = {}\n{:?} = {}", (&*r2) as *const i32, *r2, (&*r) as *const i32, *r);

Comme vous vous en serez rendu compte, l'objet contenu par r a changé. Pour éviter qu'une copie soit faite lorsque vous manipulez un type, il vous faudra passer par les types Cell ou RefCell. Cela pourra vous être très utile si vous avez des soucis avec des closures.

III-E. Les threads

Commençons par un exemple tout bête :

 
Sélectionnez
use std::thread;

fn main() {
    // on lance le thread
    let handle = thread::spawn(|| {
        "Salutations depuis un thread !"
    });

    // on attend que le thread termine son travail avant de quitter
    handle.join().unwrap();
}

La fonction thread::spawn exécute le code de la closure dans un nouveau thread. On appelle ensuite la méthode JoinHandle::join pour attendre la fin de l'exécution du thread.

Jusque là, on reste dans le classique. Que peut bien apporter Rust ici ? Hé bien essayons maintenant de partager des variables entre les threads :

 
Sélectionnez
let mut data = vec![1u32, 2, 3];

for i in 0..3 {
    // on lance le thread
    thread::spawn(move || {
        data[i] += 1;
    });
}

// on attend 50 millisecondes, le temps que les threads finissent leur travail
thread::sleep_ms(50);

Vous devriez obtenir une magnifique erreur :

 
Sélectionnez
error: capture of moved value: `data`
        data[i] += 1;

Le système de propriété que vous haïssez sans doute rentre ici aussi en jeu. Nous avons trois références mutables sur un même objet et Rust ne le permet pas, c'est aussi simple que ça. Pour contourner ce problème, plusieurs solutions s'offrent à vous :

III-E-1. Mutex

Le type Mutex permet donc d'échanger des informations entre threads. Une solution naïve serait de les utiliser de cette façon :

 
Sélectionnez
use std::thread;
use std::sync::Mutex;

fn main() {
    let mut data = Mutex::new(vec![1u32, 2, 3]); // on crée notre mutex

    for i in 0..3 {
        let data = data.lock().unwrap(); // on lock
        // on lance le thread
        thread::spawn(move || {
            data[i] += 1;
        });
    }

    // on attend 50 millisecondes, le temps que les threads finissent leur travail
    thread::sleep_ms(50);
}

Cependant nous tombons sur un autre problème :

 
Sélectionnez
<anon>:9:9: 9:22 error: the trait `core::marker::Send` is not implemented for the type `std::sync::mutex::MutexGuard<'_, collections::vec::Vec<u32>>` [E0277]
<anon>:11         thread::spawn(move || {
                  ^~~~~~~~~~~~~
<anon>:9:9: 9:22 note: `std::sync::mutex::MutexGuard<'_, collections::vec::Vec<u32>>` cannot be sent between threads safely
<anon>:11         thread::spawn(move || {
                  ^~~~~~~~~~~~~

Le trait Sync n'est pas implémenté sur le type MutexGuard retourné par la méthode Mutex::lock, impossible d'utiliser les données partagées de manière sûre ! C'est ici que rentre en jeu le type Arc !

III-E-2. Arc

Vous l'aurez deviné (ou peut-être pas), le type Arc est le même type que Rc mais thread-safe, car il implémente le trait Sync. Corrigeons le code précédent :

 
Sélectionnez
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // on crée notre mutex
    let data = Arc::new(Mutex::new(vec![1u32, 2, 3]));

    for i in 0..3 {
        // on incrémente le compteur interne de Arc
        let data = data.clone();
        thread::spawn(move || {
            let mut ret = data.lock(); // on lock

            // on vérifie qu'il n'y a pas de problème
            match ret {
                Ok(ref mut d) => {
                    // tout est bon, on peut modifier la donnée en toute sécurité !
                    d[i] += 1;
                },
                Err(e) => {
                    // une erreur s'est produite
                    println!("Impossible d'accéder aux données {:?}", e);
                }
            }
        });
    }

    // on attend 50 millisecondes, le temps que les threads finissent leur travail
    thread::sleep_ms(50);
}

Nous avons vu comment partager des données entre threads, mais il nous reste cette ligne dont on voudrait bien se débarrasser :

 
Sélectionnez
thread::sleep_ms(50);

les channels sont la solution à notre problème !

III-E-3. Les channels

Nous aimerions donc bien pouvoir continuer l'exécution de notre programme, mais uniquement après que les threads aient terminé. On crée un channel via la fonction mpsc::channel. Exemple :

 
Sélectionnez
use std::sync::{Arc, Mutex};
use std::thread;
use std::sync::mpsc;

fn main() {
    let data = Arc::new(Mutex::new(0u32));

    // on crée le channel
    let (tx, rx) = mpsc::channel();

    for _ in 0..10 {
        let (data, tx) = (data.clone(), tx.clone());

        thread::spawn(move || {
            let mut data = data.lock().unwrap();
            *data += 1;

            // on envoie le signal de fin du thread
            tx.send(());
        });
    }

    for _ in 0..10 {
        // on attend le signal de fin du thread
        rx.recv();
    }
}

Dans ce code, on crée 10 threads qui vont chacun envoyer une donnée dans le channel avant de se terminer. Il nous suffit donc d'attendre d'avoir reçu 10 données pour savoir que tous les threads se sont terminés.

Dans le code que je viens de vous montrer, on ne s'en sert que comme d'un signal en envoyant des données vides. Il est cependant possible d'envoyer des données, du moment qu'elles implémentent le trait Send. Exemple :

 
Sélectionnez
use std::thread;
use std::sync::mpsc;

fn main() {
    // on crée le channel
    let (tx, rx) = mpsc::channel();

    for _ in 0..10 {
        let tx = tx.clone();

        thread::spawn(move || {
            let answer = 42u32;

            // on envoie la donnée dans le channel
            tx.send(answer);
        });
    }

    match rx.recv() {
        Ok(data) => println!("Le channel vient de recevoir : {}", data),
        Err(e) => println!("Une erreur s'est produite : {:?}", e)
    };
}

Et voilà ! Il est important de noter que seule la méthode send est non bloquante. Si vous souhaitez ne pas attendre que des données soient disponibles, il vous faudra utiliser la méthode try_recv.

III-E-4. Utilisation détournée

Il est possible d'utiliser un thread pour isoler du code de cette façon :

 
Sélectionnez
use std::thread;

match thread::spawn(move || {
    panic!("oops!");
}).join() {
    Ok(_) => println!("Tout s'est bien déroulé"),
    Err(e) => println!("Le thread a planté ! Erreur : {:?}", e)
};

Magique !

III-E-5. Empoisonnement de Mutex

Vous savez maintenant comment partager les données de manière sûre entre des threads. Il reste cependant un petit détail à savoir concernant les mutex : si jamais un thread panic! alors qu'il a le lock, le Mutex sera « empoisonné ».

 
Sélectionnez
use std::sync::{Arc, Mutex};
use std::thread;

let lock = Arc::new(Mutex::new(0_u32));
let lock2 = lock.clone();

let _ = thread::spawn(move || -> () {
    // On lock
    let _lock = lock2.lock().unwrap();

    // On lance un panic! alors que le mutex est toujours locké
    panic!();
}).join();

Et maintenant vous vous retrouvez dans l'incapacité de lock de nouveau le Mutex dans les autres threads. Il est toutefois possible de « désempoisonner » le mutex :

 
Sélectionnez
let mut guard = match lock.lock() {
    Ok(guard) => guard,
    // on récupère les données malgré le fait que le mutex soit lock
    Err(poisoned) => poisoned.into_inner(),
};

*guard += 1;

III-F. Le réseau

Je présenterai ici surtout tout ce qui a attrait à des échanges réseaux en mode « connecté » plus simplement appelé TCP. Je pense qu'après ça, vous serez tout à fait en mesure d'utiliser d'autres protocoles réseau comme l'UDP (qui est un mode « non connecté ») sans trop de problèmes.

Commençons par écrire le code d'un client :

III-F-1. Le client

Pour le moment, je vais vous demander de tenter de comprendre le code suivant :

 
Sélectionnez
use std::net::TcpStream;

fn main() {
    println!("Tentative de connexion au serveur...");
    match TcpStream::connect("127.0.0.1:1234") {
        Ok(_) => {
            println!("Connexion au serveur réussie !");
        }
        Err(e) => {
            println!("La connexion au serveur a échoué : {}", e);
        }
    }
}

Si vous exécutez ce code, vous devriez obtenir l'erreur « Connection refused ». Cela signifie tout simplement qu'aucun serveur n'a accepté notre demande (ce qui est normal puisqu'aucun serveur n'écoute normalement sur ce port).

Je pense que ce code peut se passer de commentaire. L'objet intéressant ici est TcpStream qui permet de lire et écrire sur un flux réseau. Il implémente les traits Read et Write, donc n'hésitez pas à regarder ce qu'ils offrent !

Concernant la méthode (statique !) connect, elle prend en paramètre un objet implémentant le trait ToSocketAddrs. Les exemples de la documentation vous montrent les différentes façons d'utiliser la méthode connect, mais je vous les remets :

 
Sélectionnez
let ip = Ipv4Addr::new(127, 0, 0, 1);
let port = 1234;

let tcp_s = TcpStream::connect(SocketAddrV4::new(ip, port));
let tcp_s = TcpStream::connect((ip, port));
let tcp_s = TcpStream::connect(("127.0.0.1", port));
let tcp_s = TcpStream::connect(("localhost", port));
let tcp_s = TcpStream::connect("127.0.0.1:1234");
let tcp_s = TcpStream::connect("localhost:1234");

Il est important de noter que « localhost » est la même chose que « 127.0.0.1 ». Nous savons donc maintenant comment nous connecter à un serveur.

III-F-2. Le serveur

Voici maintenant le code du serveur :

 
Sélectionnez
use std::net::TcpListener;

fn main() {
    let listener = TcpListener::bind("127.0.0.1:1234").unwrap();

    println!("En attente d'un client...");
    match listener.accept() {
        Ok((client, addr)) => {
            println!("Nouveau client [adresse : {}]", addr);
        }
        _ => {
            println!("Un client a tenté de se connecter...")
        }
    }
}

L'objet TcpListener permet de « se mettre en écoute » sur un port donné. La méthode (statique encore une fois !) bind spécifie l'adresse (et surtout le port) sur lequel on « écoute ». Elle prend le même type de paramètre que la fonction connect. Il ne reste ensuite plus qu'à attendre la connexion d'un client avec la méthode accept. En cas de réussite, elle renvoie un tuple contenant un TcpStream et un SocketAddr (l'adresse du client).

Pour tester, lancez d'abord le serveur puis le client. Vous devriez obtenir cet affichage :

 
Sélectionnez
> ./server
En attente d′un client...
Nouveau client [adresse : 127.0.0.1:38028]
 
Sélectionnez
> ./client
Tentative de connexion au serveur...
Connexion au serveur réussie !

III-F-3. Multiclient

Gérer un seul client, c'est bien, mais qu'en est-il si on veut en gérer plusieurs ? Hé bien il vous suffit de boucler sur l'appel de la méthode accept et de gérer chaque client dans un thread. Rust fournit aussi la méthode incoming qui permet de gérer cela un peu plus élégamment :

 
Sélectionnez
let listener = TcpListener::bind("127.0.0.1:1234").unwrap();

println!("En attente d'un client...");
for stream in listener.incoming() {
    match stream {
        Ok(stream) => {
            let adresse = match stream.peer_addr() {
                Ok(addr) => format!("[adresse : {}]", addr),
                Err(_) => "inconnue".to_owned()
            };

            println!("Nouveau client {}", adresse);
        }
        Err(e) => {
            println!("La connexion du client a échoué : {}", e);
        }
    }
    println!("En attente d'un autre client...");
}

Pas beaucoup de changement donc. Maintenant comment pourrait-on faire pour gérer plusieurs clients en même temps ? Les threads semblent être une solution acceptable :

 
Sélectionnez
use std::net::{TcpListener, TcpStream};
use std::thread;

fn handle_client(mut stream: TcpStream) {
    // mettre le code de gestion du client ici
}

fn main() {
    let listener = TcpListener::bind("127.0.0.1:1234").unwrap();

    println!("En attente d'un client...");
    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                let adresse = match stream.peer_addr() {
                    Ok(addr) => format!("[adresse : {}]", addr),
                    Err(_) => "inconnue".to_owned()
                };

                println!("Nouveau client {}", adresse);
                thread::spawn(move|| {
                    handle_client(stream)
                });
            }
            Err(e) => {
                println!("La connexion du client a échoué : {}", e);
            }
        }
        println!("En attente d'un autre client...");
    }
}

Rien de bien nouveau.

III-F-4. Gérer la perte de connexion

Épineux problème que voilà ! Comment savoir si le client/serveur auquel vous envoyez des messages est toujours connecté ? Le moyen le plus simple est de lire sur le flux. Il y a alors deux cas :

  • une erreur est retournée ;
  • pas d'erreur, mais le nombre d'octets lus est égal à 0.

À vous de bien gérer ça en vérifiant bien à chaque lecture si tout est ok.

III-F-5. Exemple d'échange de message entre un serveur et un client

Le code qui va suivre permet juste de recevoir un message et d'en renvoyer un. Cela pourra peut-être vous donner des idées pour la suite :

Code complet du serveur :

 
Sélectionnez
use std::net::{TcpListener, TcpStream};
use std::io::{Read, Write};
use std::thread;

fn handle_client(mut stream: TcpStream, adresse: &str) {
    let mut msg : Vec<u8> = Vec::new();
    loop {
        let mut buf = &mut [0; 10];

        match stream.read(buf) {
            Ok(received) => {
                // si on a reçu 0 octet, ça veut dire que le client s'est déconnecté
                if received < 1 {
                    println!("Client disconnected {}", adresse);
                    return;
                }
                let mut x = 0;

                for c in buf {
                    // si on a dépassé le nombre d'octets reçus, inutile de continuer
                    if x >= received {
                        break;
                    }
                    x += 1;
                    if *c == '\n' as u8 {
                        println!("message reçu {} : {}",
                            adresse,
                            // on convertit maintenant notre buffer en String
                            String::from_utf8(msg).unwrap()
                        );
                        stream.write(b"ok\n");
                        msg = Vec::new();
                    } else {
                        msg.push(*c);
                    }
                }
            }
            Err(_) => {
                println!("Client disconnected {}", adresse);
                return;
            }
        }
    }
}

fn main() {
    let listener = TcpListener::bind("127.0.0.1:1234").unwrap();

    println!("En attente d'un client...");
    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                let adresse = match stream.peer_addr() {
                    Ok(addr) => format!("[adresse : {}]", addr),
                    Err(_) => "inconnue".to_owned()
                };

                println!("Nouveau client {}", adresse);
                thread::spawn(move|| {
                    handle_client(stream, &*adresse)
                });
            }
            Err(e) => {
                println!("La connexion du client a échoué : {}", e);
            }
        }
        println!("En attente d'un autre client...");
    }
}

Code complet du client :

 
Sélectionnez
use std::net::TcpStream;
use std::io::{Write, Read, stdin, stdout};

fn get_entry() -> String {
    let mut buf = String::new();

    stdin().read_line(&mut buf);
    buf.replace("\n", "").replace("\r", "")
}

fn exchange_with_server(mut stream: TcpStream) {
    let stdout = std::io::stdout();
    let mut io = stdout.lock();
    let mut buf = &mut [0; 3];

    println!("Enter 'quit' when you want to leave");
    loop {
        write!(io, "> ");
        // pour afficher de suite
        io.flush();
        match &*get_entry() {
            "quit" => {
                println!("bye !");
                return;
            }
            line => {
                write!(stream, "{}\n", line);
                match stream.read(buf) {
                    Ok(received) => {
                        if received < 1 {
                            println!("Perte de la connexion avec le serveur");
                            return;
                        }
                    }
                    Err(_) => {
                        println!("Perte de la connexion avec le serveur");
                        return;
                    }
                }
                println!("Réponse du serveur : {:?}", buf);
            }
        }
    }
}

fn main() {
    println!("Tentative de connexion au serveur...");
    match TcpStream::connect("127.0.0.1:1234") {
        Ok(stream) => {
            println!("Connexion au serveur réussie !");
            exchange_with_server(stream);
        }
        Err(e) => {
            println!("La connexion au serveur a échoué : {}", e);
        }
    }
}

Voilà ce que ça donne :

 
Sélectionnez
> ./server 
En attente d′un client...
Nouveau client [adresse : 127.0.0.1:41111]
En attente d′un autre client...
message reçu [adresse : 127.0.0.1:41111] : salutations !
message reçu [adresse : 127.0.0.1:41111] : tout fonctionne ?
 
Sélectionnez
> ./client 
Tentative de connexion au serveur...
Connexion au serveur réussie !
Entrez 'quit' quand vous voulez fermer ce programme
> salutations !
Réponse du serveur : [111, 107, 10]
> tout fonctionne ?
Réponse du serveur : [111, 107, 10]

Si vous avez bien compris ce chapitre (ainsi que les précédents), vous ne devriez avoir aucun mal à comprendre ces deux codes. En espérant que cette introduction au réseau en Rust vous aura plu !

III-G. Codes annexes

Cette section n'a pas réellement d'intérêt si ce n'est montrer quelques fonctionnalités ou comportements que j'ai trouvés intéressants.

III-G-1. Écrire des nombres différemment

 
Sélectionnez
let a = 0_0;
let b = 0--0_0--0;
let c = 0-!0_0-!0;
let d = 0xdeadbeef;
let e = 0x_a_bad_1dea_u64;

On peut aussi se servir du _ pour faciliter la lecture des nombres :

 
Sélectionnez
let a = 12_u32;
let b = 1_000_000;

III-G-2. Toujours plus de parenthèses !

 
Sélectionnez
fn tmp() -> Box<FnMut() -> Box<FnMut() -> Box<FnMut() -> Box<FnMut(i32) -> i32>>>> {
    Box::new(|| { Box::new(|| { Box::new(|| { Box::new(|a| { 2 * a }) }) }) })
}

fn main() {
    println!("{}", tmp()()()()(1));
}

III-G-3. Utiliser la méthode d'un trait

Vous savez qu'il est possible de définir une méthode dans un trait, mais qu'on est forcé d'implémenter ce trait pour pouvoir l'appeler. Hé bien voici une méthode pour contourner cette limitation :

 
Sélectionnez
trait T {
}

impl<'a> T + 'a {
    fn yop() {
        println!("yop");
    }
}

fn main() {
    T::yop()
}

III-G-4. Toujours plus vers le fonctionnel avec le slice pattern!

J'ai trouvé cette fonctionnalité assez sympa (mais encore instable pour le moment) donc je la mets ici :

 
Sélectionnez
#![feature(slice_patterns)]

fn sum(values: &[i32]) -> i32 {
    match values {
        [head, tail..] => head + sum(tail),
        [] => 0,
    }
}

fn main() {
    println!("Sum: {}", sum(&[1, 2, 3, 4]));
}

Remerciements

Je tiens à remercier Winjerome pour la gabarisation et f-leb pour la relecture orthographique.


précédentsommaire

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2016 Guillaume Gomez. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.