web-dev-qa-db-ja.com

型指定されたオブジェクトをTCPまたはソケット経由で送信する

Xnaで作成した非常に単純なゲームのネットワークインターフェイスを作成するのに問題があります。 TCP client/Socketを介してオブジェクトを送信する必要があるだけです。例:「Player」という名前のクラスがあります。すべてのプレーヤーに、タイプ「PlayerInfo」のフィールド名「Info」があります。クライアント/サーバーでは、すべてのプレイヤーの情報を、それを送信したクライアントを除くすべてのクライアントに送信する必要があります(明らかに)。これは簡単な例ですが、5〜10個程度のオブジェクトで実行する必要があります。加えて、プレーヤーの更新(位置、アクションなど)を送信するTCP/Sockを使用してこれを行う簡単な方法はありますか?注:C#とプログラミングに関する知識は6/10と評価します。解決策があれば、すべてを説明する必要はありません(例:変数とフィールドの違いは何ですか?)インターフェース、ライブラリなどについても知っています…よろしくお願いします!

13
Philippe Paré

私がお勧めするアプローチは1つありますが、多くのことに依存する2つのアプローチがあります。

最初のものは、Socketクラスの使用方法をすでに知っているが、それを介して送信する必要のあるクラスがたくさんあることを意味します。

トランスポートの観点からは、非常に単純なクラスを1つだけ作成/考慮する必要があります。このクラスをMyMessageと呼びましょう:

public class MyMessage {
  public byte[] Data { get; set; }
}

OK。 TCPの観点から見ると、このクラスのインスタンスを(クライアントからサーバーにそしてその逆に)渡すことができることを確認するだけです。私は、これを行うことの詳細ですが、これをうまく管理すると、TCP/IP接続の性質が「バイトストリーム」から「メッセージストリーム」に変換されることを指摘します。つまり、通常、TCP/IPは接続を介して送信するデータのチャンクが同じ形式で宛先に到着することを保証しません(結合されるか、分割される可能性があります)。保証される唯一のことは、すべてのチャンクのバイトが最終的に同じに到着することです。接続の反対側で注文します(常に)。

メッセージストリームが稼働しているので、.NETの古き良き直列化を使用して、Dataプロパティ内のクラスインスタンスをカプセル化できます。これは、オブジェクトグラフをバイトに、またはその逆にシリアル化することです。

これを(最も一般的に)行う方法は、標準ライブラリクラスを使用することです:System.Runtime.Serialization.Formatters.Binary.BinaryFormatterこれは、次のようにmscorlib.dllにあります。

public static class Foo {

  public static Message Serialize(object anySerializableObject) {
    using (var memoryStream = new MemoryStream()) {
      (new BinaryFormatter()).Serialize(memoryStream, anySerializableObject);
      return new Message { Data = memoryStream.ToArray() };
    }
  }

  public static object Deserialize(Message message) {
    using (var memoryStream = new MemoryStream(message.Data))
      return (new BinaryFormatter()).Deserialize(memoryStream);
  }

}

BinaryFormatterクラスは、Serialize(Stream、object)メソッドの2番目の引数として提供されたルート/センチネルから始まるオブジェクトのツリー/グラフをトラバースし、すべてのプリミティブ値と型情報および相対位置情報を提供されたものに書き込むことができます。ストリーム。また、提供されたストリームが以前のオブジェクトグラフのシリアル化の場所に従って配置されている限り、オブジェクトグラフ全体を正確に逆にして逆シリアル化することもできます。

ただし、ここにはいくつかの注意点があります。すべてのクラスに[SerializableAttribute]アノテーションを付ける必要があります。あなたのクラスがあなたが書いた他のクラスのフィールドを含んでいて、あなたがそれらをそう言っているなら:

[SerializableAttribute]
public class Player {
  public PlayerInfo Info; 
  //... etc 

次に、これらにも[SerializableAttribute]で注釈を付ける必要があります。

[SerializableAttribute]
public class PlayerInfo { //... etc

クラスに、他のユーザー(Microsoftなど)によって記述されたタイプのフィールドが含まれている場合、それらのフィールドにはすでに属性が付けられているはずです。シリアル化できるもののほとんどはすでにあります。プリミティブ型は当然シリアル化可能です。シリアル化してはいけないものは、FileStreams、Threads、Socketsなどです。

シリアライズ可能なクラスがあることを確認した後、必要なことは、それらのインスタンスをシリアライズし、送信し、受信し、デシリアライズすることだけです。

class Client {

  public static void SendMovement(Movement movement) {
    Message message = Foo.Serialize(movement);

    socketHelper.SendMessage(message);
  }
  public static void SendPlayer(Player player) {
    Message message = Foo.Serialize(player);

    socketHelper.SendMessage(message);
  }
  // .. etc

  public static void OnMessageReceivedFromServer(Message message) {
    object obj = Foo.Deserialize(message);
    if (obj is Movement)
      Client.ProcessOtherPlayersMovement(obj as Movement);
    else if (obj is Player)
      Client.ProcessOtherPlayersStatusUpdates(obj as Player);
    // .. etc
  }

  public static void ProcessOtherPlayersMovement(Movement movement) {
    //...
  }
  // .. etc

}

サーバー側で:

class Server {

  public static void OnMessageReceived(Message message, SocketHelper from, SocketHelper[] all) {
    object obj = Foo.Deserialize( message );
    if (obj is Movement)
      Server.ProcessMovement( obj as Movement );
    else if (obj is Player)
      Server.ProcessPlayer( obj as Player );
    // .. etc

    foreach (var socketHelper in all)
      if (socketHelper != from)
        socketHelper.SendMessage( message );
  }
}

両方の実行可能プロジェクト(クライアントとサーバー)によって参照される共通のアセンブリプロジェクト(クラスライブラリ)が必要です。

サーバーとクライアントの両方がこの非常に詳細なレベルでお互いを理解する方法を知ることができるように、受け渡す必要のあるすべてのクラスをそのアセンブリに記述する必要があります。

サーバーがクライアント間で何が言われているかを理解する必要がなく、メッセージを渡すだけの場合(1つのメッセージを他のN-1クライアントにブロードキャストする)、共通アセンブリについて私が言ったことを忘れます。この特定のケースでは、サーバーはバイトのみを認識し、クライアントは実際に送受信されるメッセージをより深く理解します。

私は3つのアプローチがあると言いました。

2つ目は、.NET Remotingです。これは、多くの作業を肩から片付けることができますが、完全に理解していないと生きるのが困難です。あなたはそれについてMSDNで読むことができます: http://msdn.Microsoft.com/en-us/library/kwdt6w2k(v = vs.100).aspx

3つ目は、XNAによって(現在または将来的に)Windows PhoneまたはBinaryFormatterクラスをサポートしないXNAの別の実装(MonoTouchを使用したExEnなど)を意味する場合にのみ、より優れています。その場合、サーバー(本格的な古き良き時代の.NETアプリケーション)が私が話し合った一般的なアセンブリを参照し、ゲームプロジェクト(古き良き時代のものではないでしょう)も必要になると、あなたは苦労します。 NETアプリですが、かなりエキゾチックな性質を持っています)、まったく同じアセンブリを参照します。

その場合は、オブジェクトのシリアル化と逆シリアル化の代替形式を使用する必要があります。また、2つのクラス(.NETとWP7またはWP8)の2つのクラスセットを同じように実装する必要があります。クラスに明示的にマップする必要があるXMLシリアライザーのいくつかの形式を使用できます(BinaryFormatterクラスほど強力ではありませんが、クラスをホストするランタイムの性質においてより汎用性があります)。

ここで、MSDNのXmlSerializerクラスについて読むことができます。 http://msdn.Microsoft.com/en-us/library/system.xml.serialization.xmlserializer.aspx

32
Eduard Dumitru

JSON.NETを使用した私の個人的な迅速でクリーンなソリューション:

class JMessage
{
    public Type Type { get; set; }
    public JToken Value { get; set; }

    public static JMessage FromValue<T>(T value)
    {
        return new JMessage { Type = typeof(T), Value = JToken.FromObject(value) };
    }

    public static string Serialize(JMessage message)
    {
        return JToken.FromObject(message).ToString();
    }

    public static JMessage Deserialize(string data)
    {
        return JToken.Parse(data).ToObject<JMessage>();
    }
}

これで、次のようにオブジェクトをシリアル化できます。

Player player = ...;
Enemy enemy = ...;
string data1 = JMessage.Serialize(JMessage.FromValue(player));
string data2 = JMessage.Serialize(JMessage.FromValue(enemy));

そのデータをネットワーク経由で送信し、反対側では次のようなことができます。

string data = ...;
JMessage message = JMessage.Deserialize(data);
if (message.Type == typeof(Player))
{
    Player player = message.Value.ToObject<Player>();
}
else if (message.Type == typeof(Enemy))
{
    Enemy enemy = message.Value.ToObject<Enemy>();
}
//etc...
10
Timothy Shields

.netフレームワークで提供されるさまざまなクラスを使用して、独自のソリューションを作成できます。 WCFまたはソケットネームスペース、特にTcpClientクラスとTcpListenerクラスをチェックアウトする必要があります。詳細は [〜#〜] msdn [〜#〜] を参照してください。これらの使用に関連する検索を実行すると、すばらしいチュートリアルがたくさんあります。入力したオブジェクトをバイト配列に変換する方法も考慮する必要があります。これは question と同様です。

別のアプローチは、ネットワークライブラリを使用することです。低レベルのライブラリと高レベルのライブラリがあります。あなたのプログラミング経験のレベルと特定の最終目標を考慮して、高レベルのライブラリを提案します。このようなネットワークライブラリの例は lidgren です。私は別のネットワークライブラリ networkComms.net の開発者です。このライブラリを使用して型付きオブジェクトを送信する方法の簡単な例を次に示します。

共有ベース(Playerオブジェクトを定義):

[ProtoContract]
class Player
{
    [ProtoMember(1)]
    public string Name { get; private set; }
    [ProtoMember(2)]
    public int Ammo { get; private set; }
    [ProtoMember(3)]
    public string Position { get; private set; }

    private Player() { }

    public Player(string name, int ammo, string position)
    {
        this.Name = name;
        this.Ammo = ammo;
        this.Position = position;
    }
}

クライアント(単一のPlayerオブジェクトを送信):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

using NetworkCommsDotNet;
using ProtoBuf;

namespace Client
{
    class Program
    {
        static void Main(string[] args)
        {
            Player player = new Player("MarcF", 100, "09.09N,21.12W");

            //Could also use UDPConnection.GetConnection...
            TCPConnection.GetConnection(new ConnectionInfo("127.0.0.1", 10000)).SendObject("PlayerData", player);

            Console.WriteLine("Send completed. Press any key to exit client.");
            Console.ReadKey(true);
            NetworkComms.Shutdown();
        }
    }
}

サーバ:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

using NetworkCommsDotNet;
using ProtoBuf;

namespace Server
{
    class Program
    {
        static void Main(string[] args)
        {
            // Convert incoming data to a <Player> object and run this method when an incoming packet is received.
            NetworkComms.AppendGlobalIncomingPacketHandler<Player>("PlayerData", (packetHeader, connection, incomingPlayer) =>
            {
                Console.WriteLine("Received player data. Player name was " + incomingPlayer.Name);
                //Do anything else with the player object here
                //e.g. UpdatePlayerPosition(incomingPlayer);
            });

            //Listen for incoming connections
            TCPConnection.StartListening(true);

            Console.WriteLine("Server ready. Press any key to shutdown server.");
            Console.ReadKey(true);
            NetworkComms.Shutdown();
        }
    }
}

上記はこの tutorial の修正版です。 NetworkCommsDotNet DLLをWebサイトからダウンロードして、「using NetworkCommsDotNet」参照に追加できるようにする必要があります。クライアントのサーバーのIPアドレスが現在「127.0。 0.1インチ、これはサーバーとクライアントの両方を同じマシンで実行する場合に機能します。

4
MarcF

2年以上経って、この問題を解決する新しい方法を見つけました。共有することが誰かにとって役立つかもしれないと思いました。受け入れられた回答はまだ有効であることに注意してください。

型付きオブジェクトをシリアル化する最も簡単な方法は、Json.NETのjsonコンバーターを使用することです。 $typeという名前の値としてjsonにタイプを保存できるようにする設定オブジェクトがあります。これを行う方法と結果のjsonは次のとおりです。

JsonSerializerSettings settings = new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.All
};

JsonConvert.SerializeObject(myObject, settings);

Jsonの結果:

{
    "$type" : "Testing.MyType, Testing",
    "ExampleProperty" : "Hello world!"
}

逆シリアル化時に同じ設定が使用されている場合、正しいタイプのオブジェクトが逆シリアル化されます。まさに私が必要としたもの!お役に立てれば。

2
Philippe Paré