Introduction au sélecteur Java NIO

Introduction au sélecteur Java NIO

1. Vue d'ensemble

Dans cet article, nous allons explorer les parties introductives du composantSelector de Java NIO.

Un sélecteur fournit un mécanisme permettant de surveiller un ou plusieurs canaux NIO et d'identifier le moment où un ou plusieurs d'entre eux deviennent disponibles pour le transfert de données.

De cette façon,a single thread can be used for managing multiple channels, et donc plusieurs connexions réseau.

2. Pourquoi utiliser un sélecteur?

Avec un sélecteur, nous pouvons utiliser un seul thread au lieu de plusieurs pour gérer plusieurs canaux. Context-switching between threads is expensive for the operating system, et en plus,each thread takes up memory.

Par conséquent, moins il y a de threads, mieux c'est. Cependant, il est important de se rappeler quemodern operating systems and CPU’s keep getting better at multitasking, de sorte que les frais généraux du multi-threading continuent de diminuer avec le temps.

Nous allons traiter ici de la façon dont nous pouvons gérer plusieurs canaux avec un seul thread à l'aide d'un sélecteur.

Notez également que les sélecteurs ne vous aident pas seulement à lire les données; ils peuvent également écouter les connexions réseau entrantes et écrire des données sur des canaux lents.

3. Installer

Pour utiliser le sélecteur, nous n’avons besoin d’aucune configuration spéciale. Toutes les classes dont nous avons besoin sont le package de basejava.nio et nous devons juste importer ce dont nous avons besoin.

Après cela, nous pouvons enregistrer plusieurs canaux avec un objet sélecteur. Lorsque l'activité d'E / S se produit sur l'un des canaux, le sélecteur nous en informe. C'est ainsi que nous pouvons lire un grand nombre de sources de données à partir d'un seul thread.

Tout canal que nous enregistrons avec un sélecteur doit être une sous-classe deSelectableChannel. Ce sont des types spéciaux de canaux qui peuvent être mis en mode non bloquant.

4. Créer un sélecteur

Un sélecteur peut être créé en appelant la méthode statiqueopen de la classeSelector, qui utilisera le fournisseur de sélecteurs par défaut du système pour créer un nouveau sélecteur:

Selector selector = Selector.open();

5. Enregistrement des canaux sélectionnables

Pour qu'un sélecteur puisse surveiller n'importe quel canal, nous devons enregistrer ces canaux avec le sélecteur. Nous faisons cela en appelant la méthoderegister du canal sélectionnable.

Mais avant qu'un canal soit enregistré avec un sélecteur, il doit être en mode non bloquant:

channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

Cela signifie que nous ne pouvons pas utiliserFileChannels avec un sélecteur car ils ne peuvent pas être mis en mode non bloquant comme nous le faisons avec les canaux de socket.

Le premier paramètre est l'objetSelector que nous avons créé précédemment, le second paramètre définit un ensemble d'intérêts, signifiant quels événements nous sommes intéressés à écouter dans le canal surveillé, via le sélecteur.

Nous pouvons écouter quatre événements différents, chacun étant représenté par une constante dans la classeSelectionKey:

  • Connect lorsqu'un client tente de se connecter au serveur. Représenté parSelectionKey.OP_CONNECT

  • Accept lorsque le serveur accepte une connexion d'un client. Représenté parSelectionKey.OP_ACCEPT

  • Read lorsque le serveur est prêt à lire à partir du canal. Représenté parSelectionKey.OP_READ

  • Write lorsque le serveur est prêt à écrire sur le canal. Représenté parSelectionKey.OP_WRITE

L’objet renvoyéSelectionKey représente l’enregistrement du canal sélectionnable avec le sélecteur. Nous l'examinerons plus en détail dans la section suivante.

6. L'objetSelectionKey

Comme nous l'avons vu dans la section précédente, lorsque nous enregistrons un canal avec un sélecteur, nous obtenons un objetSelectionKey. Cet objet contient des données représentant l’enregistrement du canal.

Il contient quelques propriétés importantes que nous devons bien comprendre pour pouvoir utiliser le sélecteur sur le canal. Nous examinerons ces propriétés dans les sous-sections suivantes.

6.1. L'ensemble d'intérêt

Un ensemble d'intérêts définit l'ensemble d'événements que le sélecteur doit surveiller sur ce canal. C'est une valeur entière; nous pouvons obtenir cette information de la manière suivante.

Tout d'abord, nous avons l'ensemble des intérêts retournés par la méthodeSelectionKey´sinterestOps. Ensuite, nous avons la constante d'événement enSelectionKey que nous avons examinée plus tôt.

Lorsque nous ET ces deux valeurs, nous obtenons une valeur booléenne nous indiquant si l'événement est surveillé ou non:

int interestSet = selectionKey.interestOps();

boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;

6.2. L'ensemble prêt

L'ensemble prêt définit l'ensemble des événements pour lesquels le canal est prêt. C'est aussi une valeur entière; nous pouvons obtenir cette information de la manière suivante.

Nous avons l'ensemble prêt renvoyé par la méthodereadyOps deSelectionKey. Lorsque nous ET cette valeur avec les constantes d'événements, comme nous l'avons fait dans le cas d'un ensemble d'intérêts, nous obtenons un booléen indiquant si le canal est prêt pour une valeur particulière ou non.

Une autre façon alternative et plus courte de le faire est d'utiliser les méthodes de commoditéSelectionKey's dans ce même but:

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWriteable();

6.3. La chaîne

L'accès à la chaîne en cours de visionnage à partir de l'objetSelectionKey est très simple. Nous appelons simplement la méthodechannel:

Channel channel = key.channel();

6.4. Le sélecteur

Tout comme pour obtenir un canal, il est très facile d’obtenir l’objetSelector à partir de l’objetSelectionKey:

Selector selector = key.selector();

6.5. Attacher des objets

Nous pouvons attacher un objet à unSelectionKey. Parfois, nous pouvons vouloir donner à un canal un ID personnalisé ou attacher n'importe quel type d'objet Java dont nous pouvons vouloir garder une trace.

Attacher des objets est un moyen pratique de le faire. Voici comment attacher et récupérer des objets à partir d'unSelectionKey:

key.attach(Object);

Object object = key.attachment();

Alternativement, nous pouvons choisir d'attacher un objet lors de l'enregistrement du canal. Nous l'ajoutons en tant que troisième paramètre à la méthoderegister de la chaîne, comme ceci:

SelectionKey key = channel.register(
  selector, SelectionKey.OP_ACCEPT, object);

7. Sélection des touches de canal

Jusqu'à présent, nous avons examiné comment créer un sélecteur, y enregistrer des canaux et inspecter les propriétés de l'objetSelectionKey qui représente l'enregistrement d'un canal dans un sélecteur.

Ce n’est que la moitié du processus, nous devons maintenant effectuer un processus continu de sélection de l’ensemble prêt à utiliser que nous avons examiné précédemment. Nous effectuons une sélection en utilisant la méthodeselect du sélecteur, comme ceci:

int channels = selector.select();

Cette méthode est bloquée jusqu'à ce qu'au moins un canal soit prêt pour une opération. Le nombre entier renvoyé représente le nombre de clés dont les canaux sont prêts pour une opération.

Ensuite, nous récupérons généralement l'ensemble des clés sélectionnées pour le traitement:

Set selectedKeys = selector.selectedKeys();

L'ensemble que nous avons obtenu est composé d'objetsSelectionKey, chaque clé représente un canal enregistré qui est prêt pour une opération.

Après cela, nous itérons habituellement sur cet ensemble et pour chaque clé, nous obtenons le canal et effectuons toutes les opérations qui apparaissent dans notre intérêt.

Pendant la durée de vie d'un canal, celui-ci peut être sélectionné plusieurs fois, car sa touche apparaît dans le jeu pour différents événements. C'est pourquoi nous devons avoir une boucle continue pour capturer et traiter les événements de canal au fur et à mesure qu'ils se produisent.

8. Exemple complet

Pour consolider les connaissances que nous avons acquises dans les sections précédentes, nous allons créer un exemple client-serveur complet.

Pour faciliter le test de notre code, nous allons créer un serveur d'écho et un client d'écho. Dans ce type de configuration, le client se connecte au serveur et commence à lui envoyer des messages. Le serveur renvoie les messages envoyés par chaque client.

Lorsque le serveur rencontre un message spécifique, tel queend, il l'interprète comme la fin de la communication et ferme la connexion avec le client.

8.1. Le serveur

Voici notre code pourEchoServer.java:

public class EchoServer {

    private static final String POISON_PILL = "POISON_PILL";

    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.bind(new InetSocketAddress("localhost", 5454));
        serverSocket.configureBlocking(false);
        serverSocket.register(selector, SelectionKey.OP_ACCEPT);
        ByteBuffer buffer = ByteBuffer.allocate(256);

        while (true) {
            selector.select();
            Set selectedKeys = selector.selectedKeys();
            Iterator iter = selectedKeys.iterator();
            while (iter.hasNext()) {

                SelectionKey key = iter.next();

                if (key.isAcceptable()) {
                    register(selector, serverSocket);
                }

                if (key.isReadable()) {
                    answerWithEcho(buffer, key);
                }
                iter.remove();
            }
        }
    }

    private static void answerWithEcho(ByteBuffer buffer, SelectionKey key)
      throws IOException {

        SocketChannel client = (SocketChannel) key.channel();
        client.read(buffer);
        if (new String(buffer.array()).trim().equals(POISON_PILL)) {
            client.close();
            System.out.println("Not accepting client messages anymore");
        }

        buffer.flip();
        client.write(buffer);
        buffer.clear();
    }

    private static void register(Selector selector, ServerSocketChannel serverSocket)
      throws IOException {

        SocketChannel client = serverSocket.accept();
        client.configureBlocking(false);
        client.register(selector, SelectionKey.OP_READ);
    }

    public static Process start() throws IOException, InterruptedException {
        String javaHome = System.getProperty("java.home");
        String javaBin = javaHome + File.separator + "bin" + File.separator + "java";
        String classpath = System.getProperty("java.class.path");
        String className = EchoServer.class.getCanonicalName();

        ProcessBuilder builder = new ProcessBuilder(javaBin, "-cp", classpath, className);

        return builder.start();
    }
}

C'est ce qui se passe; nous créons un objetSelector en appelant la méthode statiqueopen. Nous créons ensuite un canal également en appelant sa méthode statiqueopen, en particulier une instanceServerSocketChannel.

C'est parce queServerSocketChannel is selectable and good for a stream-oriented listening socket.

Nous le lions ensuite à un port de notre choix. Rappelez-vous, nous avons dit précédemment qu'avant d'enregistrer un canal sélectionnable sur un sélecteur, nous devons d'abord le définir en mode non bloquant. Nous faisons ensuite cela, puis enregistrons le canal auprès du sélecteur.

Nous n'avons pas besoin de l'instanceSelectionKey de cette chaîne à ce stade, nous ne nous en souviendrons donc pas.

Java NIO utilise un modèle orienté tampon autre qu'un modèle orienté flux. Ainsi, la communication de socket a généralement lieu en écrivant et en lisant dans un tampon.

Nous créons donc un nouveauByteBuffer sur lequel le serveur va écrire et lire. Nous l'initialisons à 256 octets, il s'agit simplement d'une valeur arbitraire, en fonction de la quantité de données que nous prévoyons de transférer.

Enfin, nous effectuons le processus de sélection. Nous sélectionnons les canaux prêts, récupérons leurs clés de sélection, les parcourons et effectuons les opérations pour lesquelles chaque canal est prêt.

Nous faisons cela dans une boucle infinie car les serveurs doivent généralement continuer à fonctionner, qu’il y ait une activité ou non.

La seule opération qu'unServerSocketChannel peut gérer est une opérationACCEPT. Lorsque nous acceptons la connexion d'un client, nous obtenons un objetSocketChannel sur lequel nous pouvons faire des lectures et des écritures. Nous le mettons en mode non bloquant et l'enregistrons pour une opération READ sur le sélecteur.

Lors d'une des sélections suivantes, ce nouveau canal sera prêt pour la lecture. Nous le récupérons et lisons le contenu dans le tampon. Fidèle à cela en tant que serveur d'écho, nous devons réécrire ce contenu sur le client.

When we desire to write to a buffer from which we have been reading, we must call the flip() method.

Nous avons finalement mis le tampon en mode écriture en appelant la méthodeflip et en y écrivant simplement.

La méthodestart() est définie de sorte que le serveur d'écho puisse être démarré en tant que processus séparé pendant le test unitaire.

8.2. Le client

Voici notre code pourEchoClient.java:

public class EchoClient {
    private static SocketChannel client;
    private static ByteBuffer buffer;
    private static EchoClient instance;

    public static EchoClient start() {
        if (instance == null)
            instance = new EchoClient();

        return instance;
    }

    public static void stop() throws IOException {
        client.close();
        buffer = null;
    }

    private EchoClient() {
        try {
            client = SocketChannel.open(new InetSocketAddress("localhost", 5454));
            buffer = ByteBuffer.allocate(256);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public String sendMessage(String msg) {
        buffer = ByteBuffer.wrap(msg.getBytes());
        String response = null;
        try {
            client.write(buffer);
            buffer.clear();
            client.read(buffer);
            response = new String(buffer.array()).trim();
            System.out.println("response=" + response);
            buffer.clear();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return response;

    }
}

Le client est plus simple que le serveur.

Nous utilisons un motif singleton pour l'instancier dans la méthode statique destart. Nous appelons le constructeur privé à partir de cette méthode.

Dans le constructeur privé, nous ouvrons une connexion sur le même port sur lequel le canal du serveur était lié et toujours sur le même hôte.

Nous créons ensuite un tampon dans lequel nous pouvons écrire et à partir duquel nous pouvons lire.

Enfin, nous avons une méthodesendMessage qui lit toute chaîne que nous lui transmettons dans un tampon d'octets qui est transmis sur le canal au serveur.

Nous lisons ensuite à partir du canal client pour obtenir le message envoyé par le serveur. Nous renvoyons cela comme l'écho de notre message.

8.3. Essai

Dans une classe appeléeEchoTest.java, nous allons créer un cas de test qui démarre le serveur, envoie des messages au serveur et ne réussit que lorsque les mêmes messages sont reçus du serveur. Enfin, le scénario de test arrête le serveur avant la fin.

Nous pouvons maintenant exécuter le test:

public class EchoTest {

    Process server;
    EchoClient client;

    @Before
    public void setup() throws IOException, InterruptedException {
        server = EchoServer.start();
        client = EchoClient.start();
    }

    @Test
    public void givenServerClient_whenServerEchosMessage_thenCorrect() {
        String resp1 = client.sendMessage("hello");
        String resp2 = client.sendMessage("world");
        assertEquals("hello", resp1);
        assertEquals("world", resp2);
    }

    @After
    public void teardown() throws IOException {
        server.destroy();
        EchoClient.stop();
    }
}

9. Conclusion

Dans cet article, nous avons décrit l’utilisation de base du composant Java NIO Selector.

Le code source complet et tous les extraits de code de cet article sont disponibles dans myGitHub project.