Sockets Java : Impossible de faire un write() directement après la fonction readAllBytes()

Utilisation de Sockets non bloquants (NIO au lieu de IO) obligatoires ?

Le problème exposé dans ce sujet a été résolu.

Bonjour à tous et à toutes !

Et oui, encore un soucis sur un projet en Java. :-°

Une fois de plus, je fais appel à vous car je n’ai pas trouvé beaucoup d’explications concernant mon problème d’envoi de données vers un client.

Ça fait plusieurs jours que je bloque dessus. Très rapidement, j’ai remarqué que le problème venait de la fonction "readAllBytes()", utilisable sur InputStream. Je n’ai trouvé que cette petite piste hier soir. Elle semble correspondre à mon problème.

Voici les deux petits exemples de code :

Celui-ci fonctionne.

DataOutputStream os = new DataOutputStream(socketClient.getOutputStream());
os.write(1);
socketClient.getInputStream().readAllBytes();

Celui-là ne fonctionne pas.

DataOutputStream os = new DataOutputStream(socketClient.getOutputStream());
socketClient.getInputStream().readAllBytes();
os.write(1);

L’erreur produite en général est la suivante : java.net.SocketException: Software caused connection abort: socket write error at java.base/java.net.SocketOutputStream.socketWrite0(Native Method).

J’ai essayé de créer des buffers de bytes, de fermer socketClient.getInputStream(). Rien. Et comme il l’est mentionné sur la documentation d’Oracle, j’ai créé des Threads pour chaque nouveau message…mais évidemment, rien de ce que j’ai fait n’a fonctionné. X/

while (true)
{                
        socketClient = serveurSocket.accept();
                
        TcpClientService tcpClientService = new TcpClientService(socketClient);
        tcpClientService.run();
}  

Mon but : J’aimerais avoir la liberté de lire et relire les informations reçues tout en ayant la possibilité d’envoyer des données au client. Ce serait vraiment mieux car actuellement, je fais des traitements sur ce que je reçois, puis pour dire au client que tout est bon, je lui renvoie une réponse (un byte). C’est donc le cas qui correspond au second exemple de code ci-dessus. Je ne travaille presque exclusivement qu’avec des bytes.

Pour en revenir au lien que je vous ai montré, les personnes parlent de sockets non-bloquants. Ok… Mais j’ai vu que la mise en œuvre était vraiment très différente des Sockets habituels et aussi…beaucoup plus compliquée. :waw:

Mes questions sont :

  • Y aurai-t’il la possibilité de garder l’ancien système de Sockets (IO au lieu de NIO ?) tout en ayant la possibilité d’envoyer des données à n’importe quel moment ? Si oui, que faudrait-il faire pour que cela puisse fonctionner ?

  • Au final…est-ce que les Sockets non bloquants sont vraiment la solution à mon problème ?

  • J’ai entendu parler de "programmation asynchrone". Est-ce que ça pourrait avoir un rapport ici ?

Voilà, je tiens à m’excuser de nouveau pour ce long sujet. Mais je vous remercie infiniment de l’aide que vous pourrez m’apporter.

Et si vous avez d’autres questions à me poser, n’hésitez pas.

A bientôt !

+0 -0

Salut,

Je suis pas un spécialiste des sockets, mais j’ai l’impression que la méthode readAllBytes ne s’arrête qu’une fois qu’elle atteint la fin du flux de données. Or, pour un socket, atteindre la fin du flux de données doit être à peu près synonyme à "fermeture du socket", ce qui expliquerait pourquoi tu ne peux plus écrire une fois avoir appelé la méthode readAllBytes (d’où l’erreur). Peut-être qu’utiliser une autre méthode, comme readFully, marcherait ?

J’espère que je n’ai pas dit trop de bêtises !

Bonjour ! Et merci de ta réponse. :)

Je crois que tu as raison. J’ai lu que "readAllBytes" attends toujours de nouvelles données à lire et pas uniquement celles qui existent déjà.

J’utilise la classe "ByteArrayInputStream" donc je n’ai pas accès à la méthode "readFully". Mais rien ne m’empêche de tester avec la classe "DataInputStream" qui implémente cette méthode.

Je fais le changement dans le code et je viendrai rééditer mon message pour te donner des nouvelles. ^^

Ah. La méthode "readFully" prends en paramètre le tableau de bytes à lire… Sauf que ce tableau, je ne sais pas vraiment comment l’avoir…autrement que avec la méthode bloquante "readAllBytes". :-°

+0 -0

Je pense que tu dois créer un tableau de byte d’une longueur précise, que tu passes en argument à readFully, qui se charge de le remplir. Par contre, ça implique d’avoir connaissance de la taille totale des informations à lire, ce qui ne te convient peut-être pas ? :-°

Bon, j’ai fait 2–3 recherches, et j’ai peut-être une solution :)

Si tu manipules des bytes, il faut peut-être définir un protocole ? Par exemple, -1 signifie que c’est la fin d’une suite de paquets envoyés par le client, -2 signifie que c’estla fin d’un paquet tout court, mais qu’il y a un paquet après, etc.

De cette manière, tu pourrais lire une série de byte avec une boucle while, un peu comme suit :

    DataInputStream dataIn = new DataInputStream(socket.getInputStream());
    DataOutputStream dataOut = new DataOutputStream(socket.getOutputStream());
    byte data = 0;
    while((data = dataIn.readByte()) != -1) {
        if(data == -2) {
            //etc...
        }
    }

Tu pourrais ensuite renvoyer des données à ton client avec dataOut.writeByte(1), etc.

Je ne sais pas si cette solution convient à ton problème ? :-°

Mince… :/ En fait, je n’ai pas le contrôle des clients. Car ce sont en fait des objets connectés. Ils communiquent avec un protocole qui leur est propre.

Par contre, je pense que ta réponse peut m’aider pour un autre projet.

Merci quand même pour cette réponse. :honte:

Ah, c’est un problème, en effet :-°

Est-ce que tu as connaissance du protocole qu’ils utilisent ? Il y a peut-être moyen d’obtenir un délimiteur de paquet, ou, à tout hasard, un indicateur de la taille du paquet ?

EDIT : sinon, il y a peut-être possibilité d’utiliser un ByteBuffer, comme ici : https://stackoverflow.com/questions/22366301/reading-stream-over-tcp-on-a-socketchannel-with-undefined-number-of-bytes. Mais encore une fois, je suis ni spécialiste en Java, ni en sockets :euh:

+0 -0

Lors de sa première connexion un objet connecté envoie une trame de ce style :

00 0f 333536333037303432343431303133. 2 bytes qui nous permettent ensuite de connaitre la taille de la donnée reçue ensuite. 00 et 0f. Qui nous donne une donnée de 15 bytes (une imei).

MAIS ! Une fois la balise acceptée, elle nous renvoie une trame bien plus compliquée. Celle des données qu’elle a pu collecter.

image.png
image.png

Et ce n’est qu’en parcourant ses données que l’on peut additionner la taille des sous parties et connaitre la véritable longueur de la trame.

4 Bytes + 4 + 1 + 1 + (taille variable que l’on connaitra plus tard avec d’autres calculs) + 1 + 4 = taille totale de la trame reçue.

Je viens de voir ton Edit. A force, je ne sais plus ce que j’ai testé. Je vais revoir ça. Merci ! :)

Et ne t’en fais pas. Je ne suis pas non plus un pro' de Java. Je suis en train de l’apprendre un peu plus en profondeur en ce moment. ^^

+0 -0

Oulà oui, effectivement :)

Mais je pense qu’avec ces informations-là, la méthode de la boucle while est toujours possible, non ?

Il faut peut-être que tu adoptes un format un peu plus complexe, comme :

    boolean done = false;
    byte buffer[] = new byte[4+4+1+1+147+1+4]; //on a au moins une taille maximale
    int currentIndex = 0;
    byte dataLength[] = new byte[4]; 
    int dataLengthIndex = 0;
    boolean dataLengthReceived = false;

    while(!done) {
       byte b = dataIn.readByte();
       buffer[currentIndex] = b;
       
       if(currentIndex >= 4 && currentIndex < 8) {
           dataLength[dataLengthIndex] = b;
           dataLengthIndex++;

           if(currentIndex == 8-1) {
               dataLengthReceived = true;
               //ici, on sait qu'on a reçu la taille de la partie variable, donc on peut calculer la taille finale du paquet, donc on peut, plus tard dans la boucle, définir une condition de sortie
           }
       }
       //...
       //etc., on s'adapte à la valeur de currentIndex à chaque fois
       
       if(dataLengthReceived && currentIndex >= dataLength) {//un truc du genre, il faut par contre convertir dataLength en int avant 
           done = true;
       }


       currentIndex++;
    }

Je n’ai pas géré la gestion du premier message de taille fixe, et y’a probablement beaucoup de fautes dans le code que je viens d’écrire, mais globablement c’est l’idée ^^

Il y a sûrement moyen d’utiliser des structures propres à Java pour étoffer le tout, je pense notamment aux ByteBuffers.

Bref, j’espère que c’est à peu près pertinent par rapport au problème initial ? :-°

Merci pour ce code ! Je vais l’essayer. ^^

Par contre, j’ai surtout peur d’une chose, c’est que je puisse pas faire de "write()" après. Une fois toutes les données lues, en général, le problème arrive. Et je ne peux pas envoyer de données vers le client en retour. :/

+0 -0

Dans ce cas-là, c’est peut-être synonyme d’autre chose : ton objet connecté ferme tout seul le socket une fois qu’il a terminé d’écrire ses données, ce qui fait que tu ne peux pas le réutiliser.

Je pense que dans ce cas-là, il faut que tu ré-ouvres toi-même un socket vers lui, et que tu lui envoies tes données ?

Salut ! Je ne sais pas trop comment ouvrir un socket de cette manière (autrement qu’en utilisant SocketServer.accept()). Je vais me renseigner. C’est pas impossible ça. :)

Ça expliquerait l’erreur que j’obtiens ensuite.

Et aussi, le fait que les fonctions "readQuelqueChose()" soient bloquantes est vraiment très ennuyant je trouve…

Salut !

Si tu veux réouvrir un socket vers ton appareil, il faut que tu agisses cette fois-ci en tant que client, en faisant un truc du genre Socket s = new Socket(ip, port);. Par contre, ça implique que ton appareil ait aussi quelque chose équivalent à SocketServer.accept() de son côté (ce qui n’est peut-être pas le cas - il faut voir la documentation).

Sinon, pour répondre aux deux questions de ton premier message auxquelles je n’ai pas répondu :

  • Les sockets non bloquantes n’ont pas l’air d’être la solution immédiate au problème posé, mais par contre, si le fait que les fonctions read bloquent te dérange, alors c’est le chemin à prendre ^^ (par contre j’y connais rien, je ne pourrais pas t’aider là-dessus :-° )

  • Oui, les sockets non bloquantes sont un exemple de programmation asynchrone. En gros, ton programme principal continue sa route si rien n’est arrivé, et n’attends pas l’arrivée de nouveaux paquets.

Enfin, dernière question : est-ce que tu aurais un lien vers la documentation de l’appareil avec lequel tu essayes de communiquer ? Histoire que je puisse regarder plus en détail :)

Pour ce qui est de l’appareil, je n’ai rien vu un équivalent de "SocketServcer.accept". Mais je vais revoir ça.

Pour ton premier point, ce qui est dommage, c’est que mon serveur est pour le moment très simple et compréhensible. Je veux lire (ce que j’arrive à faire), mais ensuite, je souhaite envoyer une réponse en fonction de ma lecture. Et…c’est là où les "readQuelqueChose" me posent problème. Les lignes suivantes suivant ne s’exécutent pas.

Je suis en train de me documenter sur les classes de NIO, mais c’est vraiment plus compliqué je trouve. :-°

Du coup, j’aimerais bien trouver une manière de continuer avec "ma" méthode, mais je ne trouve pas. :(

Oui ! Voici un lien vers le protocole des balises. :magicien:

Merci pour tout le temps que tu passes à m’aider. :)

+0 -0

Par SocketServer.accept(), je voulais dire : est-ce que par hasard, ton appareil contient aussi un serveur qui te permettent de t’y connecter sans que ce soit lui qui initie la connexion ? :)

Mais j’ai lu la doc et effectivement, y’a pas l’air d’avoir ce genre de serveur :-°

Oui, je pense que le fait de passer à NIO n’est pas nécessaire ici, en général on l’utilise plus quand y’a vraiment beaucoup de connexions à gérer et/ou quand il faut absolument que le serveur fasse autre chose en attendant ^^

Merci pour le protocole, du coup, quel appareil utilises-tu ? FM1100, 2100, 2200, etc. ?

Enfin, est-ce que tu pourrais m’envoyer une version complète de ton code ?

Pas de problème :P

Oui, il n’y a pas grand chose qui parle de serveur sur la documentation.

Ok, je vois. Mais du coup, le même problème revient. Comment fait t-on pour répondre à cette balise après un readAllBytes() ? :'( Et j’ai vraiment rien trouvé, je retombe sans arrêt sur d’anciennes recherches. :(

L’appareil que j’'utilises actuellement est la TMT2250. Mais pas de soucis à avoir. Le firmware des appareils est identiques. Donc le protocole est le même. :)

Oui, je t’envoies ça en message privé. ;)

Ok, donc, après avoir continué notre conversation en message privé, on a fini par trouver la solution !

Le code originel de AlliageSphere était le suivant :

            byte[] bytesPaquetRecu;
            int tailleDonneesAvl = 0;
            boolean isConnecte = false;
            //byte[] reponse = null;
            byte reponse = 1;
            int tailleTrameRecue = 0;
            
            DataOutputStream os = new DataOutputStream(socketClient.getOutputStream());

            InputStream is = new ByteArrayInputStreamEtendu(socketClient.getInputStream().readAllBytes());
            is.reset();
            
            tailleTrameRecue = is.readAllBytes().length;
            is.reset();
            
            bytesPaquetRecu = is.readAllBytes();
            is.reset();
            
                        
            os.write(reponse); //Ici : Exception !
            os.flush(); 
            
            // Loop to receive all the data sent by the client.
            while (tailleTrameRecue != 0)
            {
                if (!isConnecte)
                {
                    System.out.println("Balise non connectée.");

                    // Accepte imei
                    //reponse = new byte[]{ 01 };
                    reponse = 1;
                    isConnecte = true;
                    
                    //dos.write(reponse, 0, reponse.length);
                    
                    os.write(reponse);
                    os.flush(); 
                    
                    System.out.println(LocalDateTime.now().toString() + " - REPONSE : " + reponse);
                } else {
                    //...
                }
            }

La source du problème est probablement l’utilisation double de readAllBytes, même si elle reste très obscure : la documentation de Java 9 indique clairement que readAllBytes ne ferme pas le socket. Pourtant, c’est bien cela qui fait que les appels suivants à write renvoient une exception.

Du coup, on a remplacé les appels à readAllBytes par l’utilisation d’un grand tableau de bytes et par des appels à DataInputStream.read(byte[] b). L’utilisation d’un tableau de bytes ici est safe, puisque, d’après la documentation de l’appareil de AlliageSphere, la taille maximum d’un message est de ~700 bytes et quelque. Du coup, le code suivant semble fonctionner :

            byte[] bytesPaquetRecu = new byte[4096]; //ici, on donne une grande taille par défaut, y'a sûrement moyen de faire mieux avec des ByteBuffer
            boolean isConnecte = false;
            byte[] reponse = new byte[] {01};
            int tailleTrameRecue = 0;
            
            DataOutputStream os = new DataOutputStream(socketClient.getOutputStream());
          
            DataInputStream is = new DataInputStream(socketClient.getInputStream());
            
            
            // Boucle de réception : on effectue un appel à read 
            while ((tailleTrameRecue = is.read(bytesPaquetRecu)) != 0) 
            {
                if (!isConnecte)
                {
                    System.out.println("Balise non connectée.");

                    // Accepte imei
                    reponse = new byte[]{ 01 };
                    isConnecte = true;    
                    
                    //ici, on parse l'IMEI si besoin, etc.
                    ...
                    
                    //On vide le tableau pour réutilisation à chaque tour de boucle
                    Arrays.fill(bytesPaquetRecu, (byte)0);                    
                    
                    os.write(reponse);
                    os.flush(); 
                    
                    System.out.println(LocalDateTime.now().toString() + " - REPONSE : " + reponse);
                }
                else
                {
                    System.out.println("Balise connectée.");
                   
                    ...
                }
            }

Ce code est basé sur la version C# fournie par le constructeur de l’appareil, qu’on peut récupérer ici (le zip contient aussi la documentation).

Bref, d’après AlliageSphere, ce code fonctionne !

Je viens confirmer tout ce qu’a dit cosuhi. Tout fonctionne à merveille ! :magicien:

J’ai juste modifié une petite chose pour simplifier la manipulation et l’extraction des données présentes dans le tableau de bytes. Au lieu de faire le code suivant, qui était en lien direct avec le socket et son "InputStream" :

InputStream is = new ByteArrayInputStreamEtendu(socketClient.getInputStream().readAllBytes());

J’ai préféré faire le code suivant en utilisant l’étape intermédiaire qui était la création du tableau de bytes :

// byte[] bytesPaquetRecu = new byte[4096];
ByteArrayInputStreamEtendu byteArrayIS = new ByteArrayInputStreamEtendu(bytesPaquetRecu);

Et pour ceux ou celles qui se demandent d’où vient ByteArrayInputStreamEtendu, c’est juste une classe que j’ai créé et qui hérite de BufferedInputStream en y ajoutant des méthodes semblables à celles de DataInputStream pour la lecture des données.

La principale étant une fonction qui calcule une valeur grâce à un nombre donné de bytes.

public long ReadXNextBytes(int n) throws IOException
{
    byte[] tabBytes = readNBytes(n);//FF
    long resultat = 0;
    int multiplicateur = n - 1;

    for (byte byteLu : tabBytes)
    {
        if(multiplicateur <= 0)
        {
            resultat += Byte.toUnsignedLong(byteLu);
        }
        else
        {
            resultat += Byte.toUnsignedLong(byteLu) << (8 * multiplicateur);
            multiplicateur--;
        }
    }

    return resultat;
}

En tout cas, mille mercis à cosuhi pour son aide Ô combien bénéfique. :)

+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