c#中的多客户端,异步套接字,最佳实践?

我试图更好地理解c#中的tcp / ip套接字,因为我想挑战自己,看看我是否可以创建一个可用的MMO基础设施(游戏世界,地图,玩家等)纯粹用于教育目的,因为我没有意图成为另一个“OMGZ iz将让我的r0x0 MMORPG比魔兽更好!!!”,你知道我在说什么。

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

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

这是否有意义并且是有用的还是我只是简单地使事情复杂化?

非常感谢您的回复。

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

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

此外,大多数MMORPGS还加密他们的所有数据。 如果你把它写成一个有趣的练习,那么这并不重要。

一般来说,设计/编写协议是我担心的事情:

字节序

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

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

在我通常选择2的4中,并指定endianess为大多数客户端,现在几天将是小端。

前向和后向兼容性

协议是否需要向前兼容。 答案应该几乎总是肯定的。 在这种情况下,这将决定我如何在版本控制方面设计整个协议,以及如何创建每个单独的消息来处理实际上不应该成为版本控制的一部分的微小更改。 你可以利用这个并使用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的问题:

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

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

玩得开心,祝你好运

我会建议不要使用多个连接来获取不同的信息。 我会设计一些协议,其中包含一个包含要处理的信息的标头。 尽可能少地使用资源。 此外,您可能不希望从客户端向服务器发送各种位置和统计信息的更新。 否则,您可能最终会遇到用户可以修改发送回服务器的数据的情况。

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

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