Communiquez en multicast avec votre Windows Phone sous « Mango »
Ou comment réaliser un minichat pour bavarder discrètement avec ses voisins.
Date de publication : 29 juillet 2011.
Par
Immobilis (accueil)
Dans ce tutoriel, je réaliserai une application WinPhone pour « Mango » 7.1 beta 2. Elle permettra à des smartphones
d'envoyer des paquets UDP à un groupe multicast. Chaque mobile exécutant cette application, et connecté au groupe, recevra
les messages.
Commentez
I. Introduction
II. La classe « UdpAnySourceMulticastClient »
III. Le programme
III-A. La classe « UdpChannel »
III-A-1. Création de la liaison au groupe multicast
III-A-2. La mise sur écoute
III-A-3. Code complet de « UdpChannel »
III-A-4. Autres objets
III-B. La page principale
III-B-1. Le fichier XAML
III-B-2. Code behind
III-C. Test
IV. Pour aller plus loin et autres détails
Conclusion
Remerciements
Références supplémentaires
I. Introduction
Le multicasting sur les téléphones Windows, je ne crois pas trop me tromper en disant que les développeurs attendaient cette fonctionnalité depuis longtemps,
ne serait-ce « que » pour les jeux. Pour ceux qui ne peuvent pas tester « Mango », il faudra attendre encore un peu.
Pour les autres, à vos starting-blocks.
II. La classe « UdpAnySourceMulticastClient »
Comme d'habitude, la MSDN est très claire. C'est un récepteur client de trafic multicast. Je vous renvoie vers elle
pour éviter de la paraphraser :
UdpAnySourceMulticastClient, classe.
III. Le programme
Pour bien faire, je l'ai baptisé « Whisper » parce qu'il ne fait pas de bruit et qu'il permet de communiquer discrètement.
J'aurais bien utilisé un bruit d'oiseau, mais c'est déjà pris.
Il se compose des objets suivants :
- une classe appelée « UdpChannel » qui va encapsuler le processus de communication ;
- une classe appelée « UdpPacketEventArgs » pour transférer les données lors des évènements ;
- un objet métier (une structure) appelé « Whisper » pour transférer les données envoyées
par les utilisateurs ;
- une classe statique pour sérialiser et désérialiser des objets afin de les envoyer sur le réseau ;
- une page standard (mode portrait).
III-A. La classe « UdpChannel »

La classe « UdpChannel »
Parmi les points importants cette classe dispose :
- d'une méthode « Open » afin de facilement établir la liaison avec le groupe multicast ;
- d'un évènement « OnAfterOpened » qui permettra à l'interface d'afficher un message de bienvenue ;
- d'une méthode « Receive » afin de facilement mettre le téléphone à l'écoute des paquets UDP ;
- d'un évènement « OnPacketReceived » qui permettra d'afficher les messages diffusés au groupe.
III-A-1. Création de la liaison au groupe multicast
| Code de la méthode « Open » |
<summary>
</summary>
public void Open()
{
if (!isJoined)
{
this.client.BeginJoinGroup(
result =>
{
try
{
this.client.EndJoinGroup(result);
isJoined = true;
this.AfterOpened();
this.Receive();
}
catch(Exception e)
{
throw e;
}
}, null);
}
}
|
 |
Aucune information personnelle ne peut-être récupérée par le programme sans l'autorisation de l'utilisateur.
La méthode « AfterOpened » ne peut donc pas envoyer le nom de l'appareil ou de son propriétaire.
Les messages envoyés sont donc anonymes. Par la suite, l'utilisateur peut saisir un pseudo.
|
III-A-2. La mise sur écoute
Encore une expression lambda contenant la méthode de rappel (callback). Le programme se met en attente
de paquets et exécute la méthode de rappel dès qu'un paquet est reçu. Pour plus de détails, référez-vous aux pages
suivantes :
BeginReceiveFromGroup et
EndReceiveFromGroup.
| Code de la méthode « Receive » |
private void Receive()
{
if (isJoined)
{
Array.Clear(this.buffer, 0, this.buffer.Length);
this.client.BeginReceiveFromGroup(this.buffer, 0, this.buffer.Length, result =>
{
if (!isDisposed)
{
IPEndPoint source;
try
{
this.client.EndReceiveFromGroup(result, out source);
this.AfterReceived(source, this.buffer);
this.Receive();
}
catch
{
isJoined = false;
this.Open();
}
}
}, null);
}
}
|
 |
Contrairement à l'établissement de la liaison, la méthode « EndReceiveFromGroup » permet de connaître
l'adresse IP de l'émetteur du message. Ce n'est pas une indiscrétion. Cela permet éventuellement d'utiliser
la classe « UdpSingleSourceMulticastClient »
pour adresser un message destiné uniquement à l'émetteur.
|
III-A-3. Code complet de « UdpChannel »
J'espère que les commentaires seront suffisants pour la compréhension.
| Code complet de « UdpChannel » |
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace Whisper.PhoneApp
{
public class UdpChannel : IDisposable
{
private UdpAnySourceMulticastClient client;
private byte[] buffer = new byte[1024];
private int localPort;
private bool isJoined;
private bool isDisposed;
public IPAddress groupAddress { get; private set; }
public event EventHandler<UdpPacketEventArgs> OnPacketReceived;
public event EventHandler<UdpPacketEventArgs> OnAfterOpened;
public event EventHandler<UdpPacketEventArgs> OnBeforeClosing;
<summary>
</summary>
<param name="address"></param>
<param name="port"></param>
public UdpChannel(IPAddress address, int port)
{
groupAddress = address;
localPort = port;
client = new UdpAnySourceMulticastClient(groupAddress, localPort);
}
<summary>
</summary>
public void Open()
{
if (!isJoined)
{
this.client.BeginJoinGroup(
result =>
{
try
{
this.client.EndJoinGroup(result);
isJoined = true;
this.AfterOpened();
this.Receive();
}
catch (Exception e)
{
throw e;
}
}, null);
}
}
private void AfterOpened()
{
EventHandler<UdpPacketEventArgs> handler = this.OnAfterOpened;
if (handler != null)
handler(this, new UdpPacketEventArgs(string.Empty, string.Empty));
}
private void Receive()
{
if (isJoined)
{
Array.Clear(this.buffer, 0, this.buffer.Length);
this.client.BeginReceiveFromGroup(this.buffer, 0, this.buffer.Length, result =>
{
if (!isDisposed)
{
IPEndPoint source;
try
{
this.client.EndReceiveFromGroup(result, out source);
this.AfterReceived(source, this.buffer);
this.Receive();
}
catch
{
isJoined = false;
this.Open();
}
}
}, null);
}
}
<summary>
</summary>
<param name="message"></param>
public void SendTo(string message)
{
byte[] data = Encoding.UTF8.GetBytes(message);
if (isJoined)
{
this.client.BeginSendToGroup(data, 0, data.Length,
result =>
{
this.client.EndSendToGroup(result);
}, null);
}
}
<summary>
</summary>
<param name="source"></param>
<param name="p"></param>
private void AfterReceived(IPEndPoint source, byte[] p)
{
EventHandler<UdpPacketEventArgs> handler = this.OnPacketReceived;
if (handler != null)
handler(this, new UdpPacketEventArgs(source.Address.ToString(), Encoding.UTF8.GetString(p, 0, p.Length)));
}
private void BeforeClose()
{
EventHandler<UdpPacketEventArgs> handler = this.OnBeforeClosing;
if (handler != null)
handler(this, new UdpPacketEventArgs(string.Empty, string.Empty));
}
#region IDisposable
public void Dispose()
{
if (!isDisposed)
{
Dispose(true);
GC.SuppressFinalize(this);
}
isDisposed = true;
}
~UdpChannel()
{
Dispose(false);
}
private void Dispose(bool disposing)
{
if (disposing)
{
client.Dispose();
}
}
#endregion
}
}
|
III-A-4. Autres objets
| Code de la structure « Whisper » |
namespace Whisper.PhoneApp
{
public struct Whisper
{
public string Message;
public string User;
}
}
|
| Code de l'argument « UdpPacketEventArgs » |
using System;
using System.Text;
namespace Whisper.PhoneApp
{
public class UdpPacketEventArgs : EventArgs
{
public string Message { get; private set; }
public string Source { get; private set; }
public UdpPacketEventArgs(string source, string data)
{
this.Message = data;
this.Source = source;
}
}
}
|
| Code du sérialiseur désérialiseur |
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
namespace Whisper.PhoneApp
{
public static class Serializer<T>
{
public static string SerializeMe(T obj)
{
XmlSerializer serializer = new XmlSerializer(typeof(T));
using (MemoryStream mem = new MemoryStream())
{
serializer.Serialize(mem, obj);
return Encoding.UTF8.GetString(mem.ToArray(), 0, mem.ToArray().Length);
}
}
public static T DeserializeMe(string obj)
{
obj = obj.Replace("\n", "").Replace("\0", "");
XmlSerializer serializer = new XmlSerializer(typeof(T));
using (MemoryStream mem = new MemoryStream(Encoding.UTF8.GetBytes(obj)))
{
using (XmlReader reader = XmlReader.Create(mem))
{
return (T)serializer.Deserialize(reader);
}
}
}
}
}
|
 |
Pour je ne sais quelle raison, il faut remplacer les caractères « \n » et « \0 ».
Si on ne le fait pas, on risque une erreur hexadecimal value 0x00, is an invalid character. Il y a pas mal
de discussions à ce propos sur Internet. Peut-être est-ce dû à la taille du tableau fixée à 1024. Les espaces vides sont
comblés avec des valeurs nulles.
|
III-B. La page principale
Son rôle va être d'ouvrir le canal UDP, de gérer la saisie des messages et leur affichage lors de l'émission ou
de la réception. Voici le code XAML et le résultat en mode design.
III-B-1. Le fichier XAML
| Code du fichier XAML |
<phone:PhoneApplicationPage
x:Class="Whisper.PhoneApp.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="768"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
Foreground="{StaticResource PhoneForegroundBrush}"
SupportedOrientations="Portrait" Orientation="Portrait"
shell:SystemTray.IsVisible="True">
<Grid x:Name="LayoutRoot" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
<TextBlock x:Name="ApplicationTitle" Text="Whisper" Style="{StaticResource PhoneTextNormalStyle}"/>
<TextBlock x:Name="PageTitle" Text="Chat window" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
</StackPanel>
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<TextBox Height="72" HorizontalAlignment="Left" Margin="0,0,0,0" Name="textBox1" Text="Say something"
VerticalAlignment="Top" Width="460" GotFocus="textBox1_GotFocus" />
<Button Content="Send" Height="72" HorizontalAlignment="Left" Margin="0,70,0,0" Name="button1"
VerticalAlignment="Top" Width="160" Click="button1_Click" />
<ScrollViewer VerticalScrollBarVisibility="Auto" Margin="0,148,0,6" Name="scroller1">
<TextBlock HorizontalAlignment="Left" Margin="0,0,0,0" Name="textBlock1" Text=""
VerticalAlignment="Top" Width="450" ScrollViewer.VerticalScrollBarVisibility="Auto" />
</ScrollViewer>
<TextBox Height="72" HorizontalAlignment="Right" Margin="0,70,0,0" Name="textBox2" Text="Username"
VerticalAlignment="Top" Width="300" GotFocus="textBox2_GotFocus" />
</Grid>
</Grid>
</phone:PhoneApplicationPage>
|

La page principale en mode design.
III-B-2. Code behind
| Code behind de la page principale |
using System;
using System.Net;
using System.Text;
using System.Windows;
using Microsoft.Phone.Controls;
namespace Whisper.PhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
UdpChannel channel;
Whisper whisper = new Whisper();
public MainPage()
{
InitializeComponent();
channel = new UdpChannel(IPAddress.Parse("224.109.108.106"), 3000);
this.channel.OnAfterOpened += new EventHandler<UdpPacketEventArgs>(channel_OnAfterOpen);
this.channel.OnPacketReceived += new EventHandler<UdpPacketEventArgs>(Channel_PacketReceived);
this.channel.Open();
}
<summary>
</summary>
<param name="sender"></param>
<param name="e"></param>
void channel_OnAfterOpen(object sender, UdpPacketEventArgs e)
{
textBlock1.Text = "You joined the group " + channel.groupAddress;
whisper.User = textBox2.Text;
whisper.Message = "Hello";
channel.SendTo(Serializer<Whisper>.SerializeMe(whisper));
}
<summary>
</summary>
<param name="sender"></param>
<param name="e"></param>
void Channel_PacketReceived(object sender, UdpPacketEventArgs e)
{
whisper = Serializer<Whisper>.DeserializeMe(e.Message);
textBlock1.Dispatcher.BeginInvoke(() =>
{
textBlock1.UpdateLayout();
textBlock1.Text += Environment.NewLine + whisper.User + " says: " + whisper.Message;
scroller1.UpdateLayout();
scroller1.ScrollToVerticalOffset(textBlock1.ActualHeight);
});
}
private void button1_Click(object sender, RoutedEventArgs e)
{
whisper.User = textBox2.Text;
whisper.Message = textBox1.Text;
channel.SendTo(Serializer<Whisper>.SerializeMe(whisper));
textBox1.Text = string.Empty;
}
private void textBox1_GotFocus(object sender, RoutedEventArgs e)
{
textBox1.Text = string.Empty;
}
private void textBox2_GotFocus(object sender, RoutedEventArgs e)
{
textBox2.Text = string.Empty;
}
}
}
|
III-C. Test
Compilez l'application en mode « Release ». Déployez-la sur le mobile avec l'outil
« Application Deployment » en sélectionnant le fichier « xap » du dossier « \Bin\Release ».
Sur Visual Studio, lancez l'application en mode « Debug » ou « Release » selon ce que vous souhaitez.
Parallèlement, sélectionnez l'application dans la liste du téléphone. La page principale s'affiche rapidement. Lors du démarrage,
l'application envoie un premier message au groupe avec les valeurs par défaut des « TextBox ». La première à
être prête affichera deux fois le même message de bienvenue.
Les images ci-dessous montrent l'émulateur et le mobile. Dans l'émulateur, j'ai saisi mon pseudo ainsi que le message
« hello world! ». Puis, une fois le bouton « Send » actionné, le message a été envoyé au groupe.
Tous les appareils liés l'ont reçu et affiché.

L'émulateur et le téléphone échangent des messages.
IV. Pour aller plus loin et autres détails
La concaténation des messages du bloc n'est certainement pas la meilleure façon de procéder. Il vaudrait mieux garder
en mémoire une liste des objets « Whisper » échangés et lier cette source de données avec un contrôle. Cette
source de données pourrait être sauvegardée localement par la suite.
Il faudrait probablement vérifier que le message envoyé ne dépasse pas la taille du tampon sinon

.
Quand le thread est occupé à recevoir ou écouter il n'est pas possible de modifier l'interface utilisateur. Il faut donc
faire appel au «
Dispatcher » qui va mettre ces modifications dans la file d'attente.
Les performances de l'application seront améliorées grâce à l'utilisation de la
sérialisation JSON. En effet, celle-ci envoie moins de texte que la sérialisation XML. Voici
deux méthodes à ajouter à la classe « Serializer<T> » pour utiliser ce mode de sérialisation.
| Méthode générique de sérialisation JSON |
public static string JsonSerializeMe(T obj)
{
using (MemoryStream mem = new MemoryStream())
{
DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(T));
ser.WriteObject(mem, obj);
byte[] s = mem.ToArray();
return Encoding.UTF8.GetString(s, 0, s.Length);
}
}
|
| Méthode générique de désérialisation JSON |
public static T JsonDeserializeMe(string obj)
{
obj = obj.Replace("\n", "").Replace("\0", "");
using (MemoryStream mem = new MemoryStream(Encoding.UTF8.GetBytes(obj)))
{
DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(T));
return (T)ser.ReadObject(mem);
}
}
|
Pour pouvoir les utiliser, il faut ajouter les références aux assemblies « System.ServiceModel.Web »
et « System.Runtime.Serialization » au projet. Il faut aussi ajouter les attributs
« DataContract » et « DataMember » comme dans le code ci-dessous :
| Code de la structure « Whisper » |
using System.Runtime.Serialization;
namespace Whisper.PhoneApp
{
[DataContract]
public struct Whisper
{
[DataMember]
public string Message;
[DataMember]
public string User;
}
}
|
La page pricipale doit aussi être modifiée pour utiliser les bonnes méthodes. Vous pouvez le faire manuellement ou bien
implémenter un délégué. Comme les méthodes de sérialisation XML et JSON ont la même signature, vous pouvez déclarer des
délégués pour gérer l'une et l'autre des méthodes. Ainsi, si vous souhaitez changer pour l'une ou l'autre des méthodes de
sérialisation, il suffira d'intervenir uniquement sur l'instanciation des délégués.
| Délégation de la sérialisation |
public partial class MainPage : PhoneApplicationPage
{
delegate string SerializerDelegate(Whisper obj);
delegate Whisper DeserializerDelegate(string obj);
SerializerDelegate serializer = new SerializerDelegate(Serializer<Whisper>.JsonSerializeMe);
DeserializerDelegate deserializer = new DeserializerDelegate(Serializer<Whisper>.JsonDeserializeMe);
}
|
Il suffit ensuite d'appeler le bon délégué lorsque c'est nécessaire.
| Sérialisation et envoi d'un message |
channel.SendTo(serializer(whisper));
|
| Désérialisation d'un message |
whisper = deserializer(e.Message)
|
Conclusion
Malheureusement, ce programme nécessite un point d'accès pour fonctionner. Les deux appareils doivent être sur le même réseau.
Il ne s'agit pas d'une connexion ad hoc. Malgré ce petit inconvénient, ce nouveau mode de communication ouvre de
nouveaux horizons pour les téléphones Windows. Nous allons pouvoir développer des applications clientes (sans serveur)
multiutilisateurs. Cela promet des activités très ludiques, jouables partout sans connexion Internet.
Remerciements
Références supplémentaires


Les sources présentées sur cette page sont libres de droits
et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation
constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ©
2011 Immobilis. Aucune reproduction,
même partielle, ne peut être faite de ce site et de l'ensemble de son contenu :
textes, documents, images, etc. sans l'autorisation expresse de l'auteur.
Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 €
de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.
Cette page est déposée.