Georg fährt extra nach Berlin um Steve Ballmer zu treffen

31 Tage Mango | Tag #21: Sockets

Die­ser Arti­kel ist Tag #21 der Serie 31 Tage Man­go von Jeff Blankenburg.

Der Ori­gi­nal­ar­ti­kel befin­det sich hier: Day #21: Sockets.

Die­ser Arti­kel wur­de in der Ori­gi­nal­se­rie von Gast­au­tor Parag Joshi geschrie­ben. Bei Twit­ter kann Parag unter @ilovethexbox erreicht werden.

Heu­te beschäf­ti­gen wir uns mit Sockets. Die Unter­stüt­zung für Sockets ist neu im Win­dows Pho­ne SDK 7.1 gekom­men. Aber was sind Sockets über­haupt? Etwas ver­ein­facht sind sie ein Kom­mu­ni­ka­ti­ons­me­cha­nis­mus basie­rend auf TCP/IP, der zum Bei­spiel zum Dru­cken, für Video­kon­fe­ren­zen und vie­les mehr ver­wen­det wird.

Bevor wir eine Win­dows Pho­ne Anwen­dung mit Sockets bau­en, las­sen Sie uns erst noch genau­er anschau­en, was Sockets sind.

Sockets — was ist das?

Anwen­dun­gen kön­nen über Sockets basie­rend auf einer Socket­adres­se über ein loka­les Netz­werk oder über das Inter­net kom­mu­ni­zie­ren. Eine Socket­adres­se ist die Kom­bi­na­ti­on aus einer IP Adres­se und einem Port.

Der unten emp­foh­le­ne Arti­kel gibt eine gute Erklä­rung im Hin­blick auf die dar­un­ter­lie­gen­de Tech­no­lo­gie. Als Ent­wick­ler muss man eigent­lich nur wis­sen, wie man sich ver­bin­det (sofern man TCP/IP benutzt) und wie man Daten sen­det und empfängt.

Sockets sind eine ziem­lich sys­tem­na­he Tech­no­lo­gie. Sie müs­sen selbst fest­le­gen, wie die zu sen­den­den und zu emp­fan­gen­den Daten aus­se­hen. Das erhöht zwar einer­seits die Feh­ler­an­fäl­lig­keit beim Par­sen der Daten — ande­rer­seits kön­nen Sie durch die effi­zi­en­te Gestal­tung des Nach­rich­ten­for­mats höhe­re Geschwin­dig­kei­ten errei­chen als bei der Ver­wen­dung von Webservices.

Hier ist der Link zu einem aus­führ­li­chen Arti­kel über Sockets und deren Unter­stüt­zung in Win­dows Pho­ne 7.1:

http://msdn.microsoft.com/en-us/library/hh202874(v=vs.92).aspx.

Was werden wir machen?

Wir bau­en eine klei­ne Anwen­dung, um Bestel­lun­gen in einem Restau­rant auf­zu­neh­men. Sie wird bestehen aus einer klei­nen Win­dows Pho­ne Anwen­dung, wel­che die Bestel­lun­gen auf­nimmt, und einer Sil­ver­light Anwen­dung für den PC, um die Bestel­lun­gen emp­fan­gen und zu ver­ar­bei­ten. Das grund­le­gen­de Kon­zept ist also: Ser­ver auf den Win­dows Pho­ne Gerä­ten neh­men die Bestel­lun­gen auf und der Host emp­fängt die Bestel­lun­gen und sen­det den Sta­tus der Bestel­lung sobald die­se abge­schlos­sen ist.

Die Ser­ver wer­den zu jeder abge­schlos­se­nen Bestel­lung benachrichtigt.

Die­se Anwen­dung basiert auf der „Rock, Paper, Scis­sors“ („Papier, Sche­re, Stein“) Mul­ti­cast Anwen­dung auf MSDN (http://msdn.microsoft.com/en-us/library/ff431744(v=vs.92).aspx).

Las­sen Sie uns anfangen!

Kurzer Überblick

Auf jeden Fall wer­den wir eine Host-Anwen­dung brau­chen, wel­che Daten von den Cli­ents emp­fängt. Für die Demo wäh­len wir eine Sil­ver­light (Out of Brow­ser) Anwendung.

Wich­tig: Wir müs­sen die Sil­ver­light Anwen­dung im „Out of Brow­ser“ Modus mit erhöh­ten Rech­ten aus­füh­ren, damit die­se Zugriff auf die Sockets hat.

Wir müs­sen uns auf das For­mat der Daten eini­gen, die zum Host gesen­det wer­den. Wir brau­chen eine Tisch­num­mer, Infor­ma­tio­nen zur Bestel­lung und wie scharf das Essen gewürzt sein soll.

/// <summary>
/// When we receive the Order, we pass on the table number and other parameters
/// </summary>
public class OrderReceivedEventArgs : EventArgs
{
   public int tableNumber { get; set; }
   public int spiceLevel { get; set; }
   public string order { get; set; }
}

Wir wer­den wei­ter­hin die Win­dows Pho­ne Gerä­te anzei­gen, die sich mit dem Host ver­bun­den haben. Damit kann die Per­son, die den Host ver­wal­tet, sehen wie vie­le Ser­ver gera­de arbei­ten. Die­se Infor­ma­ti­on stammt aus dem „Device­Info“ Objekt. Hier ist der Aus­schnitt der Deklaration:

/// <summary>
/// The information about each device. This class must implement INotifyPropertyChanged as it is
/// bound to a datagrid using an observable collection.
/// </summary>
public class DeviceInfo : INotifyPropertyChanged
{
   private IPEndPoint _DeviceEndPoint;
   private string _DeviceOrServerName;
   public DeviceInfo(string deviceOrServerName, IPEndPoint endPoint)
   {
      _DeviceEndPoint = endPoint;
      _DeviceOrServerName = deviceOrServerName;
   }
}

Hier ist ein Screen­shot des Hosts:

Jetzt brau­chen wir noch die Win­dows Pho­ne Cli­ents um Bestel­lun­gen auf­zu­neh­men. Die­se Anwen­dung wird also die oben dekla­rier­ten Daten auf­neh­men und abschicken.

Hier ist ein Screen­shot der Win­dows Pho­ne Anwendung:

Wie Sie sehen, steht ganz oben der Ser­ver­na­me. Momen­tan wir das von der Anwen­dung fest­ge­legt. Als Erwei­te­rung könn­ten wir dem Anwen­der die Mög­lich­keit bie­ten, sich ein­zu­log­gen und dann den Namen des Ser­vers anzei­gen, auf dem er sich ein­ge­loggt hat.

Wir haben zwei Slider:

  1. Mit dem ers­ten Slider wäh­len wir die Tisch­num­mer. Er reicht von 1 bis 10. Das ist natür­lich eine etwas pri­mi­ti­ve Mög­lich­keit der Tisch­aus­wahl — für unse­re Zwe­cke reicht sie aber.
  2. Mit dem zwei­ten Slider legen wir fest, wie scharf das Essen sein soll. Auch die­ser Slider reicht von 1 bis 10.

Als drit­tes Ele­ment haben wir eine Text­Box um die Details zur Bestel­lung ein­zu­ge­ben. Wenn Sie die Demo aus­bau­en wol­len, könn­ten sie zum Bei­spiel eine List­Box zur Aus­wahl des Gerichts ein­bau­en. Für unse­re Demo habe ich es bei einer ein­fa­chen Text­Box belassen.

Als letz­tes haben wir noch einen But­ton, um die Bestel­lung abzu­schi­cken. Das war’s!

Die fol­gen­den Punk­te sind noch bemerkenswert:

  1. Die Klas­se Restau­rant­Com­mands: Die­se Klas­se ent­hält die von unse­rer Socket-Anwen­dung erlaub­ten Kom­man­dos. Sie wird sowohl im Cli­ent als auch im Ser­ver ent­hal­ten sein und stellt sicher, dass bei­de eine gemein­sa­me Spra­che sprechen.
  2. UdpAnySourceMulticastChannel.cs und UdpPacketReceivedEventArgs.cs: Die­se bei­den Datei­en habe ich aus der SDK Mul­ti­cast Anwen­dung über­nom­men, die Code für UDP Mul­ti­cast Sockets beinhaltet.
  3. Die Klas­se Order: Die­se Klas­se erle­digt die gan­ze Kom­mu­ni­ka­ti­on. Die Kom­mu­ni­ka­ti­on besteht dabei aus den Kom­man­dos, die wir in der Klas­se Restau­rant­Com­mands defi­niert haben. Die­se Kom­man­dos bil­den also das Voka­bu­lar, das wir über­tra­gen, emp­fan­gen und inter­pre­tie­ren können.

Wie die Beispielanwendung funktioniert

Vor­be­rei­tun­gen: Instal­lie­ren Sie die Man­go Tools von http://create.msdn.com/. Damit haben Sie das Visu­al Stu­dio Express 2010 und das Win­dows Pho­ne SDK. Da wir auch auf dem PC eine Sil­ver­light Anwen­dung haben wer­den, laden Sie noch das Visu­al Stu­dio C# Express Edi­ti­on her­un­ter, um die nöti­gen Pro­jekt­vor­la­gen zu bekommen.

  • Öff­nen Sie das Visu­al Stu­dio, navi­gie­ren Sie zur Solu­ti­on Datei und öff­nen Sie diese.
  • Ach­tung: die Solu­ti­on Datei hat zwei Start­up-Pro­jek­te. Das Pro­jekt „Tak­e­MyOr­der“ ist das Win­dows Pho­ne Pro­jekt und „Poin­tOfSa­le­App“ ist die Desk­top Anwendung.
  • Dies ist ein Bei­spiel für Mul­ti­cast Sockets. Um es aus­zu­pro­bie­ren, müs­sen Sie das Win­dows Pho­ne Pro­jekt auf ein ech­tes Gerät instal­lie­ren und die Sil­ver­light Anwen­dung auf dem PC lau­fen lassen.
  • Wenn Sie kein Gerät haben, kön­nen Sie alter­na­tiv die Sil­ver­light Desk­top­an­wen­dung auf einem ande­ren Rech­ner instal­lie­ren (z.B. in einer vir­tu­el­len Maschi­ne) und die Win­dows Pho­ne Anwen­dung im Emu­la­tor auf dem Host.
  • Sie kön­nen die Sil­ver­light Desk­top­an­wen­dung und die Win­dows Pho­ne Anwen­dung im Emu­la­tor nicht auf der sel­ben Maschi­ne ausführen.
  • Wenn Sie meh­re­re Gerä­te haben, kön­nen Sie die Win­dows Pho­ne Anwen­dung auf allen gleich­zei­tig lau­fen las­sen. Damit haben Sie meh­re­re Kun­den, die gleich­zei­tig Bestel­lun­gen aufgeben.
  • Da wir das UDP Pro­to­koll ver­wen­den, gibt es kei­ne Garan­tie, dass Nach­rich­ten erfolg­reich gesen­det und emp­fan­gen wer­den. Das liegt dar­an, dass wir kei­ne direk­te Ende-zu-Ende Ver­bin­dung haben. Die Gerä­te neh­men nur an einer Mul­ti­cast-Grup­pe teil, die über eine IP Adres­se und einen Port iden­ti­fi­ziert wird. Sie wer­den aber sehen, dass die Kom­mu­ni­ka­ti­on recht zuver­läs­sig ist.

Der Ablauf:

  • Star­ten Sie die Sil­ver­light Out of Brow­ser Anwendung.
  • Star­ten Sie die Win­dows Pho­ne Anwen­dung auf dem Gerät oder im Emu­la­tor auf einem ande­ren Rechner.
  • Der Host wird den Namen des Ser­vers und des­sen Adres­se anzeigen.
  • Sie sind jetzt bereit, eine Bestel­lung aufzugeben.
  • Wäh­len Sie einen Tisch zwi­schen 1 und 10 in der Win­dows Pho­ne Anwen­dung, indem Sie den Slider bewegen.
  • Geben Sie eine Bestel­lung ein. Z.B. frit­tier­ten Reis mit Hün­chen und Shrimps, etc.
  • Wäh­len Sie die Schär­fe des Gerichts mit dem ent­spre­chen­den Slider.
  • Drü­cken Sie auf Order. Die Win­dows Pho­ne Anwen­dung schickt jetzt das „Send­Or­der“ Kom­man­do an alle Teil­neh­mer der Grup­pe. Wir hät­ten das auch fil­tern kön­nen, so dass nur an den Host gesen­det wird.
  • Die Bestel­lung wird auf dem Host emp­fan­gen und zur Tabel­le „Inco­ming Orders“ hin­zu­ge­fügt. Sie wird dort im Sta­tus „In Pro­cess“ auf­tau­chen und ihr wird eine ein­deu­ti­ge Order ID (abhän­gig von der Anzahl der ein­ge­gan­ge­nen Bestel­lun­gen) zugewiesen.
  • Ange­nom­men, Sie haben eine schnel­le Küche und die Bestel­lung ist bereit, sobald Sie ein­trifft. Kli­cken Sie die Check­box neben „In Pro­cess“ und drü­cken Sie auf den „Order Rea­dy“ But­ton. Der Host sen­det jetzt den Sta­tus der Bestel­lung an alle Gerä­te im Netz­werk und aktua­li­siert den Sta­tus in der Lis­te der Bestel­lun­gen. Für Details zu INo­ti­fy­Pro­per­ty­Ch­an­ged sehen Sie wei­ter unten nach.
  • Alle Win­dows Pho­ne Gerä­te emp­fan­gen den Sta­tus der Bestel­lung. Auch hier hät­ten wir wie­der fil­tern kön­nen, so dass nur das Gerät die aktua­li­sier­te Bestel­lung emp­fängt, das sie auch abge­schickt hat.

Hier ist ein Screen­shot, nach­dem eine Bestel­lung abge­schlos­sen wurde:

Wie der Code funktioniert:

Hier sind die ent­schei­den­den Stel­len zum Ver­ständ­nis des Codes.

Der Grup­pe bei­tre­ten: Der fol­gen­de Code erle­digt den Bei­tritt in die Gruppe:

/// <summary>
/// All communication takes place using a UdpAnySourceMulticastChannel.
/// A UdpAnySourceMulticastChannel is a wrapper we create around the UdpAnySourceMulticastClient.
/// </summary>
/// <value>The channel.</value>
private UdpAnySourceMulticastChannel Channel { get; set; }

/// <summary>
/// The IP address of the multicast group.
/// </summary>
/// <remarks>
/// A multicast group is defined by a multicast group address, which is an IP address
/// that must be in the range from 224.0.0.0 to 239.255.255.255. Multicast addresses in
/// the range from 224.0.0.0 to 224.0.0.255 inclusive are well-known reserved multicast
/// addresses. For example, 224.0.0.0 is the Base address, 224.0.0.1 is the multicast group
/// address that represents all systems on the same physical network, and 224.0.0.2 represents
/// all routers on the same physical network.The Internet Assigned Numbers Authority (IANA) is
/// responsible for this list of reserved addresses. For more information on the reserved
/// address assignments, please see the IANA website.
/// http://go.microsoft.com/fwlink/?LinkId=221630
/// </remarks>
private const string GROUP_ADDRESS = "224.0.1.11";

/// <summary>
/// This defines the port number through which all communication with the multicast group will take place.
/// </summary>
/// <remarks>
/// The value in this example is arbitrary and you are free to choose your own.
/// </remarks>
private const int GROUP_PORT = 54329;

Das fol­gen­de Kom­man­do wird abge­setzt, um der Grup­pe beizutreten:

//Send command to join the multi cast group
App.Order.Join(serverName);

Und schlie­ßich öff­net die Metho­de Join einen Kommunikationskanal.

/// <summary>
/// Join the multicast group.
/// </summary>
/// <param name="serverName">The server name I want to join as.</param>
/// <remarks>The server name is not needed for multicast communication. it is
/// used in this example to identify each member of the multicast group with
/// a friendly name. </remarks>
public void Join(string serverName)
{
   if (IsJoined)
   {
      return;
   }

   // Store my player name
   _serverName = serverName;

   //Open the connection
   this.Channel.Open();
}

An dem Kanal mel­den wir uns für eini­ge Events zur Ver­ar­bei­tung an:

/// <summary>
/// Register for events on the multicast channel.
/// </summary>
private void RegisterEvents()
{
   // Register for events from the multicast channel
   this.Channel.Joined += new EventHandler(Channel_Joined);
   this.Channel.BeforeClose += new EventHandler(Channel_BeforeClose);
   this.Channel.PacketReceived += new EventHandler<UdpPacketReceivedEventArgs>(Channel_PacketReceived);
}

Beson­ders her­vor­he­bens­wert ist der Event „Packet­Re­cei­ved“. Alle emp­fan­ge­nen Kom­man­dos (auf dem Gerät und auf dem Host) wer­den in die­sem Event Hand­ler ver­ar­bei­tet. Hier par­sen und iden­ti­fi­zie­ren wir das ein­ge­trof­fe­ne Kom­man­do. Abhän­gig vom Kom­man­do und der Anzahl der über­ge­be­nen Argu­men­te ent­schei­den wir, was wei­ter zu tun ist.

Eine Bestel­lung sen­den: Das fol­gen­de Kom­man­do wird abge­setzt, wenn der Anwen­der auf den Order But­ton drückt:

App.Order.SendOrder(txtOrder.Text, TableSlider.Value.ToString(), spiceSlider.Value.ToString());

Die Metho­de dazu sieht so aus:

/// <summary>
/// Send the order
/// </summary>
/// <param name="Order"></param>
/// <param name="tableNumber"></param>
/// <param name="spiceLevel"></param>
public void SendOrder(string Order,string tableNumber, string spiceLevel)
{
   if (this.Channel != null)
   {
      //Send order to all devices. Only the server will process the send order command. Others will simply ignore it.
      this.Channel.Send(RestaurantCommands.SendOrderFormat, _serverName, Order, tableNumber, spiceLevel);
   }
}

Das „Send­Or­der­For­mat“ haben wir wie folgt definiert:

public const string SendOrder = "SO";
public const string SendOrderFormat = SendOrder + CommandDelimeter + "{0}" + CommandDelimeter + "{1}" + CommandDelimeter + "{2}" + CommandDelimeter + "{3}";

Hier ist schließ­lich noch der Code für die Metho­de Send:

/// <summary>
/// Sends the specified format. This is a multicast message that is sent to all members of the multicast group.
/// </summary>
/// <param name="format">The format.</param>
/// <param name="args">The args.</param>
public void Send(string format, params object[] args)
{
   try
   {
      if (this.IsJoined)
      {
         byte[] data = Encoding.UTF8.GetBytes(string.Format(format, args));
         this.Client.BeginSendToGroup(data, 0, data.Length, new AsyncCallback(SendToGroupCallback), null);
      }
   }
   catch (SocketException socketEx)
   {
      // See if we can do something when a SocketException occurs.
      HandleSocketException(socketEx);
   }
   catch (InvalidOperationException)
   {
      Debug.WriteLine("BeginSendToGroup IOE");
   }
}

Wie wir sehen, wir die Nach­richt ent­spre­chend des Nach­rich­ten­for­mats for­ma­tiert und dann übertragen.

Eine Bestel­lung emp­fan­gen: Auf dem Host wird eine Nach­richt im Hand­ler emp­fan­gen, den wir für den „Packet­Re­cei­ved“ Event defi­niert haben. Wir par­sen die Nach­richt und iden­ti­fi­zie­ren das Kommando.

string message = e.Message.Trim('\0');
string[] messageParts = message.Split(RestaurantCommands.CommandDelimeter.ToCharArray());

else if (messageParts.Length == 5 && messageParts[0]== RestaurantCommands.SendOrder)
{
   //Status of order received
   OrderReceivedEventArgs args = new OrderReceivedEventArgs();
   args.tableNumber = Convert.ToInt32(messageParts[3]);
   args.spiceLevel = Convert.ToInt32( messageParts[4]);
   args.order = messageParts[2];
   if (DataReceivedFromDevice != null)
   {
      DataReceivedFromDevice(this, args);
   }
}

Sobald wir unse­re Argu­men­te für die Metho­de Data Recei­ved iden­ti­fi­ziert, gepar­sed und erstellt haben, rufen wir die­se auf.

In der Host­an­wen­dung behan­deln wir die­sen Event und fügen die ein­ge­gan­ge­ne Bestel­lung unse­rer Orders Coll­ec­tion hinzu.

/// <summary>
/// Handles incoming orders
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void Order_DataReceivedFromDevice(object sender, OrderReceivedEventArgs e)
{
   int OrderID = App.OrdersCollection.Count +1;
   Orders objOrders = new Orders(OrderID, e.tableNumber, e.spiceLevel, e.order);
   App.OrdersCollection.Add(objOrders);
}

Bekannte Probleme

Ab und zu bekom­men Sie eine Feh­ler­mel­dung, wenn Sie das Debug­gen beenden:

File or assem­bly name ‚System.Net.debug.resources, Version=2.0.5.0, Culture=en-US, PublicKeyToken=7cec85d7bea7798e‘, or one of its depen­den­ci­es, was not found.

Das ist ein bekann­tes Pro­blem, wel­ches hier beschrie­ben ist: http://forums.create.msdn.com/forums/p/89666/537141.aspx. Wenn die­ser Feh­ler auf­tritt, müs­sen Sie das Visu­al Stu­dio mög­li­cher­wei­se schlie­ßen und neu starten.

Bestell­sta­tus schi­cken: Zum Sen­den des Bestell­sta­tus ver­wen­den wir einen ähn­li­chen Mecha­nis­mus wie oben:

/// <summary>
/// Sends the order status
/// </summary>
/// <param name="OrderID"></param>
/// <param name="status"></param>
public void SendOrderStatus(int OrderID, string status)
{
   if (this.Channel != null)
   {
      //Send order to all devices. Only the server will process the send order command. Others will simply ignore it.
      this.Channel.Send(RestaurantCommands.ReceiveOrderFormat, _serverName, OrderID, status);
   }
}

public const string ReceiveOrder = "RO";
        
public const string ReceiveOrderFormat = ReceiveOrder + CommandDelimeter + "{0}" + CommandDelimeter + "{1}" + CommandDelimeter + "{2}";

else if (messageParts.Length == 4 && messageParts[0] == RestaurantCommands.ReceiveOrder)
{
   //Status of order received
   OrderStatusReceivedEventArgs args = new OrderStatusReceivedEventArgs();
   args.orderId = Convert.ToInt32(messageParts[2]);
   args.orderStatus = messageParts[3];
   if (DataReceivedFromDevice != null)
   {
      DataReceivedFromDevice(this, args);
   }
}

/// <summary>
/// Shows a message box with the order status
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void Order_DataReceivedFromDevice(object sender, OrderStatusReceivedEventArgs e)
{
   DiagnosticsHelper.SafeShow("Order Status of '" +e.orderStatus + "' Received For Order:" + e.orderId.ToString());
}

INotifyPropertyChanged

Die­ses Inter­face imple­men­tie­ren wir für Objek­te, die wir an Datagrids bin­den wol­len. Auch wenn die Coll­ec­tion wie folgt dekla­riert ist, wird unse­re Benut­zer­schnitt­stel­le nicht aktua­li­sie­ren, wenn die Eigen­schaf­ten nicht selbst einen Pro­per­ty Chan­ged Event feuern.

public static ObservableCollection<DeviceInfo> Devices = new ObservableCollection<DeviceInfo>();

Zusammenfassung

Mit den hier vor­ge­stell­ten Mit­teln soll­te es Ihnen recht leicht fal­len, eine Anwen­dung zu bau­en, die Sockets ver­wen­det. Die Anwen­dung kann zu einer voll­stän­di­gen Restau­rant-Anwen­dung aus­ge­baut wer­den, indem wir sie um die bereits ange­spro­che­nen Fea­tures erweitern:

  • Mög­lich­keit der Aus­wahl eines Gerichts aus einer Speisekarte
  • Anzei­ge der aus­ste­hen­den Bestel­lun­gen auf dem Gerät
  • Mög­lich­keit, sich einzuloggen

Die wesent­li­chen Schrit­te bei der Ent­wick­lung einer Socket-basier­ten Anwen­dung sind:

  • Ent­schei­den Sie sich für den Typ der Socket-Anwen­dung: Mul­ti­cast (UDP) oder Uni­cast (TCP).
  • Defi­nie­ren Sie die aus­zu­tau­schen­den Kommandos.
  • Kom­mu­ni­zie­re.
  • Ver­ar­bei­te.

Mit die­sem Arti­kel soll­ten Sie einen klei­nen Ein­blick in die Socket Pro­gram­mie­rung für Win­dows Pho­ne bekom­men haben. Ich emp­feh­le Ihnen auch die Code­bei­spie­le hier http://msdn.microsoft.com/en-us/library/hh202874(v=vs.92).aspx

Um die kom­plet­te Anwen­dung zur Kom­mu­ni­ka­ti­on über Sockets her­un­ter­zu­la­den, drü­cken Sie auf den Down­load Code Button:

Mor­gen wird Matt Eland Ihnen App Con­nect (auch bekannt als Search Exten­si­bi­li­ty) vor­stel­len. Damit kön­nen Sie Anwen­der auf Ihre Anwen­dung auf­merk­sam machen, auch wenn die­se die Anwen­dung noch gar nicht instal­liert haben.

Bis dahin!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert