III. Aller plus loin
8. Le réseau
Je présenterai ici surtout tout ce qui a attrait à des échanges réseaux en mode "connecté", plus simplement appelé TCP. Vous serez ensuite tout à fait en mesure d'utiliser d'autres protocoles réseaux comme l'UDP (qui est un mode "non-connecté") sans trop de problèmes. Le code présenté sera synchrone, donc nous ne verrons pas l'asynchrone en Rust ici.
Commençons par écrire le code d'un client :
Le client
Commençons par écrire le code d'un client. Pour le moment, nous allons tenter de comprendre le code suivant :
Runuse 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 de connexion (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 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 :
Runlet 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.
Le serveur
Voici maintenant le code du serveur :
Runuse std::net::TcpListener;
fn main() {
let listener = TcpListener::bind("127.0.0.1:1234").expect("failed to bind");
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 méthode 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 :
$ ./server
En attente d'un client...
Nouveau client [adresse : 127.0.0.1:38028]
Et côté client :
$ ./client
Tentative de connexion au serveur...
Connexion au server réussie !
Multi-client
Gérer un seul client, c'est bien, mais qu'en est-il si on veut en gérer plusieurs ? Il vous suffit de boucler sur l'appel de la méthode accept et de gérer chaque client dans un thread (c'est une gestion volontairement très simplifiée d'un serveur !). Rust fournit aussi la méthode incoming qui permet de gérer cela un peu plus élégamment :
Runlet 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 changements donc. Maintenant comment pourrait-on faire pour gérer plusieurs clients en même temps ? Comme dit un peu au-dessus, les threads semblent être une solution acceptable :
Runuse 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.
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 2 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.
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 :
Runuse 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 :
Runuse 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 :
$ ./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 ?
$ ./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 !