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.
Ce tutoriel adapte le code Windows Phone Peer-to-Peer Multiplayer Game using Sockets in XNAWindows Phone Peer-to-Peer Multiplayer Game using Sockets in XNA du blogueur Ricky_TRicky_T à une application Windows Phone.
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, classeUdpAnySourceMulticastClient, 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 »▲
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▲
Pour bien comprendre, je vous invite à lire le tutoriel de François GuillotFrançois Guillot intitulé « Introduction aux délégués en C#Introduction aux délégués en C# ». Tout particulièrement le paragraphe .NET 3.5 et les expressions lambda.NET 3.5 et les expressions lambda.
De même, reportez-vous aux pages suivantes pour avoir plus de détails sur les méthodes BeginJoinGroupBeginJoinGroup et EndJoinGroupEndJoinGroup.
///
<
summary
>
/// Ouverture de la connexion
///
<
/summary
>
public
void
Open
(
)
{
if
(!
isJoined)
{
// Lie le socket et commence une opération de jointure au groupe multicast
// pour autoriser la réception de datagrammes provenant de tous les participants de groupe.
this
.
client.
BeginJoinGroup
(
result =>
{
try
{
// Termine l'opération d'envoi de groupe de jointure asynchrone à un groupe multicast.
this
.
client.
EndJoinGroup
(
result);
isJoined =
true
;
// Appel de la méthode d'après ouverture qui va déclencher l'évènement correspondant.
this
.
AfterOpened
(
);
// Mise à l'écoute.
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 : BeginReceiveFromGroupBeginReceiveFromGroup et EndReceiveFromGroupEndReceiveFromGroup.
private
void
Receive
(
)
{
if
(
isJoined)
{
Array.
Clear
(
this
.
buffer,
0
,
this
.
buffer.
Length);
// Commence l'opération de réception d'un paquet provenant du groupe multicast joint et
// appelle le rappel spécifié lorsqu'un paquet est arrivé sur le groupe en provenance de l'un des expéditeurs.
this
.
client.
BeginReceiveFromGroup
(
this
.
buffer,
0
,
this
.
buffer.
Length,
result =>
{
if
(!
isDisposed)
{
// Déclaration d'un IPEndPoint qui permettra de connaître l'adresse IP de l'émetteur
IPEndPoint source;
try
{
// Termine l'opération asynchrone de réception d'un paquet provenant du
// groupe multicast joint et fournit les informations reçues.
this
.
client.
EndReceiveFromGroup
(
result,
out
source);
// Appel de la méthode d'après réception qui va déclencher l'évènement correspondant.
this
.
AfterReceived
(
source,
this
.
buffer);
// Mise à l'écoute.
this
.
Receive
(
);
}
// En cas d'erreur on tente d'ouvrir de nouveau la connexion.
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 « UdpSingleSourceMulticastClientUdpSingleSourceMulticastClient » 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.
using
System;
using
System.
Net;
using
System.
Net.
Sockets;
using
System.
Text;
namespace
Whisper.
PhoneApp
{
public
class
UdpChannel :
IDisposable
{
// Client Udp
private
UdpAnySourceMulticastClient client;
// Tampon de stockage des messages
private
byte
[]
buffer =
new
byte
[
1024
];
// Port d'écoute et d'émission
private
int
localPort;
// Variable indiquant l'état de connexion avec le groupe
private
bool
isJoined;
// Implémentation de l'interface IDisposable
private
bool
isDisposed;
// Adresse du groupe
public
IPAddress groupAddress {
get
;
private
set
;
}
// Évènement déclenché lors de la réception d'un paquet
public
event
EventHandler<
UdpPacketEventArgs>
OnPacketReceived;
// Évènement déclenché après l'ouverture de la connexion au groupe
public
event
EventHandler<
UdpPacketEventArgs>
OnAfterOpened;
// Évènement déclenché avant la fermeture de la connexion
public
event
EventHandler<
UdpPacketEventArgs>
OnBeforeClosing;
///
<
summary
>
/// Constructeur
///
<
/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
>
/// Ouverture de la connexion
///
<
/summary
>
public
void
Open
(
)
{
if
(!
isJoined)
{
// Lie le socket et commence une opération de jointure au groupe multicast
// pour autoriser la réception de datagrammes provenant de tous les participants de groupe.
this
.
client.
BeginJoinGroup
(
result =>
{
try
{
// Termine l'opération d'envoi de groupe de jointure asynchrone à un groupe multicast.
this
.
client.
EndJoinGroup
(
result);
isJoined =
true
;
// Appel de la méthode d'après ouverture qui va déclencher l'évènement correspondant.
this
.
AfterOpened
(
);
// Mise à l'écoute.
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);
// Commence l'opération de réception d'un paquet provenant du groupe multicast joint et
// appelle le rappel spécifié lorsqu'un paquet est arrivé sur le groupe en provenance de l'un des expéditeurs.
this
.
client.
BeginReceiveFromGroup
(
this
.
buffer,
0
,
this
.
buffer.
Length,
result =>
{
if
(!
isDisposed)
{
// Déclaration d'un IPEndPoint qui permettra de connaître l'adresse IP de l'émetteur
IPEndPoint source;
try
{
// Termine l'opération asynchrone de réception d'un paquet provenant du
// groupe multicast joint et fournit les informations reçues.
this
.
client.
EndReceiveFromGroup
(
result,
out
source);
// Appel de la méthode d'après réception qui va déclencher l'évènement correspondant.
this
.
AfterReceived
(
source,
this
.
buffer);
// Mise à l'écoute.
this
.
Receive
(
);
}
// En cas d'erreur on tente d'ouvrir de nouveau la connexion.
catch
{
isJoined =
false
;
this
.
Open
(
);
}
}
},
null
);
}
}
///
<
summary
>
/// Permet d'envoyer les paquets
///
<
/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
>
/// Méthode appelée après la réception d'un paquet
///
<
/summary
>
///
<
param
name
=
"source"
><
/param
>
///
<
param
name
=
"p"
><
/param
>
private
void
AfterReceived
(
IPEndPoint source,
byte
[]
p)
{
EventHandler<
UdpPacketEventArgs>
handler =
this
.
OnPacketReceived;
if
(
handler !=
null
)
// Passage des arguments au gestionnaire: l'adresse IP de l'émetteur, le chuchotement sérialisé
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▲
namespace
Whisper.
PhoneApp
{
public
struct
Whisper
{
public
string
Message;
public
string
User;
}
}
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;
}
}
}
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)
{
// Cette ligne est importante pour éviter une erreur de caractère hexadécimal invalide.
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▲
<
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>
III-B-2. Code behind▲
using
System;
using
System.
Net;
using
System.
Text;
using
System.
Windows;
using
Microsoft.
Phone.
Controls;
namespace
Whisper.
PhoneApp
{
public
partial
class
MainPage :
PhoneApplicationPage
{
// Déclaration du canal
UdpChannel channel;
// Déclaration et instanciation d'un chuchotement
Whisper whisper =
new
Whisper
(
);
public
MainPage
(
)
{
InitializeComponent
(
);
// Instanciation du canal sur l'adresse multicast 224.109.108.106 et le port 3000
channel =
new
UdpChannel
(
IPAddress.
Parse
(
"224.109.108.106"
),
3000
);
// Abonnement de la page à l'évènement OnAfterOpened du canal
this
.
channel.
OnAfterOpened +=
new
EventHandler<
UdpPacketEventArgs>(
channel_OnAfterOpen);
// Abonnement de la page à l'évènement OnPacketReceived du canal
this
.
channel.
OnPacketReceived +=
new
EventHandler<
UdpPacketEventArgs>(
Channel_PacketReceived);
// Ouverture du canal
this
.
channel.
Open
(
);
}
///
<
summary
>
/// Méthode appelée après l'ouverture du canal UDP
///
<
/summary
>
///
<
param
name
=
"sender"
><
/param
>
///
<
param
name
=
"e"
><
/param
>
void
channel_OnAfterOpen
(
object
sender,
UdpPacketEventArgs e)
{
// Affichage d'un message informant l'utilisateur de la connexion
textBlock1.
Text =
"You joined the group "
+
channel.
groupAddress;
whisper.
User =
textBox2.
Text;
whisper.
Message =
"Hello"
;
// Envoi d'un petit bonjour au groupe
channel.
SendTo
(
Serializer<
Whisper>.
SerializeMe
(
whisper));
}
///
<
summary
>
/// Méthode appelée lors de la réception d'un paquet UDP
///
<
/summary
>
///
<
param
name
=
"sender"
><
/param
>
///
<
param
name
=
"e"
><
/param
>
void
Channel_PacketReceived
(
object
sender,
UdpPacketEventArgs e)
{
// Désérialisation du message
whisper =
Serializer<
Whisper>.
DeserializeMe
(
e.
Message);
// Mise à jour différée de l'interface pour éviter une erreur "Invalid cross-thread access"
textBlock1.
Dispatcher.
BeginInvoke
((
) =>
{
textBlock1.
UpdateLayout
(
);
// Affichage du message
textBlock1.
Text +=
Environment.
NewLine +
whisper.
User +
" says: "
+
whisper.
Message;
scroller1.
UpdateLayout
(
);
// Déplacement de l'ascenseur jusqu'au dernier message reçu
scroller1.
ScrollToVerticalOffset
(
textBlock1.
ActualHeight);
}
);
}
private
void
button1_Click
(
object
sender,
RoutedEventArgs e)
{
// Assignation de la saisie sur l'instance de l'objet Whisper
whisper.
User =
textBox2.
Text;
whisper.
Message =
textBox1.
Text;
// Sérialisation et envoi du message
channel.
SendTo
(
Serializer<
Whisper>.
SerializeMe
(
whisper));
// Effacement du contenu de la textbox
textBox1.
Text =
string
.
Empty;
}
private
void
textBox1_GotFocus
(
object
sender,
RoutedEventArgs e)
{
// Effacement du contenu de la textbox lors du focus
textBox1.
Text =
string
.
Empty;
}
private
void
textBox2_GotFocus
(
object
sender,
RoutedEventArgs e)
{
// Effacement du contenu de la textbox lors du focus
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é.
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 « DispatcherDispatcher » 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 JSONsé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.
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);
}
}
public
static
T JsonDeserializeMe
(
string
obj)
{
// Cette ligne est importante pour éviter une erreur de caractère hexadécimal invalide.
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 :
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.
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);
/*
* Reste du code
*/
}
Il suffit ensuite d'appeler le bon délégué lorsque c'est nécessaire.
channel.
SendTo
(
serializer
(
whisper));
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▲
Merci à GuruuMeditationGuruuMeditation d'avoir résolu mon bogue. Merci à ClaudeLELOUPClaudeLELOUP pour sa relecture.