通过tcp或套接字发送类型对象

时间:2013-02-21 21:27:01

标签: c# sockets tcpclient

我在为Xna制作的非常简单的游戏创建网络界面时遇到了麻烦。我只需要通过TCP客户端/ Socket发送对象。例如: 我有一个名为“播放器”的课程。 在每个播放器中,都有一个字段名称“Info”,类型为“PlayerInfo”。在客户端/服务器中,我需要将每个玩家的信息发送给每个客户,除了发送它的人(显然)。 这是一个简单的例子,但我需要用大约5-10个对象来做,再加上发送玩家更新(位置,动作......) 使用TCP / Sock有一种简单的方法吗? 注意:我会将我的知识评为C#并编程为6/10,因此如果您有解决方案,则无需解释所有内容(例如:变量和字段之间的区别)。我也知道接口,库等... 提前谢谢!

3 个答案:

答案 0 :(得分:30)

我推荐一种方法,两种较小的方法依赖于很多东西。

第一个意味着你已经知道如何使用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)方法的第二个参数提供的root / sentinel开始的对象的树/图,并写入所有原始值以及类型信息和相对位置信息到提供的流。 只要提供的流相对于以前的对象图序列化的位置定位,它也能够完全反向并反序列化整个对象图。

虽然有一些捕获:您需要使用[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或XNA的另一个实现不支持BinaryFormatter类(ExEn与MonoTouch或其他)。 在这种情况下,如果你需要你的服务器(一个完整的,老式的.NET应用程序)来引用我所谈到的公共程序集并且还有游戏项目(这不是一个很好的老式),你会很难。 NET应用程序,但具有相当奇特的性质)引用完全相同的程序集。

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

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

答案 1 :(得分:8)

我的个人快速清洁解决方案,使用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...

答案 2 :(得分:3)

您可以使用.net框架中提供的各种类创建自己的解决方案。您可能想要签出WCF或套接字namepsace,特别是TcpClient和TcpListener类,请参阅MSDN。如果您进行与使用这些相关的搜索,那么有很多很棒的教程。您还需要考虑如何将类型化对象转换为字节数组,类似于question

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

共享基础(定义播放器对象):

[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,以便您可以在“使用NetworkCommsDotNet”参考中添加它。另请参阅客户端示例中的服务器IP地址当前为“127.0.0.1”,如果您在同一台计算机上同时运行服务器和客户端,这应该可以使用。