17

我正在尝试更好地理解 c# 中的 tcp/ip 套接字,因为我想挑战自己,看看我是否可以纯粹出于教育目的创建一个有效的 MMO 基础设施(游戏世界、地图、玩家等),因为我没有打算成为那些“OMGZ iz 会制作出比 WoW 更好的 r0x0r MMORPG !!!”中的另一个,你知道我在说什么。

无论如何,我想知道是否有人可以阐明如何设计这种系统以及需要什么样的东西,以及我应该注意什么?

我最初的想法是将系统分解为单独的客户端/服务器连接,每个连接(在其自己的端口上)执行特定任务,例如更新玩家/怪物位置,发送和接收聊天消息等。处理数据更容易,因为您并不总是需要在数据上放置标头以了解数据包包含哪些信息。

这是否有意义并且有用还是我只是让事情复杂化了?

非常感谢您的回复。

4

4 回答 4

30

如果您正在进行套接字级别的编程,那么无论您为每种消息类型打开多少个端口,您仍然需要具有某种类型的标头。即使它只是消息其余部分的长度。话虽如此,向消息添加简单的标题和尾部结构很容易。我认为只需要在客户端处理一个端口会更容易。

我相信现代 MMORPG(甚至可能是旧的)有两个级别的服务器。验证您是否为付费客户的登录服务器。一旦经过验证,这些会将您转到包含所有游戏世界信息的游戏服务器。即便如此,这仍然只需要客户端打开一个套接字,但不允许拥有更多。

此外,大多数 MMORPGS 还会加密其所有数据。如果你写这篇文章是为了好玩,那么这并不重要。

对于一般的设计/编写协议,这是我担心的事情:

字节序

客户端和服务器是否始终保证具有相同的字节序。如果不是,我需要在我的序列化代码中处理它。有多种方法可以处理字节顺序。

  1. 忽略它 - 显然是一个糟糕的选择
  2. 指定协议的字节顺序。这就是旧协议所做/所做的,因此术语网络顺序总是大端。实际上,您指定哪种字节序并不重要,只需指定一个或另一个即可。如果您不使用大字节序,一些顽固的老网络程序员会站起来,但是如果您的服务器和大多数客户端都是小字节序,那么您除了通过使协议大字节序来增加额外的工作之外,真的不会给自己买任何东西。
  3. 在每个标头中标记字节序 - 您可以添加一个 cookie,它会告诉您客户端/服务器的字节序,并根据需要对每条消息进行相应的转换。加班!
  4. 使您的协议不可知 - 如果您将所有内容作为 ASCII 字符串发送,那么字节序无关紧要,而且效率也低得多。

在 4 个中,我通常会选择 2 个,并将字节序指定为大多数客户端的字节序,现在将是小字节序。

向前和向后兼容性

协议是否需要向前和向后兼容。答案几乎总是肯定的。在这种情况下,这将决定我如何根据版本控制设计整个协议,以及如何创建每个单独的消息来处理实际上不应该成为版本控制一部分的微小更改。您可以在此基础上使用 XML,但会损失很多效率。

对于整体版本控制,我通常设计一些简单的东西。客户端发送一个版本控制消息,指定它使用版本 XY,只要服务器可以支持该版本,它就会发回一条消息,确认客户端的版本,然后一切继续进行。否则,它会接收客户端并终止连接。

对于每条消息,您都有以下内容:

+-------------------------+-------------------+-----------------+------------------------+
| Length of Msg (4 bytes) | MsgType (2 bytes) | Flags (4 bytes) | Msg (length - 6 bytes) |
+-------------------------+-------------------+-----------------+------------------------+

长度显然告诉你消息有多长,不包括长度本身。MsgType 是消息的类型。这只有两个字节,因为 65356 是应用程序的大量消息类型。这些标志可以让您知道消息中序列化的内容。该字段与长度相结合,为您提供了向前和向后的兼容性。

const uint32_t FLAG_0  = (1 << 0);
const uint32_t FLAG_1  = (1 << 1);
const uint32_t FLAG_2  = (1 << 2);
...
const uint32_t RESERVED_32 = (1 << 31);

然后您的反序列化代码可以执行以下操作:

uint32 length = MessageBuffer.ReadUint32();
uint32 start = MessageBuffer.CurrentOffset();
uint16 msgType = MessageBuffer.ReadUint16();
uint32 flags = MessageBuffer.ReadUint32();

if (flags & FLAG_0)
{
    // Read out whatever FLAG_0 represents.
    // Single or multiple fields
}
// ...
// read out the other flags
// ...

MessageBuffer.AdvanceToOffset(start + length);

这允许您在消息末尾添加新字段,而无需修改整个协议。它还确保旧的服务器和客户端将忽略他们不知道的标志。如果他们必须使用新的标志和字段,那么您只需更改整个协议版本。

是否使用框架

我会考虑将各种网络框架用于业务应用程序。除非我有特殊需要从头开始,否则我会使用标准框架。在您的情况下,您想学习套接字级编程,所以这是一个已经为您解答的问题。

如果确实使用了框架,请确保它解决了上述两个问题,或者如果您需要在这些领域对其进行自定义,至少不会妨碍您。

我在和第三方打交道吗

在许多情况下,您可能正在处理需要与之通信的第三方服务器/客户端。这意味着几个场景:

  • 他们已经定义了协议 - 只需使用他们的协议。
  • 您已经定义了一个协议(并且他们愿意使用它) - 再次简单地使用定义的协议
  • 他们使用标准框架(基于 WSDL 等) - 使用框架。
  • 双方都没有定义协议 - 尝试根据手头的所有因素(我在这里提到的所有因素)以及他们的能力水平(至少据你所知)确定最佳解决方案。无论如何,请确保双方同意并理解该协议。根据经验,这可能是痛苦的或愉快的。这取决于你和谁一起工作。

在任何一种情况下,您都不会与第三方合作,因此这实际上只是为了完整性而添加的。

我觉得我可以写更多关于这个的东西,但它已经相当冗长了。我希望这会有所帮助,如果您有任何具体问题,请在 Stackoverflow 上提问。

回答 knoopx 问题的编辑:

于 2008-11-13T01:04:43.133 回答
2

我认为你需要在走路和跑步之前爬行。首先要正确设计框架和连接,然后再考虑可扩展性。如果您的目标是学习 C# 和 tcp/ip 编程,那么不要让自己变得更难。

继续您的最初想法,并将数据流分开。

玩得开心,祝你好运

于 2008-11-12T20:13:45.013 回答
1

我建议不要针对不同的信息进行多个连接。我会设计一些协议,其中包含要处理的信息的标头。使用尽可能少的资源。此外,您可能不想将各种职位和统计数据的更新从客户端发送到服务器。否则,您最终可能会遇到用户可以修改他们的数据被发送回服务器的情况。

假设用户伪造怪物的位置以绕过某些东西。我只会更新用户的位置向量和动作。让服务器处理和验证其余信息。

于 2008-11-12T18:51:41.840 回答
0

是的,我认为您对单个连接是正确的,当然客户端不会向服务器发送任何实际数据,更像是简单的命令,如“前进”、“左转”等,服务器会移动地图上的角色并将新坐标发送回客户端。

于 2008-11-12T18:56:18.117 回答