Tutorial sur l’utilisation du framework BlueCove avec une application client/serveur
Tutorial sur l’utilisation du framework BlueCove avec une application client/serveur
À bien des égards, ce chapitre est semblable au chapitre B1, en ce qu’il concerne une application client / serveur en écho utilisant Bluetooth. Cela mérite son propre chapitre, car j’ai réécrit le code pour utiliser l’implémentation BlueCove de l’API Java Bluetooth (), destinée à JavaSE. Cela signifie que l'interface graphique midlet a dû être changée en Swing - une tâche assez facile. Une tâche plus importante et plus difficile consistait à réorganiser le code de découverte de périphérique et de service. L’approche standard, décrite au chapitre B1, n’est pas assez robuste pour traiter les interférences radio dans mon environnement de test. J'ai apporté plusieurs modifications pour tenir compte des temps de découverte et de recherche très longs et des échecs de connexion peu fréquents.
Je pourrais écrire longuement sur les difficultés liées à la recherche de la pile Bluetooth «correcte» pour Windows XP, mais vous serez peut-être heureux de lire que j’ai déplacé mes gémissements au chapitre B3, «Problèmes de programmation Bluetooth».
() En gros, lorsque le protocole de communication Bluetooth est RFCOMM (comme ici), vous pouvez également vous en tenir à la pile Microsoft: c’est beaucoup moins compliqué que de tenter de la remplacer par une implémentation plus complète telle que la pile Widcomm / Broadcom. Je le sais, car dans ce chapitre, le serveur exécute BlueCove sur une pile Widcomm, tandis que mes clients de test utilisent BlueCove sur la pile de Microsoft.
Si l'appariement pose problème, le côté serveur de l'application doit être exécuté sur la pile Linux BlueZ, ce qui peut créer un processus 'agent' pour accepter les demandes d'association. Les clients peuvent continuer à utiliser la pile Microsoft (ou Widcomm) mais doivent utiliser des extensions non standard de JSR-82 dans BlueCove pour envoyer des demandes de couplage. Je parlerai plus à ce sujet dans la section 8.
- Présentation de l'application client / serveur
L'exemple de ce chapitre montre comment un serveur Bluetooth traite les connexions et les messages client. Un client recherche des périphériques et des services Bluetooth, se connecte à un serveur correspondant et lui envoie des messages. Plusieurs clients peuvent se connecter au serveur à la fois, le serveur utilisant un thread dédié pour chacun.
Le serveur est une application de ligne de commande qui attend la plupart du temps en attente de connexions client. Une fois la connexion établie, la tâche de l’application est assez simple: un message envoyé par le client est renvoyé en écho par le thread du serveur, remplacé par une majuscule. La figure 1 illustre le serveur démarrant sur le périphérique «Andrew», créant un thread permettant de gérer une connexion à partir du client «oak» et traitant deux messages.
Figure 1. Le serveur BlueCove Echo.
Le client envoie d’abord le message "hello" et reçoit "HELLO". Puis «aujourd'hui est jeudi» arrive sur le serveur et est également changé en majuscule. La figure 2 montre le client peu de temps après avoir reçu la deuxième réponse.
Figure 2. Le client BlueCove Echo.
La figure 3 montre les diagrammes de classe pour le côté serveur de l'application. Le serveur de niveau supérieur est créé par la classe EchoServer et chaque thread ThreadedEchoHandler gère une connexion client distincte.
Figure 3. Diagrammes de classes pour le serveur.
La nature filetée de l'application est visible à la figure 4, qui montre trois clients connectés au serveur.
Figure 4. Communication client et serveur.
La connexion de flux entre un client et un gestionnaire utilise le protocole RFCOMM de Bluetooth.
La figure 5 montre les classes utilisées du côté client de l'application. Comme avec la figure 3, les méthodes publiques et protégées sont présentées.
Figure 5. Diagrammes de classes pour le client.
EchoClient implémente le client, ce qui est assez simple, principalement parce que le dur labeur de la découverte de périphériques et de services est géré par DevicesFinder et
Classes ServiceFinder. TimeOutTimer est utilisé pour empêcher le client d'attendre pour toujours si un message n'est pas répondu. ServiceFinder utilise la classe RemDevice pour créer un objet RemoteDevice, comme expliqué ultérieurement.
- Le serveur
EchoServer commence par rendre le périphérique du serveur détectable, en affichant son nom et son adresse Bluetooth. Le client peut utiliser cette adresse pour se connecter plus directement (et rapidement) au serveur, comme je le verrai plus tard lorsque je parlerai de la méthode de recherche findOn () de la section 6.4.
initDevice ()
{
essayez {// rendre le périphérique du serveur détectable
LocalDevice local = LocalDevice.getLocalDevice ();
.println ("Nom du périphérique:" + local.getFriendlyName ()); .println ("Adresse Bluetooth:" + local.getBluetoothAddress ()); boolean res = local.setDiscoverable ();
.println ("Ensemble de découvrabilité:" + res);
}
catch (BluetoothStateException e) {
.println (e);
(1);
}
} // fin de initDevice ()
Un coup d’œil sur la Figure 1 révèle que ce périphérique «Andrew» a l’adresse Bluetooth 001F81000250.
Le périphérique du serveur doit être détectable par un client:
LocalDevice local = LocalDevice.getLocalDevice (); boolean res = local.setDiscoverable ();
La constante (Code d'accès à l'enquête générale / illimitée) signifie que tous les périphériques distants (c'est-à-dire tous les clients) seront en mesure de trouver le périphérique.
createRFCOMMConnection () est appelée pour créer un notificateur de connexion RFCOMM pour le serveur, avec l'UUID et le nom donnés. Les deux paramètres sont également utilisés par le client pour trouver le service au moment de la recherche.
// globals
// UUID et nom du service d'écho privé statique final String UUID_STRING = "1111111111111111111111111111111111";
// 32 chiffres hexadécimaux qui deviendront un identifiant de 128 bits privé statique final String SERVICE_NAME = "echoserver";
serveur privé StreamConnectionNotifier;
privé void createRFCOMMConnection ()
{essayer {
.println ("Lancer la publicité" + SERVICE_NAME + ""); serveur = (StreamConnectionNotifier) ("btspp: // localhost:" + UUID_STRING +
"; name =" + SERVICE_NAME + "; authenticate = false"); }
catch (IOException e) {.println (e);
(1);
}
} // fin de createRFCOMMConnection ()
La connexion de flux RFCOMM offerte par le serveur nécessite une URL formatée de manière appropriée. Le format de base est le suivant: btspp: // :;
J'utilise localhost comme nom d'hôte. Le champ UUID est un identifiant unique de 128 bits représentant le service; J'utilise une chaîne hexadécimale de 32 chiffres (chaque chiffre hexadécimal utilise 4 bits).
Les paramètres de l'URL sont des paires "=", séparées par des points-virgules. Les valeurs typiques sont "nom" pour le nom du service (utilisé ici) et des paramètres de sécurité tels que "authentifier", "autoriser" et "chiffrer". Il est judicieux de définir la valeur de la chaîne "name" en minuscule, sinon certaines piles ne pourront pas reconnaître le nom du service au moment de la découverte.
L'inclusion de «authenticate = false» dans l'URL Bluetooth peut permettre au serveur de contourner l'appariement explicite avec le client, mais cela dépend des types de périphériques et de piles utilisés. Lors de mes tests, le couplage des clients accédant au serveur à partir d'un ordinateur portable ou d'un netbook n'était pas désactivé.
2.1. En attente d'un client
Le serveur entre une boucle While dans processClients () qui génère un thread ThreadedEchoHandler lorsqu'un client se connecte au serveur.
// globals
gestionnaires privés ArrayList; boolean volatile privé isRunning = false;
processClients () vide privé
{
isRunning = true; essayer {
while (isRunning) {
.println ("En attente de connexion entrante");
StreamConnection conn = server.acceptAndOpen ();
// attend une connexion client
.println ("Connexion demandée");
ThreadedEchoHandler hand = nouveau ThreadedEchoHandler (conn);
// crée un gestionnaire client (hand); hand.start ();
}}
catch (IOException e) {.println (e);
}
} // fin de processClients ()
L'appel à acceptAndOpen () bloque le serveur jusqu'à l'arrivée d'une connexion client et ajoute également l'enregistrement de service du serveur à la base de données SDD (Service Discovery Database) du périphérique. Lorsqu'un client effectue une découverte de périphérique et de service, il contacte les SDDB des périphériques sur lesquels il enquête.
Lorsqu'une connexion client est établie, acceptAndOpen () renvoie un objet StreamConnection, qui est transmis à une instance ThreadedEchoHandler afin qu'il puisse gérer la communication client.
2.2. Fermeture
Étant donné que le serveur ne dispose pas d’interface graphique avec une boîte fermée, le constructeur du serveur utilise un point d’arrêt pour appeler la méthode closeDown () lorsque le serveur est arrêté.
// dans le constructeur de EchoServer:
Runtime.getRuntime (). AddShutdownHook (new Thread () {public void run ()
{closeDown (); }
});
Les threads du serveur et du gestionnaire se terminent par closeDown ().
privé void closeDown ()
{
.println ("Fermeture du serveur"); if (isRunning) {isRunning = false; essayer {
server.close ();
}
catch (IOException e) {.println (e); }
// ferme tous les gestionnaires pour (handedEchoHandler hand: handlers) hand.closeDown (); handlers.clear ();
}
} // fin de closeDown ();
La fermeture de StreamConnectionNotifier indique également à la SDDB de supprimer l'enregistrement de service du serveur.
Les gestionnaires sont chacun terminés en appelant leur méthode closeDown ().
- Le gestionnaire d'écho fileté
Un gestionnaire commence par extraire des informations sur le client à partir de l'instance StreamConnection.
// globals
connexion privée StreamConnection; // connexion client
private String clientName;
public ThreadedEchoHandler (connexion StreamConnection)
{
= conn;
// stocke le nom du client connecté clientName = reportDeviceName (conn);
.println ("Gestionnaire généré pour le client:" + nomClient);
} // fin de ThreadedEchoHandler ()
private String reportDeviceName (connexion StreamConnection)
/ * Renvoie le nom 'convivial' du périphérique examiné ou "périphérique ??" * /
{
String devName; essayer {
RemoteDevice rd = RemoteDevice.getRemoteDevice (conn);
devName = rd.getFriendlyName (false);
}
catch (IOException e) {devName = "device ??"; } return devName;
} // fin de reportDeviceName ()
Les informations sur le périphérique distant (le client dans ce cas) sont stockées dans une
Instance RemoteDevice. RemoteDevice inclut des méthodes pour rechercher l'adresse Bluetooth du client, son nom de périphérique 'convivial', des détails sur ses paramètres de sécurité et pour vérifier ces paramètres.
Le gestionnaire récupère le nom de périphérique du client en appelant RemoteDevice.getFriendlyName (): devName = dev.getFriendlyName (false);
L'argument false empêche le gestionnaire d'obtenir le nom en contactant le client via une nouvelle connexion. Au lieu de cela, les informations présentes dans l'objet RemoteDevice (dev) sont utilisées.
Si true est utilisé, il est possible qu'une exception IOException soit déclenchée sur certains périphériques, car la connexion demandée peut dépasser le nombre total de connexions dépassant le nombre maximal autorisé.
3.1. Connexion au client
La méthode run () de ThreadedEchoHandler crée des flux d'entrée et de sortie et commence le traitement des messages du client. Lorsque le traitement des messages est terminé, les flux et la connexion sont fermés.
// globals
connexion privée StreamConnection; InputStream privé dans; sortie privée OutputStream;
public void run ()
{
essayer {
// Récupère les flux d'E / S de la connexion de flux dans = conn.openInputStream (); out = conn.openOutputStream ();
processMsgs ();
.println ("Fermeture" + nom du client + "connexion"); if (conn! = null) {in.close (); out.close (); conn.close ();
}}
catch (IOException e) {.println (e); }
} // fin de course ()
InputStream et OutputStream sont extraits de l'instance StreamConnection et utilisés dans processMsgs ().
Il est possible de mapper un DataInputStream et un DataOutputStream à l'instance StreamConnection, de sorte que les types de données Java de base (par exemple, les entiers, les flottants, les doubles, les chaînes) puissent être lus et écrits. J'ai utilisé InputStream et OutputStream car leurs méthodes read () et write () basées sur des octets peuvent facilement être utilisées comme "blocs de construction" pour la mise en œuvre de différentes formes de traitement de message (comme indiqué ci-dessous).
3.2. Traitement des messages du client
La méthode processMsgs () attend à plusieurs reprises qu'un message arrive du client, le convertit en majuscule et le renvoie. Si le message est "bye $$", le client souhaite fermer le lien. La boucle de traitement se ferme.
// globals
boolean volatile privé isRunning = false;
processus void privéMsgs ()
{
isRunning = true; Ligne de corde; while (isRunning) {if ((line = readData ()) == null) isRunning = false;
else {// il y a eu des entrées
.println ("" + clientName + "-> \" "+ line +" \ "");
if (() .equals ("bye $$")) isRunning = false;
autre {
Chaîne upper = () .toUpperCase (); if (isRunning) {
.println ("" + clientName + "<---" + upper); sendMessage (en haut);
}
}
}
}
} // fin de processMsgs ()
Les détails compliqués de la lecture d'un message sont cachés à l'intérieur de readData (), qui renvoie le message sous forme de chaîne, ou null en cas de problème. Un message est transmis avec sendMessage ().
3.3. Lire un message
Lorsqu'un client envoie un message au gestionnaire (par exemple, "hello"), il est en fait envoyé sous la forme d'un flux d'octets préfixés par sa longueur (par exemple, "5hello"). Le nombre est codé dans un seul octet, ce qui limite la longueur du message à 255 caractères.
Comme un message commence toujours par sa longueur, readData () peut utiliser cette valeur pour limiter le nombre d'octets lus à partir du flux d'entrée.
chaîne privée readData ()
{
byte [] data = null; essayer {
int len = (); // récupère la longueur du message if (len <= 0) {
.println (clientName + ": Erreur de longueur de message"); return null;
}
data = new byte [len]; len = 0;
// lit le message, nécessitant peut-être plusieurs appels read () while (len! = data.length) {
int ch = (data, len, data.length - len);
si (ch == -1) {
.println (clientName + ": Message Read Error"); return null;
} len + = ch;
}}
catch (IOException e)
{.println ("readData ():" + e); return null;
}
retourne une nouvelle chaîne (data) .trim (); // convertit octet [] en chaîne
} // fin de readData ()
() est appelé à plusieurs reprises jusqu'à ce que le nombre d'octets nécessaire ait été obtenu. Les octets sont convertis en chaîne et renvoyés. la longueur du message est supprimée.
3.4. Envoi d'un message
sendMessage () ajoute la longueur du message au début d'un message, qui est envoyé sous forme d'une séquence d'octets:
sendMessage booléen privé (String msg)
{essayer {
out.write (msg.length ()); out.write (msg.getBytes ()); out.flush (); retourne vrai;
}
catch (Exception e)
{.println (clientName + "sendMessage ():" + e); retourne faux;
}
}
3.5 Fermeture du manipulateur
Le serveur termine un gestionnaire en appelant sa méthode closeDown (), qui définit le boolean isRunning sur false pour que processMsgs () se termine.
public void closeDown () {isRunning = false; }
- Comment un client trouve le serveur
Avant d'appeler un objet client, la fonction main () de crée une instance de ServiceFinder pour effectuer des recherches de périphériques et de services. Si un enregistrement de service approprié est trouvé, alors seulement le client est instancié.
// globals: UUID et nom du service echo privé statique final String UUID_STRING =
"1111111111111111111111111111111111";
// 32 chaînes de chiffres hexadécimaux (deviendront un identifiant de 128 bits) private static final String SERVICE_NAME = "echoserver";
public static void main (String args [])
// pour
{
ServiceFinder srvFinder =
nouveau ServiceFinder (ServiceFinder.RFCOMM,
UUID_STRING, SERVICE_NAME);
ServiceRecord sr = (); if (sr! = null) nouvel EchoClient (sr);
} // fin de main ()
