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.Selector
Un 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é. SelectionKey
Chaque 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); } }