Sélectionner une socket ou System.in grâce à un Selector

System.in ne contient pas de SelectableChannel, comment faire ?

a marqué ce sujet comme résolu.

Bonjour, bonsoir, chers zagrumes !

Pour mes cours de réseaux Java (Licence 3 Informatique), on apprend à utiliser les sockets réseaux, de même que l’on a appris à les utiliser en C (sockets POSIX).

En C, je sais parfaitement utiliser la fonction select et les sets qui vont avec. De cette façon, je sais lire à la fois le clavier ou une socket quelconque en lecture et/ou écriture. C’est simple : en UNIX, tout est fichier et à chaque fichier correspond un entier que l’on appelle "file descriptor".

En Java, les choses se ressemblent mais sont différentes. On utilise la classe Selector dont la méthode statique open() permet de créer un nouveau sélecteur, dans lequel on ajoute des SelectableChannel (comme des SocketChannel, par exemple) grâce à la méthode SelectableChannel.register(Selector sel, SelectionKey interestOps), puis on récupère les SelectionKey des canaux prêts correspondants grâce à Selector.select() et Selector.selectedKeys(). De la même manière qu’en C, j’aimerais pouvoir ajouter stdin (System.in en Java) dans le sélecteur pour récupérer les sockets et/ou l’entrée standard prêtes pour la lecture. Seulement voilà, System.in est (indirectement) un InputStream et ne contient pas de SelectableChannel.

Comment puis-je donc procéder pour attendre qu’une entrée soit prête, System.in et quelconque Socket confondues ?

PS : Le prof de réseau Java ne nous a pas du tout parlé de tout ça, mais connaissant le concept en C, j’ai bien envie de savoir l’utiliser en Java également. L’UE réseau est découpée en 2 : partie C / partie Java. Ce sont des profs différents pour chaque langage.

+0 -0

C’est pas sûr que tu puisses en Java. La raison sous-jacente c’est qu’il n’y a que pour Unix que tout est fichier. Sous Windows, seuls les sockets sont conformes à POSIX et fonctionnent avec select. De coup il est possible qu’un langage portable comme Java n’ait pas jugé bon d’implémenter ce mécanisme ailleurs que pour les sockets…

+0 -0

J’aurais apprécié que les OS non UNIX utilisent ce principe… Mais dans ce cas, ce ne seraient pas vraiment des systèmes non UNIX.
Mais quand même, c’est bien dommage que Java ne propose pas un moyen simple de rendre sélectionnable System.in et System.out :(

Je vais explorer la solution de Genroa, en passant par un Pipe, mais elle m’a l’air un peu lourde pour le simple chat en console que je me suis proposé de faire.
J’ai pourtant cherché un moment mais je ne suis pas tombé là-dessus, je vais revoir ma façon de choisir mes mots-clefs :o

Mon premier but est en fait de lire l’entrée standard sans bloquer le processus et pouvoir réagir dès que je reçois des données via une socket. J’imagine qu’il doit y avoir un moyen assez simple de le faire, mais apparemment pas via un Selector.
Je vais explorer d’autres moyens que la "conversion" System.in -> SelectableChannel.

PS : Je ferai ça ce soir, là je ne suis pas sur mon PC et il n’y a pas de quoi développer en Java sur ce Windows (je pourrais installer ce qu’il faut mais je respecte son propriétaire qui est occupé ^^ ).

+0 -0

Globalement, chercher à reproduire la programmation système (généralement UNIX qu’on apprend en cours) du C à Java est une assez mauvaise idée : l’API propose souvent des classes pour abstraire le tout et des patterns différents, permis par la programmation orientée objet. C’est principalement dû au fait qu’il faut que ce soit portable avant tout. :)

Sinon tu peux bosser avec des Threads, ce qui semble être une des solutions possibles et pas trop compliquées à mettre en place.

Ah mais bien sûr, je cherche justement à trouver quels classes me permettent de faire ce que je souhaite. Mais utiliser l’entrée standard comme entrée dans une sélection de canaux (entrée standard et sockets confondus) ne me parait pas à l’encontre du principe de l’orienté objet ni de faire un code portable.

Par contre, ça m’embête d’utiliser un thread juste pour lire l’entrée standard dans un chat avec un côté client et un côté serveur basiques (le serveur fait aussi client en fait, et n’accepte qu’un autre client, pour simplifier l’exercice et se concentrer sur la base).
Je ne sais pas trop encore comment s’utilisent les Threads en Java. Je vais potasser tout ça après manger.

EDIT : A-priori, InputStream.available() (ou BufferedReader.ready()) ferait l’affaire, malgré qu’elle ne soit pas vraiment fiable d’après la doc.

+0 -0

A propos des Threads, oui je sais. Ca fais d’ailleurs partie de ce que l’on apprendra pendant cette UE réseaux Java. Mais là je veux d’abord réussir à faire fonctionner de simples sockets, un sélecteur et les flux qui vont avec ^^
D’ailleurs, si ce n’est pas fiable, pourquoi ces méthodes existent-elles ?

Bon, j’ai un code de test qui ne fonctionne pas. Visiblement, il y a un détail que je n’ai pas compris à propos des sockets Java ou j’ai fait une erreur bidon que je n’arrive pas à voir.

Quand je lance le client, j’ai un NullPointerException. Après quelques tests, j’ai compris que srv.getChannel() renvoie null, mais je n’arrive pas à comprendre pourquoi.
Client.java :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package client;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.Set;

public class Client {

  public static void main(String[] args) {

      System.out.print("CLT> Connecting to server... ");
      try (Socket srv = new Socket("localhost", 11012)) {
          System.out.println("OK.");
          SelectableChannel ch = srv.getChannel();

          if(ch == null)
              throw new IOException("Error : channel is null.");

          try (
              BufferedReader srvin = new BufferedReader(new InputStreamReader(
                  srv.getInputStream()));
              BufferedWriter srvout = new BufferedWriter(new OutputStreamWriter(
                  srv.getOutputStream()));
              BufferedReader stdin = new BufferedReader(new InputStreamReader(
                  System.in));

              Selector sel = Selector.open()
          )
          {
              if(!srv.isConnected() || srv.isInputShutdown() || srv.isOutputShutdown())
                  throw new IOException("Socket srv error.");
              ch.register(sel, SelectionKey.OP_READ);

              System.out.println("CLT> Chat is running.");

              String s = "";
              while(!s.equals("exit"))
              {
                  sel.select();
                  Set<SelectionKey> set = sel.selectedKeys();
                  for(SelectionKey key : set) {
                      if(key.isReadable())
                      {
                          s = srvin.readLine();
                          System.out.println("SRV : "+ s);
                      }
                  }
                  if(System.in.available() > 0)
                  {
                      s = stdin.readLine();
                      srvout.write(s);
                  }
              }

              System.out.println("CLT> Good bye.");
          }

      } catch (IOException ex) {
          System.out.println("\n"+ ex.getMessage());
      }
  }
}

`

Serveur.java :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package serveur;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.Set;

public class Serveur {

  public static void main(String[] args) {
      System.out.print("SRV> Starting... ");
      try (ServerSocket srv = new ServerSocket(11012)) {
          System.out.print("OK\nSRV> Waiting for a client... ");
          System.in.read();

          try(
              Socket clt = srv.accept();
              BufferedReader cltin = new BufferedReader(new InputStreamReader(
                  clt.getInputStream()));
              BufferedWriter cltout = new BufferedWriter(new OutputStreamWriter(
                  clt.getOutputStream()));
              BufferedReader stdin = new BufferedReader(new InputStreamReader(
                  System.in));

              Selector sel = Selector.open();
          ) {
              SelectableChannel ch = clt.getChannel();
              if(ch != null)
                  ch.register(sel, SelectionKey.OP_READ);
              else
                  throw new IOException("\nError : channel is null.\n");

              System.out.println("Client connected.\nSRV> Chat is running.");

              String s = "";
              while(!s.equals("exit"))
              {
                  sel.select();
                  Set<SelectionKey> set = sel.selectedKeys();
                  for(SelectionKey key : set) {
                      if(key.isReadable())
                      {
                          s = cltin.readLine();
                          System.out.println("CLT : "+ s);
                      }
                  }
                  if(System.in.available() > 0)
                  {
                      s = stdin.readLine();
                      cltout.write(s);
                  }
              }

              System.out.println("SRV> Good bye.");
          }
      } catch(IOException ex) {
          System.out.println(ex.getMessage());
      }
  }
  
}

`

Je tâtonne encore avec les sockets en Java, n’hésitez pas toute remarque.

+0 -0

Après une courte recherche, il s’avère que tu sembles essayer des choses compliquées alors que les SocketChannel fournissent tout ce dont tu as besoin. Ce tutorial (et les suivants dans la liste du site) expliquent comment créer directement ce que tu recherches via les SocketChannel et SocketChannelServer:http://tutorials.jenkov.com/java-nio/socketchannel.html

Il semblerait que la logique soit plutôt de créer via SocketChannel l’objet voulu, puis si besoin (peu probable) d’en récupérer la Socket dessous. Mais un Socket ne semble pas forcément lié à un SocketChannel, d’où le retour = null quand tu demandes à une Socket quelconque de te retourner son SocketChannel. :) d’après ce tuto et la doc, l’objet SocketChannel dispose de tout ce dont tu as besoin (connect, read, write) pour faire ce que tu veux, tu n’as peut-être pas besoin de descendre aussi bas que la Socket.

+0 -0

D’accord, donc il y a bien quelque chose que je ne comprenais pas vraiment. Les SocketChannel sont l’équivalent des sockets POSIX en C, apparemment. Merci pour le tutoriel :)

Cependant, je dirais plutôt "monter aussi haut" que la Socket : la classe sert d’encapsulation sur les SocketChannel, non ?
Enfin, bref. Avec Java, je galère toujours un peu à faire ce que je veux ^^ Le langage n’est pas compliqué mais savoir utiliser sa bibliothèque standard, c’est une autre histoire !

Je trouvais ça cool de ne pas avoir à m’embêter avec les SocketChannel mais bon, puisque c’est grosso modo la même chose que les sockets POSIX C, ça va pas me demander beaucoup d’efforts. N’empêche que j’aimerais bien comprendre pour quelle raison la socket côté client renvoie null pour getChannel(), je n’ai pas trouvé de réponse dans la documentation.

EDIT : En utilisant les canaux directement, on arrive à un code un peu étonnant pour lier la socket serveur à un port :

1
2
ServerSocketChannel srv = ServerSocketChannel.open();
srv.socket().bind( ... );

Au final, on passe (temporairement) par un objet Socket. Je suis surpris qu’on ne puisse pas le faire directement sur un ServerSocketChannel.

EDIT 2 : Passer par un objet Socket reste utile finalement, puisqu’on peut accéder aux objets InputStream et OutputStream correspondants de cette manière.

+0 -0

J’ai dit des bêtises à propos de la méthode bind, elle existe sur ServerSocketChannel. Mais dans ce cas, je ne comprends pas l’intérêt de passer par l’objet Socket pour faire un bind, tel que ça a été fait dans le tutoriel (celui donné par Genroa).

J’ai donc essayé directement avec les canaux mais le serveur n’a pas l’air de capter qu’un client se connecte alors que le client me semble bien connecté.
Voici mon code :
Client.java :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Set;

public class Client {

  public static void main(String[] args) {

      System.out.print("CLT> Connecting to server... ");
      try (SocketChannel srv = SocketChannel.open()) {
          srv.connect(new InetSocketAddress("localhost", 11012));
          System.out.println("OK.");

          try (
              Socket srvSo = srv.socket();
              BufferedReader srvin = new BufferedReader(new InputStreamReader(
                  srvSo.getInputStream()));
              BufferedWriter srvout = new BufferedWriter(new OutputStreamWriter(
                  srvSo.getOutputStream()));
              BufferedReader stdin = new BufferedReader(new InputStreamReader(
                  System.in));

              Selector sel = Selector.open()
          )
          {
              srv.configureBlocking(false);
              srv.register(sel, SelectionKey.OP_READ);

              System.out.println("CLT> Chat is running.");

              String s = "";
              while(!s.equals("exit"))
              {
                  sel.select();
                  Set<SelectionKey> set = sel.selectedKeys();
                  for(SelectionKey key : set) {
                      if(key.isReadable())
                      {
                          s = srvin.readLine();
                          System.out.println("SRV : "+ s);
                      }
                  }
                  if(System.in.available() > 0)
                  {
                      s = stdin.readLine();
                      srvout.write(s);
                  }
              }

              System.out.println("CLT> Good bye.");
          }

      } catch (IOException ex) {
          System.out.println("\n"+ ex.getMessage());
      }
  }
}

J’ai quand même essayé en utilisant la méthode available et ça ne fonctionne pas, en effet. Je vais donc m’atteler à une vraie solution, en passant par un thread. Ou une interface graphique ? On devra en faire pour le projet de toute façon, autant commencer le plus tôt possible.

Serveur.java :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Set;

public class Serveur {

  public static void main(String[] args) {
      System.out.print("SRV> Starting... ");
      try (ServerSocketChannel srv = ServerSocketChannel.open()) {
          srv.bind(new InetSocketAddress(11012));

          System.out.print("OK\nSRV> Waiting for a client... ");

          try (
              SocketChannel clt = srv.accept();
              Socket cltSo = clt.socket();
              BufferedReader cltin = new BufferedReader(new InputStreamReader(
                  cltSo.getInputStream()));
              BufferedWriter cltout = new BufferedWriter(new OutputStreamWriter(
                  cltSo.getOutputStream()));
              BufferedReader stdin = new BufferedReader(new InputStreamReader(
                  System.in));

              Selector sel = Selector.open();
          ) {
              clt.configureBlocking(false);
              clt.register(sel, SelectionKey.OP_READ);

              System.out.println("Client connected.\nSRV> Chat is running.");

              String s = "";
              while(!s.equals("exit"))
              {
                  sel.select();
                  Set<SelectionKey> set = sel.selectedKeys();
                  for(SelectionKey key : set) {
                      if(key.isReadable())
                      {
                          s = cltin.readLine();
                          System.out.println("CLT : "+ s);
                      }
                  }
                  if(System.in.available() > 0)
                  {
                      s = stdin.readLine();
                      cltout.write(s);
                  }
              }

              System.out.println("SRV> Good bye.");
          }
      } catch(IOException ex) {
          System.out.println(ex.getMessage());
      }
  }
}

EDIT : Bon, j’étais pas en forme en écrivant le code. Il y avait un vestige de test, un petit read glissé juste après l’affichage "Waiting for a client…". Forcément, le serveur ne risquait pas de bouger ^^

+0 -0

J’ai essayé d’adapter mon code pour utiliser une classe telle que celle donnée dans ce tuto. Par contre, j’ai un doute sur ma manière de faire l’équivalent d’un FD_ISSET(fd, set).
Ici, on a un Set<SelectionKey> que l’on parcourt, j’en déduis qu’on peut comparer le canal de chaque clef au canal recherché pour effectuer la tâche correspondante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
String s = "";
while(!s.equals("exit")) {
    selector.select();
    Set set = selector.selectedKeys();
    for(SelectionKey key : set) {
        if(set.channel().equals(socketChannel)) {
            // On lit la socket dans s et on écrit le résultat à l'écran.
        }
        if(set.channel().equals(stdinChannel)) {
            // On lie stdin dans s et on envoie à la socket.
        }
}

Est-ce une manière correcte de procéder ?

Autre chose : la classe donnée plus haut permet d’avoir un SelectableChannel sur System.in, ce qui ne permet pas de faire des read() dessus. J’ai cherché un peu dans la doc et j’ai vu qu’il existe des PipedInputStream et des PipedOutputStream. Je pense que c’est plus adapté et que je devrais moins galérer, mais je ne suis sûr de rien.

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte