15

我在为我在 Xna 中制作的一个非常简单的游戏创建网络接口时遇到了麻烦。我只需要通过 TCP 客户端/套接字发送对象。例如:我有一个名为“Player”的类。在每个 Player 中,都有一个名为“Info”的字段,类型为“PlayerInfo”。在客户端/服务器中,我需要将每个玩家的信息发送给每个客户端,除了发送它的那个(显然)。这是一个简单的例子,但我需要大约 5 到 10 个对象,加上发送玩家更新(位置、动作……)。注意:我将我在 C# 和编程方面的知识评为 6/10,因此如果您有解决方案,则无需解释所有内容(例如:变量和字段之间的区别是什么)。我也知道接口、库等等……在此先感谢!

4

4 回答 4

38

我有一种我会推荐的方法和两种较小的方法,它们依赖于许多事情。

第一个意味着您已经知道如何使用 Socket 类,但是有很多类需要通过它发送。

从传输的角度来看,您应该只创建/考虑一个非常简单的类。让我们称这个类为 MyMessage:

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

好的。从 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) 方法的第二个参数提供的根/哨兵开始遍历对象的树/图,并将所有原始值加上类型信息和相对位置信息写入提供的溪流。只要提供的流的位置与前一个对象图序列化的位置相对应,它也能够进行精确的逆向和反序列化整个对象图。

不过这里有一些问题:您需要使用 [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 );
  }
}

您将需要一个通用的程序集项目(类库)供两个可执行项目(客户端和服务器)引用。

您需要传递的所有类都必须编写在该程序集中,以便服务器和客户端都知道如何在这个非常详细的级别上相互理解。

如果服务器不需要了解客户端之间所说的内容并且只传递消息(将一条消息广播到其他 N-1 个客户端),那么忘记我所说的关于通用程序集的内容。在这种特殊情况下,服务器只看到字节,而客户端对来回发送的实际消息有更深入的了解。

我说我有三种方法。

第二个涉及 .NET Remoting,它可以减轻你的大量工作,但如果你不完全理解它,就很难忍受。你可以在 MSDN 上阅读它,这里:http: //msdn.microsoft.com/en-us/library/kwdt6w2k (v=vs.100).aspx

仅当(现在或将来)XNA 指的是 Windows Phone 或不支持 BinaryFormatter 类的 XNA 的另一个实现(带有 MonoTouch 的 ExEn 或其他)时,第三个会更好。在这种情况下,如果您需要您的服务器(一个成熟的老式 .NET 应用程序)来引用我谈到的通用程序集并且还有游戏项目(这不是一个好的老式 . NET 应用程序,但具有相当奇特的性质)引用完全相同的程序集。

在这种情况下,我们需要使用序列化和反序列化对象的替代形式。您还需要在两个世界(.NET 和 WP7 或 WP8)中相同地实现两组类。您可以使用某种形式的 XML 序列化程序,您需要将其显式映射到您的类(不如 BinaryFormatter 类强大,但在托管您的类的运行时的性质方面更通用)。

您可以在此处阅读 MSDN 上的 XmlSerializer 类:http: //msdn.microsoft.com/en-us/library/system.xml.serialization.xmlserializer.aspx

于 2013-02-21T22:21:48.903 回答
11

我个人使用 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...
于 2013-02-21T22:38:58.317 回答
3

您可以使用 .net 框架中提供的各种类来创建自己的解决方案。您可能想要检查 WCF 或 Sockets 名称空间,特别是 TcpClient 和 TcpListener 类,请参阅MSDN。如果您进行与使用这些相关的搜索,将会有很多很棒的教程。您还需要考虑如何将类型化对象转换为字节数组,类似于这个问题

另一种方法是使用网络库。有低级库和高级库。鉴于您的编程经验水平和特定的最终目标,我建议您使用高级库。这种网络库的一个例子是lidgren。我是另一个网络库networkComms.net的开发人员,下面是如何使用此库发送类型化对象的快速示例:

Shared Base(定义 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();
        }
    }
}

以上是本教程的修改版。您显然需要从网站下载 NetworkCommsDotNet DLL,以便可以将其添加到“使用 NetworkCommsDotNet”参考中。另请参阅客户端示例中的服务器 IP 地址当前为“127.0.0.1”,如果您在同一台机器上同时运行服务器和客户端,这应该可以工作。

于 2013-02-21T22:04:11.383 回答
2

两年多后,我找到了解决这个问题的新方法,我认为分享它可能对某人有用。请注意,接受的答案仍然有效。

我能找到的序列化类型对象的最简单方法是使用 Json.NET 中的 json 转换器。有一个设置对象允许您将 json 中的类型存储为名为$type. 以下是如何执行此操作以及生成的 json:

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

JsonConvert.SerializeObject(myObject, settings);

json结果:

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

反序列化时,如果使用相同的设置,则会反序列化正确类型的对象。正是我需要的!希望这可以帮助。

于 2016-04-11T12:58:08.060 回答