Fondamentaux du multiplexage
Le cœur de NIO réside dans sa capacité à surveiller simultanément de nombreux canaux. Un Selector agit comme un orchestrateur central qui détecte les événements prêts à être traités sur des canaux enregistrés, éliminant ainsi le besoin d'un thread par connexion.
Mise en place du sélecteur
La première étape consiste à instancier un sélecteur et à y associer un canal serveur configuré en mode non-bloquant. Ce canal écoute les demandes de connexion sur un port spécifique sans jamais bloquer le thread principal. Initialement, le sélecteur ne surveille que ce canal d'écoute, attendant les premières sollicitations clients.
Élimination des blocages opérationnels
Acceptation des connexions en mode réactif
Contrairement au modèle traditionnel où accept() suspend l'exécution jusqu'à l'arrivée d'un client, l'approche NIO enregistre le canal serveur avec l'opération OP_ACCEPT. Le sélecteur n'active le traitement que lorsqu'une demande de connexion est effectivement présente, grâce à son mécanisme de sondage événementiel.
La méthode select() offre trois modes de fonctionnement :
- Bloquant : attend indéfiniment un événement
- Bloquant avec timeout : retourne après un délai maximal ou à la détection d'un événement
- Non-bloquant (
selectNow()) : retourne immédiatement avec le nombre d'événements prêts
Cette stratégie assure une utilisation optimale des ressources : le système reste inactif en l'absence de travail, mais répond instantanément dès qu'une opération est disponible. C'est la différence fondamentale avec l'IO bloquant où chaque connexion occuperait un thread en attente.
Transfert de données sans attente passive
Une fois la connexion établie, chaque SocketChannel généré hérite également du mode non-bloquant grâce à configureBlocking(false). Lors d'une opération de lecture, si aucune donnée n'est disponible, l'appel retourne immédiatement sans consommer de cycles CPU inutilement. Ce comportement est conditionné par l'exécution dans une boucle d'événements continue qui re-soumet les canaux au sélecteur après chaque traitement.
Limites du modèle : les points de blocage résiduels
Malgré ses avantages, le sélecteur lui-même peut bloquer dans deux circonstances :
- Pendant l'attente d'événements via
select() - Lors du traitement des opérations I/O détectées
Toutefois, ce blocage s'applique collectivement à tous les canaux surveillés, maintenant l'efficacité du modèle à grande échelle sans coût de commutation de contexte ni consommation excessive de mémoire thread.
Analogie fonctionnelle
Considérez le sélecteur comme un gardien surveillant un hall d'entrée avec de nombreuses portes. Initialement, une seule porte principale (ServerSocketChannel) est observée. Quand un visiteur (client) frappe, le gardien reçoit une clé d'événement, ouvre une nouvelle porte de dialogue (SocketChannel) et peut maintenant surveiller à la fois les nouvelles arrivées et les conversations existantes. Ces paires de canaux créées représentent l'abstraction Java de la connexion TCP sous-jacente, fournissant une API simplifiée pour des opérations complexes.
Types d'événements surveillés
Le masque d'intérêt d'une SelectionKey définit les opérations surveillées :
OP_ACCEPT: Demande de connexion entrante prête à être acceptéeOP_CONNECT: Confirmation de connexion côté client établieOP_READ: Données disponibles dans le canal pour lecture immédiateOP_WRITE: Canal prêt à accepter des données en écriture
Code d'exemple complet
Implémentation côté serveur
public class ServeurMultiplexe {
private static final int TAILLE_TAMPON = 1024;
public static void main(String[] args) throws IOException {
Selector multiplexeur = Selector.open();
ServerSocketChannel canalEcoute = ServerSocketChannel.open();
canalEcoute.configureBlocking(false);
canalEcoute.register(multiplexeur, SelectionKey.OP_ACCEPT);
canalEcoute.bind(new InetSocketAddress("localhost", 9999));
while (true) {
multiplexeur.select();
Iterator<SelectionKey> iterateur = multiplexeur.selectedKeys().iterator();
while (iterateur.hasNext()) {
SelectionKey cle = iterateur.next();
iterateur.remove();
if (cle.isAcceptable()) {
gererNouvelleConnexion(multiplexeur, cle);
} else if (cle.isReadable()) {
traiterLecture(cle);
}
}
}
}
private static void gererNouvelleConnexion(Selector multiplexeur, SelectionKey cle) throws IOException {
ServerSocketChannel serveur = (ServerSocketChannel) cle.channel();
SocketChannel canalClient = serveur.accept();
canalClient.configureBlocking(false);
canalClient.register(multiplexeur, SelectionKey.OP_READ);
}
private static void traiterLecture(SelectionKey cle) throws IOException {
SocketChannel canalClient = (SocketChannel) cle.channel();
ByteBuffer tampon = ByteBuffer.allocate(TAILLE_TAMPON);
StringBuilder message = new StringBuilder();
int octetsLus;
while ((octetsLus = canalClient.read(tampon)) > 0) {
tampon.flip();
message.append(StandardCharsets.UTF_8.decode(tampon));
tampon.compact();
}
if (octetsLus == -1) {
canalClient.close();
} else {
System.out.println("Message reçu: " + message);
}
}
}
Implémentation côté client
public class ClientNIO {
public static void main(String[] args) throws IOException {
SocketChannel canal = SocketChannel.open();
canal.configureBlocking(false);
canal.connect(new InetSocketAddress("localhost", 9999));
while (!canal.finishConnect()) {
// Attente active minimale jusqu'à établissement complet
}
String donnees = "Test NIO non-bloquant";
ByteBuffer tampon = ByteBuffer.wrap(donnees.getBytes(StandardCharsets.UTF_8));
canal.write(tampon);
canal.close();
}
}