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

31 Tage Mango | Tag #21: Sockets

Dieser Artikel ist Tag #21 der Serie 31 Tage Mango von Jeff Blankenburg.

Der Originalartikel befindet sich hier: Day #21: Sockets.

Dieser Artikel wurde in der Originalserie von Gastautor Parag Joshi geschrieben. Bei Twitter kann Parag unter @ilovethexbox erreicht werden.

Heute beschäftigen wir uns mit Sockets. Die Unterstützung für Sockets ist neu im Windows Phone SDK 7.1 gekommen. Aber was sind Sockets überhaupt? Etwas vereinfacht sind sie ein Kommunikationsmechanismus basierend auf TCP/IP, der zum Beispiel zum Drucken, für Videokonferenzen und vieles mehr verwendet wird.

Bevor wir eine Windows Phone Anwendung mit Sockets bauen, lassen Sie uns erst noch genauer anschauen, was Sockets sind.

Sockets – was ist das?

Anwendungen können über Sockets basierend auf einer Socketadresse über ein lokales Netzwerk oder über das Internet kommunizieren. Eine Socketadresse ist die Kombination aus einer IP Adresse und einem Port.

Der unten empfohlene Artikel gibt eine gute Erklärung im Hinblick auf die darunterliegende Technologie. Als Entwickler muss man eigentlich nur wissen, wie man sich verbindet (sofern man TCP/IP benutzt) und wie man Daten sendet und empfängt.

Sockets sind eine ziemlich systemnahe Technologie. Sie müssen selbst festlegen, wie die zu sendenden und zu empfangenden Daten aussehen. Das erhöht zwar einerseits die Fehleranfälligkeit beim Parsen der Daten – andererseits können Sie durch die effiziente Gestaltung des Nachrichtenformats höhere Geschwindigkeiten erreichen als bei der Verwendung von Webservices.

Hier ist der Link zu einem ausführlichen Artikel über Sockets und deren Unterstützung in Windows Phone 7.1:

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

Was werden wir machen?

Wir bauen eine kleine Anwendung, um Bestellungen in einem Restaurant aufzunehmen. Sie wird bestehen aus einer kleinen Windows Phone Anwendung, welche die Bestellungen aufnimmt, und einer Silverlight Anwendung für den PC, um die Bestellungen empfangen und zu verarbeiten. Das grundlegende Konzept ist also: Server auf den Windows Phone Geräten nehmen die Bestellungen auf und der Host empfängt die Bestellungen und sendet den Status der Bestellung sobald diese abgeschlossen ist.

Die Server werden zu jeder abgeschlossenen Bestellung benachrichtigt.

Diese Anwendung basiert auf der „Rock, Paper, Scissors“ („Papier, Schere, Stein“) Multicast Anwendung auf MSDN (http://msdn.microsoft.com/en-us/library/ff431744(v=vs.92).aspx).

Lassen Sie uns anfangen!

Kurzer Überblick

Auf jeden Fall werden wir eine Host-Anwendung brauchen, welche Daten von den Clients empfängt. Für die Demo wählen wir eine Silverlight (Out of Browser) Anwendung.

Wichtig: Wir müssen die Silverlight Anwendung im „Out of Browser“ Modus mit erhöhten Rechten ausführen, damit diese Zugriff auf die Sockets hat.

Wir müssen uns auf das Format der Daten einigen, die zum Host gesendet werden. Wir brauchen eine Tischnummer, Informationen zur Bestellung 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 werden weiterhin die Windows Phone Geräte anzeigen, die sich mit dem Host verbunden haben. Damit kann die Person, die den Host verwaltet, sehen wie viele Server gerade arbeiten. Diese Information stammt aus dem „DeviceInfo“ Objekt. Hier ist der Ausschnitt 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 Screenshot des Hosts:

Jetzt brauchen wir noch die Windows Phone Clients um Bestellungen aufzunehmen. Diese Anwendung wird also die oben deklarierten Daten aufnehmen und abschicken.

Hier ist ein Screenshot der Windows Phone Anwendung:

Wie Sie sehen, steht ganz oben der Servername. Momentan wir das von der Anwendung festgelegt. Als Erweiterung könnten wir dem Anwender die Möglichkeit bieten, sich einzuloggen und dann den Namen des Servers anzeigen, auf dem er sich eingeloggt hat.

Wir haben zwei Slider:

  1. Mit dem ersten Slider wählen wir die Tischnummer. Er reicht von 1 bis 10. Das ist natürlich eine etwas primitive Möglichkeit der Tischauswahl – für unsere Zwecke reicht sie aber.
  2. Mit dem zweiten Slider legen wir fest, wie scharf das Essen sein soll. Auch dieser Slider reicht von 1 bis 10.

Als drittes Element haben wir eine TextBox um die Details zur Bestellung einzugeben. Wenn Sie die Demo ausbauen wollen, könnten sie zum Beispiel eine ListBox zur Auswahl des Gerichts einbauen. Für unsere Demo habe ich es bei einer einfachen TextBox belassen.

Als letztes haben wir noch einen Button, um die Bestellung abzuschicken. Das war’s!

Die folgenden Punkte sind noch bemerkenswert:

  1. Die Klasse RestaurantCommands: Diese Klasse enthält die von unserer Socket-Anwendung erlaubten Kommandos. Sie wird sowohl im Client als auch im Server enthalten sein und stellt sicher, dass beide eine gemeinsame Sprache sprechen.
  2. UdpAnySourceMulticastChannel.cs und UdpPacketReceivedEventArgs.cs: Diese beiden Dateien habe ich aus der SDK Multicast Anwendung übernommen, die Code für UDP Multicast Sockets beinhaltet.
  3. Die Klasse Order: Diese Klasse erledigt die ganze Kommunikation. Die Kommunikation besteht dabei aus den Kommandos, die wir in der Klasse RestaurantCommands definiert haben. Diese Kommandos bilden also das Vokabular, das wir übertragen, empfangen und interpretieren können.

Wie die Beispielanwendung funktioniert

Vorbereitungen: Installieren Sie die Mango Tools von http://create.msdn.com/. Damit haben Sie das Visual Studio Express 2010 und das Windows Phone SDK. Da wir auch auf dem PC eine Silverlight Anwendung haben werden, laden Sie noch das Visual Studio C# Express Edition herunter, um die nötigen Projektvorlagen zu bekommen.

  • Öffnen Sie das Visual Studio, navigieren Sie zur Solution Datei und öffnen Sie diese.
  • Achtung: die Solution Datei hat zwei Startup-Projekte. Das Projekt „TakeMyOrder“ ist das Windows Phone Projekt und „PointOfSaleApp“ ist die Desktop Anwendung.
  • Dies ist ein Beispiel für Multicast Sockets. Um es auszuprobieren, müssen Sie das Windows Phone Projekt auf ein echtes Gerät installieren und die Silverlight Anwendung auf dem PC laufen lassen.
  • Wenn Sie kein Gerät haben, können Sie alternativ die Silverlight Desktopanwendung auf einem anderen Rechner installieren (z.B. in einer virtuellen Maschine) und die Windows Phone Anwendung im Emulator auf dem Host.
  • Sie können die Silverlight Desktopanwendung und die Windows Phone Anwendung im Emulator nicht auf der selben Maschine ausführen.
  • Wenn Sie mehrere Geräte haben, können Sie die Windows Phone Anwendung auf allen gleichzeitig laufen lassen. Damit haben Sie mehrere Kunden, die gleichzeitig Bestellungen aufgeben.
  • Da wir das UDP Protokoll verwenden, gibt es keine Garantie, dass Nachrichten erfolgreich gesendet und empfangen werden. Das liegt daran, dass wir keine direkte Ende-zu-Ende Verbindung haben. Die Geräte nehmen nur an einer Multicast-Gruppe teil, die über eine IP Adresse und einen Port identifiziert wird. Sie werden aber sehen, dass die Kommunikation recht zuverlässig ist.

Der Ablauf:

  • Starten Sie die Silverlight Out of Browser Anwendung.
  • Starten Sie die Windows Phone Anwendung auf dem Gerät oder im Emulator auf einem anderen Rechner.
  • Der Host wird den Namen des Servers und dessen Adresse anzeigen.
  • Sie sind jetzt bereit, eine Bestellung aufzugeben.
  • Wählen Sie einen Tisch zwischen 1 und 10 in der Windows Phone Anwendung, indem Sie den Slider bewegen.
  • Geben Sie eine Bestellung ein. Z.B. frittierten Reis mit Hünchen und Shrimps, etc.
  • Wählen Sie die Schärfe des Gerichts mit dem entsprechenden Slider.
  • Drücken Sie auf Order. Die Windows Phone Anwendung schickt jetzt das „SendOrder“ Kommando an alle Teilnehmer der Gruppe. Wir hätten das auch filtern können, so dass nur an den Host gesendet wird.
  • Die Bestellung wird auf dem Host empfangen und zur Tabelle „Incoming Orders“ hinzugefügt. Sie wird dort im Status „In Process“ auftauchen und ihr wird eine eindeutige Order ID (abhängig von der Anzahl der eingegangenen Bestellungen) zugewiesen.
  • Angenommen, Sie haben eine schnelle Küche und die Bestellung ist bereit, sobald Sie eintrifft. Klicken Sie die Checkbox neben „In Process“ und drücken Sie auf den „Order Ready“ Button. Der Host sendet jetzt den Status der Bestellung an alle Geräte im Netzwerk und aktualisiert den Status in der Liste der Bestellungen. Für Details zu INotifyPropertyChanged sehen Sie weiter unten nach.
  • Alle Windows Phone Geräte empfangen den Status der Bestellung. Auch hier hätten wir wieder filtern können, so dass nur das Gerät die aktualisierte Bestellung empfängt, das sie auch abgeschickt hat.

Hier ist ein Screenshot, nachdem eine Bestellung abgeschlossen wurde:

Wie der Code funktioniert:

Hier sind die entscheidenden Stellen zum Verständnis des Codes.

Der Gruppe beitreten: Der folgende Code erledigt den Beitritt 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 folgende Kommando wird abgesetzt, um der Gruppe beizutreten:

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

Und schließich öffnet die Methode 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 melden wir uns für einige Events zur Verarbeitung 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);
}

Besonders hervorhebenswert ist der Event „PacketReceived“. Alle empfangenen Kommandos (auf dem Gerät und auf dem Host) werden in diesem Event Handler verarbeitet. Hier parsen und identifizieren wir das eingetroffene Kommando. Abhängig vom Kommando und der Anzahl der übergebenen Argumente entscheiden wir, was weiter zu tun ist.

Eine Bestellung senden: Das folgende Kommando wird abgesetzt, wenn der Anwender auf den Order Button drückt:

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

Die Methode 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 „SendOrderFormat“ 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 Methode 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 Nachricht entsprechend des Nachrichtenformats formatiert und dann übertragen.

Eine Bestellung empfangen: Auf dem Host wird eine Nachricht im Handler empfangen, den wir für den „PacketReceived“ Event definiert haben. Wir parsen die Nachricht und identifizieren 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 unsere Argumente für die Methode Data Received identifiziert, geparsed und erstellt haben, rufen wir diese auf.

In der Hostanwendung behandeln wir diesen Event und fügen die eingegangene Bestellung unserer Orders Collection 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 bekommen Sie eine Fehlermeldung, wenn Sie das Debuggen beenden:

File or assembly name ‚System.Net.debug.resources, Version=2.0.5.0, Culture=en-US, PublicKeyToken=7cec85d7bea7798e‘, or one of its dependencies, was not found.

Das ist ein bekanntes Problem, welches hier beschrieben ist: http://forums.create.msdn.com/forums/p/89666/537141.aspx. Wenn dieser Fehler auftritt, müssen Sie das Visual Studio möglicherweise schließen und neu starten.

Bestellstatus schicken: Zum Senden des Bestellstatus verwenden wir einen ähnlichen Mechanismus 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

Dieses Interface implementieren wir für Objekte, die wir an Datagrids binden wollen. Auch wenn die Collection wie folgt deklariert ist, wird unsere Benutzerschnittstelle nicht aktualisieren, wenn die Eigenschaften nicht selbst einen Property Changed Event feuern.

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

Zusammenfassung

Mit den hier vorgestellten Mitteln sollte es Ihnen recht leicht fallen, eine Anwendung zu bauen, die Sockets verwendet. Die Anwendung kann zu einer vollständigen Restaurant-Anwendung ausgebaut werden, indem wir sie um die bereits angesprochenen Features erweitern:

  • Möglichkeit der Auswahl eines Gerichts aus einer Speisekarte
  • Anzeige der ausstehenden Bestellungen auf dem Gerät
  • Möglichkeit, sich einzuloggen

Die wesentlichen Schritte bei der Entwicklung einer Socket-basierten Anwendung sind:

  • Entscheiden Sie sich für den Typ der Socket-Anwendung: Multicast (UDP) oder Unicast (TCP).
  • Definieren Sie die auszutauschenden Kommandos.
  • Kommuniziere.
  • Verarbeite.

Mit diesem Artikel sollten Sie einen kleinen Einblick in die Socket Programmierung für Windows Phone bekommen haben. Ich empfehle Ihnen auch die Codebeispiele hier http://msdn.microsoft.com/en-us/library/hh202874(v=vs.92).aspx

Um die komplette Anwendung zur Kommunikation über Sockets herunterzuladen, drücken Sie auf den Download Code Button:

Morgen wird Matt Eland Ihnen App Connect (auch bekannt als Search Extensibility) vorstellen. Damit können Sie Anwender auf Ihre Anwendung aufmerksam machen, auch wenn diese die Anwendung noch gar nicht installiert haben.

Bis dahin!

Schreibe einen Kommentar

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