Auf das Thema antworten  [ 1 Beitrag ] 
Klasse Network - Nicht vollständig 
Autor Nachricht
Administrator
Benutzeravatar

Registriert: Sa 15. Dez 2012, 19:15
Beiträge: 137
Wohnort: Karlsruhe
Mit Zitat antworten
Hallo Leute!

Im Forum von XNA.mag wurde nach einer einfachen Netzwerklösung gefragt. Deshalb habe ich mich dazu entschlossen meine aktuelle Netzwerk-Klasse hier zu veröffentlichen. Es sei gleich gesagt: Sie ist nicht vollständig. Sie kann alles, was ich zum Testen benötige. Die absoluten Basics: Byte-Nachrichten senden und empfangen. Es gibt keine Verbindungskontrolle, momentan keine Methode um die Verbindung wieder zu schließen oder sonst irgendwas. Dafür kann sie aber als virtueller Server gestartet werden.


Virtueller Server
Begrifflichkeit: Server ist für mich eigentlich mein Webspace, Host der Computer der den "Server" startet mit dem sich die Clients dann verbinden. Wenn ich jetzt von Server spreche, dann meine ich die instanziierte Klasse TcpListener auf dem Host.

Ein virtueller Server ist für mich, wenn eine Instanz TcpListener auf dem Computer gestartet wird und dieser sich gleichzeitig als Client mit sich selbst verbindet. Der Vorteil dieser Geschichte ist, dass man das Programm grundsätzlich aus Sicht des Clients schreiben kann. Das heißt, Nachrichten werden immer an den Server gesendet und Nachrichten kommen auch immer vom Server.


Ablauf

Start des Servers
Als Host verwendet man die Methode StartServer(IPEndPoint endpoint, int maxConnections, bool virtuell). Der erste Parameter braucht eine IP-Adresse und einen Port. Der Port ist die Nummer der Tür, an der der Server auf mögliche Clients wartet. Das heißt, dass auch die Clients an derselben Adresse (IP-Adresse) und an derselben Hausnummer anfragen müssen, damit sie auf den wartenden Host treffen.

Als Port verwende ich 14300. Der Port ist eine ushort und kann 0 - 65365 sein. Es gibt reservierte Ports und welche die öfters verwendet werden. Dazu siehe in Wiki die "Well known ports" und überhaupt mal das hier.
Als maxConnections gibt man die Zahl der Clients an. Wenn der Boolean virtuell auf true gesetzt wird, dann wird die maxConnections in der Methode automatisch um eins erhöht.

Nach dem Aufruf der Methode wartet der Listener nun an der vereinbarten Stelle auf Client-Anfragen. Natürlich asynchron. Die instanziierte Klasse TcpListener ist über Network.Listener aufrufbar.


Start des Clients
Der Client wird über die Methode StartClient(IPEndPoint endpoint) gestartet.

Hier ein Beispiel:
Code:
 
 // Start des Servers - 127.0.0.1 ist die Lokale IP-Adresse. Die ist im Netzwerk natürlich nicht für andere zugreifbar.
 // Man sollte deshalb eine "richtige" IP-Adresse angeben und diese nur für Testzwecke auf dem eigenen Computer verwenden.
 Network.StartServer(new IPEndPoint(IPAddress.Parse("169.254.165.40"), 14300), 0, true);
 
  // Start der Verbindung beim Client
  Network.StartClient(new IPEndPoint(IPAddress.Parse("169.254.165.40"), 14300));
 


Host akzeptiert Client
Wenn ein Client sich erfolgreich angemeldet hat, dann wird das Event TcpClientAccepted(Caller caller) ausgelöst. Wenn das Property Network.MaxConnections geändert wird, dann wartet der Listener erneut auf eingehende Verbindungen bis die MaxConnections erreicht sind.

Die Klasse Caller
Jeder Client hat eine ID mit der er eindeutig zu identifizieren ist. Ich empfehle es, diese ID dem Client auch über eine Nachricht zu übermitteln, sodass er seine ID von nun an bei jeder Nachricht, die er an den Server (Host) sendet, mit angibt. Ansonsten ist alles was von dieser Klasse public ist nur die instanziierte Klasse TcpClient. Um das Senden und Empfangen von Nachrichten muss man sich nicht weiter kümmern, das macht die Klasse von selbst asynchron.

Client ist verbunden
Auf Client-Seite wird auch ein Event ausgelöst: ClientConnected(). Es gibt dazu keine Parameter. Die Instanz der Klasse TcpClient für den Client ist über Network.Client aufrufbar.

Senden einer Nachricht
SendToServer(IEnumerable<byte> bytes) ist für den Client gedacht um eine Nachricht an den Server zu senden.
SendToAll(IEnumerable<byte> bytes) ist für den Server gedacht, um allen Clients eine Nachricht zu senden. So kann der Server auch als Hub für Broadcast fungieren.
SentToMost(uint exceptID, IEnumerable<byte> bytes) dient dazu eine Nachricht von einem Client an alle anderen weiterzuleiten. Der Client, an den man die Nachricht nicht senden will, dessen ID muss im ersten Parameter angegeben werden.
SendToClients(IEnumerable<byte> bytes, params uint[] clientIDs) ist für das selektive Senden einer Nachricht an die ausgewählten Clients gedacht.
Send(IEnumerable<byte> bytes) überprüft, ob der Computer der Host ist und sendet die Nachricht dann an alle, oder sendet die Nachricht dann an den Server. Diese Methode gilt als Ergänzung fürs Broadcasting.

Beispiel:
Code:
 
 // Zum Versenden einer Textnachricht
 Network.Send(Encoding.UTF8.GetBytes("Dies ist eine Testnachricht"));
 
 // Um die gesendeten bytes wieder zurück zu konvertieren: (Im Incoming-Event)
 string nachricht = Encoding.UTF8.GetString(byte[] bytes);
 

Beachten muss man, dass der Puffer knapp 8KB groß ist. Größer sollte eine Nachricht nicht sein. Die Klasse kann nämlich noch keine Nachrichten splitten oder so etwas.


Nachricht gesendet
Wegen der Eigenschaft eines virtuellen Servers (Netzwork.IsVirtuell prüft dies) gibt es zwei Events, wenn eine Nachricht gesendet wurde.
SentedToServer(byte[] bytes) wird aufgerufen, wenn man als Client eine Nachricht an den Server verschickt hat.
SentedToClient(byte[] bytes) wird aufgerufen, wenn man als Server eine Nachricht an einen Client gesendet hat.


Nachricht empfangen
IncomingFromServer(byte[] bytes, uint id) Hier ist zu beachten, dass die ID des Callers übergeben wird, der bei einem Client immer 1 ist. Man kann die ID allerdings auch modifizieren, sobald man seine richtige ID vom Server bekommen hat. Network.Client.ID muss dazu geändert werden.
IncomingFromClient(byte[] bytes, uint id) Hier macht die Übergabe der ID eher Sinn, weil sie hier immer richtig ist. Ich empfehle die Übersendung der ID durch den Client deshalb, weil man nur so ein einfaches Hub aufbauen kann. Wenn der Client gleichzeitig sagt wessen Figur eigentlich geändert werden soll. Aber das kann man machen wie man will.


Tipps zur Verwendung:
Um das Versenden der Nachrichten zu vereinheitlichen ist es sinnvoll, für jede Nachricht eine Methode zu zu schreiben und als Parameter die notwendigen Informationen zu übergeben. Ich benutze dazu eine internal static class Netcom mit statischen Methoden:
Code:
 
        /// <summary>
        /// Festlegung der Position der Spielerfigur
        /// </summary>
        internal static byte[] Figur_Position(Figure fig)
        {
            byte[] buffer = new byte[Figur_Position_Count];
            BitConverter.GetBytes(NetCom.C_Figur_Position).CopyTo(buffer, 0);
            BitConverter.GetBytes(fig.ID).CopyTo(buffer, 2);
            BitConverter.GetBytes(fig.posX).CopyTo(buffer, 6);
            BitConverter.GetBytes(fig.posY).CopyTo(buffer, 10);
            return buffer;
        }
        internal const ushort C_Figur_Position = 3;
        private const byte Figur_Position_Count = 14;
 

Dabei lege ich in einer Konstanten ushort die ID des Befehls fest und füge dem Array dann die jeweiligen Informationen an. In einer weiteren Konstanten speichre ich die Länge dieses Buffers.

Der BitConverter ist eine tolle Klasse! Man muss nur darauf achten, dass die Werte, die man übergibt, auch mit ihrem richtigen Typ erkannt werden. Wenn man eine ushort in Bytes haben will, dann sollte man BitConverter.GetBytes((ushort)100) verwenden, gibt man die Zahl selbst an. Eine short oder ushort hat zwei Bytes, eine int oder float vier und eine long oder double 8. Decimal hat 16 Bytes. Ein string hingegen hat mindestens so viel Bytes wie Zeichen.

Verwendet man UTF8 für die Konvertierung von Text in Bytes - wodurch man zwar eine variable Byte-Länge bekommt aber dafür alle möglichen Zeichen übertragen kann, sollte man vorher den string in ein Byte-Array konvertieren und die Länge des Arrays in der Nachricht mit angeben:
Code:
 
        internal static byte[] Text(string text)
        {
            // Liste für eine variable Array-Länge
            List<byte> list = new List<byte>(text.Length * 2);
 
            // Hinzufügen der Bytes für den Text
            list.AddRange(Encoding.UTF8.GetBytes(text));
 
            // Hinzufügen der Länge der Bytes für den Text
            list.InsertRange(0, BitConverter.GetBytes((ushort)list.Count));
 
            // Befehl-ID 100 übergeben, damit man weiß, was für eine Nachricht das hier eigentlich ist
            list.InsertRange(0, BitConverter.GetBytes((ushort)100));
 
            // Ausgabe
            return list.ToArray();
        }
 


Außerdem solltet ihr noch die folgende Methode in eure statische NetCom-Klasse einbauen:
Code:
 
 internal static byte[] Concat(params byte[][] bytes)
 {
   List<byte> q = new List<byte>(20 * bytes.Length);
   foreach (byte[] b in bytes) { q.AddRange(b); }
   return q.ToArray();
 }
 

Sie kombiniert Befehle, sodass sie zusammen gesendet werden können.


Das Auslesen von Nachrichten
Das Auslesen von Nachrichten ist einfacher als man glaubt :)
Code:
 
void Network_IncomingFromServer(byte[] bytes, uint from)
{
        try
        {
                ushort netCom; uint id;
 
                for (int ix = 0; ix < bytes.Length; )
                {
                        netCom = BitConverter.ToUInt16(bytes, ix); ix += 2;
                        id = BitConverter.ToUInt32(bytes, ix); ix += 4;
                        switch (netCom)
                        {
                                case NetCom.C_Figur_Position:
                                        float C_Figur_Position_X = BitConverter.ToSingle(bytes, ix); ix += 4;
                                        float C_Figur_Position_Y = BitConverter.ToSingle(bytes, ix); ix += 4;
                                        Figure C_Figur_Position_Figur = Spielfeld.Figuren[id];
                                        C_Figur_Position_Figur.posX = C_Figur_Position_X;
                                        C_Figur_Position_Figur.posY = C_Figur_Position_Y;
                                        C_Figur_Position_Figur.locX = (int)C_Figur_Position_X;
                                        C_Figur_Position_Figur.locY = (int)C_Figur_Position_Y;
                                        break;
// (...)
 

Das Ganze befindet sich in einem try-catch-Block damit das Programm nicht abschmiert, wenn es zu Fehlern kommt. Man sollte sich die catch-Fehler beim Debuggen aber durchaus ausgeben lassen. So wär mir ein Fehler viel früher aufgefallen der damit zu tun hatte, dass ich ein altes Codefragment bei der Überarbeitung übersehen hatte.

Zunächst werden die zwei Variablen netCom und id aus der Nachricht gelesen. Weil diese die Clients und der Server grundsätzlich übermitteln sollen. netCom ist die Befehl-ID die hilft, die Nachrichten voneinander zu unterscheiden und die id ist eine uint mit der Spieler-ID. Also nicht der Client-ID. Ich benutze eine uint, weil eine jede Figur, auch jedes Geschoss, eine solche ID bekommt.

Mit einer Switch-Anweisung schauen wir, um was für eine Nachricht es sich handelt. Dazu vergleichen wir die netCom mit der Konstanten in der statischen Klasse NetCom. Bei jedem Auslesen wird die int ix um die Zahl erhöht, wie viele Bytes gerade ausgelesen wurden. So kann man später auch den Code umstrukturieren und arbeiten mit dynamischen und nicht absoluten Zahlen.

Im Beispiel werden zunächst die lokalen Variablen deklariert und dann die Werte zugeordnet.


Wenn es noch Fragen und Anregungen gibt: Eine Nachricht genügt!

Schöne Grüße,
Magony



Download

_________________
Bei Fragen, Lob, Kritik, Vorschläge, hilfreiche Hinweise oder Alternativvorschläge: Beitrag, neues Thema oder PN.
Für Dinge die diskutiert werden sollten, bitte neues Thema im jeweiligen Forum.
Wenn du nicht weißt wohin: Forum Unsortiert.


Mi 27. Mär 2013, 13:20
Diesen Beitrag melden
Profil Website besuchen
Beiträge der letzten Zeit anzeigen:  Sortiere nach  
Auf das Thema antworten   [ 1 Beitrag ] 

Wer ist online?

Mitglieder in diesem Forum: 0 Mitglieder und 1 Gast


Du darfst neue Themen in diesem Forum erstellen.
Du darfst Antworten zu Themen in diesem Forum erstellen.
Du darfst deine Beiträge in diesem Forum nicht ändern.
Du darfst deine Beiträge in diesem Forum nicht löschen.
Du darfst keine Dateianhänge in diesem Forum erstellen.

Suche nach:
Gehe zu:  
cron
Powered by phpBB® Forum Software © phpBB Group
Designed by ST Software
Deutsche Übersetzung durch phpBB.de

Impressum