Une des difficultés que nous avons rencontrées jusqu'à présent est que, par défaut, les méthodes datagramChannel.receive() et datagramChannel.send() sont bloquantes.
Pour gérer le fait qu'il est possible de rester bloqué dans un appel à datagramChannel.receive(), nous avons utilisé des
threads.
On peut changer ce comportement et rendre non-bloquantes les deux méthodes send() et receive().
DatagramChannel dc = DatagramChannel.open(); dc.configureBlocking(false);
En mode non-bloquant, l'appel à datagramChannel.receive() retourne immédiatement même si aucun paquet n'a été reçu par le système.
buffer et l'adresse de l'expéditeur est renvoyée.null.
var dc = DatagramChannel.open();
dc.configureBlocking(false);
var sender = dc.receive(buffer);
if (sender != null){
System.out.println("Paquet reçu !");
} else {
System.out.println("Pas de paquet");
}
En non-bloquant, l'appel à datagramChannel.send() retourne immédiatement, même si on ne peut pas envoyer le paquet sans délai dans le buffer système (parce qu'il n'y a pas assez de place).
buffer sont consommées et envoyées dans le buffer système;
DatagramChannel dc = DatagramChannel.open();
dc.configureBlocking(false);
...
dc.send(buffer, sender);
if (!buffer.hasRemaining()){
System.out.println("Paquet envoyé !");
} else {
System.out.println("Le paquet n'a pas été envoyé");
}
receive bloquant ?Peut on se mettre en attente de recevoir des données lorsqu'on est en mode non-bloquant ?
var dc = DatagramChannel.open();
dc.configureBlocking(false);
var buffer = ByteBuffer.allocate(BUFFER_SIZE);
InetSocketAddress sender = null;
while(sender == null) { // Argh : attente active !!!
sender = dc.receive(buffer);
}
À ne jamais faire !!! C'est de l'attente active.
On doit utiliser un nouveau mécanisme appelé sélecteur qui va nous prévenir quand un paquet est arrivé (ou quand l'envoi est possible).
Un sélecteur est un mécanisme permettant d'être notifié lorsqu'un paquet est arrivé et/ou lorsque l'on peut envoyer un paquet.
Un sélecteur surveille un certain nombre de DatagramChannel qui sont enregistrés auprès de ce sélecteur.
Pour chaque DatagramChannel enregistré sur un sélecteur, on peut demander à être notifié de l'arrivée d'un paquet et/ou de la possibilité d'en envoyer un.
Quand on l'interroge, le sélecteur renvoie l'ensemble de tous les DatagramChannel sur lesquels au moins une opération est possible.
java.nio.channels.SelectorUn Selector est construit avec la factory Selector.open().
var selector = Selector.open();
Un Selector utilise la classe
SelectionKey pour stocker les informations relatives
à un DatagramChannel qu'il surveille.
Le Selector maintient deux ensembles de SelectionKey :
selector.keys()selector.selectedKeys(), sur lesquelles un évenement surveillé a été détecté. SelectionKeyChaque SelectionKey contient :
Channel qui correspond au DatagramChannel enregistré pour cette clé, accessible en lecture par l'accesseur selectionKey.channel() ;int qui code les opérations pour lesquelles on souhaite être notifié, consultable par l'accesseur interestOps() :
SelectionKey.OP_READ pour la réception seulementSelectionKey.OP_WRITE pour l'envoi seulementSelectionKey.OP_READ|SelectionKey.OP_WRITE pour l'envoi ET la réception.On peut modifier cette valeur avec la méthode selectionKey.interestOps(int interestOps).
Pour enregistrer un DatagramChannel auprès du sélecteur, il faut que le
DatagramChannel soit configuré en non-bloquant.
var selector = Selector.open(); var dc = DatagramChannel.open(); dc.configureBlocking(false); // enregistrement de dc auprès de selector pour la réception dc.register(selector, SelectionKey.OP_READ);
L'appel dc.register(selector, SelectionKey.OP_READ) crée une SelectionKey qui est ajoutée à l'ensemble des clés selector.keys(), pour que le sélecteur surveille les opérations de réception sur dc.
Selector (2/2)
Lorsqu'un ou des DatagramChannel sont enregistrés auprès d'un sélecteur, avec leurs opérations d'intérêt (représentées par l'ensemble selector.keys()), il suffit d'interroger le sélecteur grâce à la méthode selector.select().
On répète en boucle :
selector.select() qui place dans l'ensemble des clés sélectionnées selector.selectedKeys() les clés des DatagramChannel pour lesquels au moins une opération d'intérêt est disponible,selector.selectedKeys(),
selector.selectedKeys().
var selectedKeys = selector.selectedKeys();
while (!Thread.interrupted()) {
selector.select();
for (var key : selectedKeys) {
if (key.isValid() && key.isWritable()) {
doWrite(key);
}
if (key.isValid() && key.isReadable()) {
doRead(key);
}
}
selectedKeys.clear();
}
Le test key.isValid() && key.isWritable() est évalué à vrai si le DatagramChannel
est prêt pour un envoi de packet.
Le test key.isValid() && key.isReadable() est évalué à vrai si un paquet est prêt à être reçu sur le DatagramChannel.
Attention, même avec l'aide du sélecteur, il n'y a toujours pas de garantie que les appels à datagramChannel.send() et datagramChannel.receive() réussissent.
Depuis Java 11, Selector offre une variante de select utilisant une lambda.
Selector.select(Consumer<SelectionKey> action)
Le Consumer action est appliqué à chaque clé pour laquelle une opération est prête. On n'a plus besoin de gérer selectedKeys avec une boucle !
Et, a priori, cette méthode est plus efficace que de faire la boucle de sélection.
var selectedKeys = selector.selectedKeys();
while (!Thread.interrupted()) {
selector.select(this::treatKey);
}
private void treatKey(SelectionKey key) {
try{
if (key.isValid() && key.isWritable()) {
doWrite(key);
}
if (key.isValid() && key.isReadable()) {
doRead(key);
}
} catch (IOException e) {
...
}
}
Comment gérer les IOException ?
Il faut réfléchir à ce que signale l'exception.
Dans le cas d'un client/serveur UDP, cela signale en général une erreur irrécupérable. Il faut donc logger l'exception et la propager.
Comme on est dans une lambda, il faut la caster en une
UncheckedIOException pour pouvoir la relancer.
public void launch() throws IOException {
while(!Thread.interrupted()) {
try {
selector.select(this::treatKey);
} catch (UncheckedIOException tunneled) {
throw tunneled.getCause();
}
}
}
private void treatKey(SelectionKey key) {
try {
if (key.isValid() && key.isWritable()) {
...
}
if (key.isValid() && key.isReadable()) {
...
}
} catch (IOException ioe) {
throw new UncheckedIOException(ioe);
}
}