Using Marauroa-Network Design
转自https://stendhalgame.org/wiki/NetworkDesign
请注意:此页面介绍了低级网络通信。 如果您想使用Marauroa编写游戏,则无需为这些实现细节烦恼。 我们为Marauroa本身的贡献者记录网络设计。 对于将Marauroa移植到其他编程语言的人来说很有帮助。
Messages
Marauroa使用消息在客户端和服务器之间进行通信。 从客户端发送到服务器的消息以C2S为前缀,从服务器发送到客户端的消息使用前缀S2C。
每条消息都在marauroa.common.net.message包中的自己的Java类中实现。 您可以在javadoc中查找有关每条消息的详细信息。 如果您想将Marauroa移植到另一种编程语言,您需要知道消息是如何准确序列化的。 最简单的学习方法是查看readObject()和writeObject()方法的源代码。
游戏的不同客户状态有:连接,登录,游戏,注销。 根据状态,不同的消息有效:
State connected
由于安全性要求,登录过程有点复杂。不要害怕,只需一步一步地遵循它。
一旦建立了TCP连接,客户端就会使用C2SLoginRequestKey从服务器请求RSA公钥。服务器检查每个消息中隐含的协议版本。如果它是兼容的,它将回复包含RSA公钥的S2CLoginSendKey。它由两个字节数组组成:第一个包含'n'的值,第二个包含'e'的值。
客户端现在计算一个nonce(随机数)并将其散列作为字节数组发送到C2SLoginPromise消息中的服务器。服务器记住客户端nonce并在S2CLoginSendNonce中使用自己的nonce进行回答。
即:客户端现在拥有实际发送C2SLoginSendNonceNameAndPassword所需的所有信息:其nonce,用户名和值rsaCrypt(xor(xor(客户端nonce,服务器nonce),密码))。第一个字段是包含客户端nonce的字节数组,第二个字段是包含用户名的字符串,第三个字段是包含加密密码的字节数组。在接收时,服务器检查他最初收到的哈希是他刚收到的nonce的哈希值。然后它解码密码字段,并具有客户端nonce值及其随机数,它获得密码的值。
S2CLoginNACK消息从服务器发送到客户端,告诉客户端其登录请求被拒绝,因为用户名或密码错误,帐户被禁止或服务器已满。包含的结果对象将告知哪些情况阻止了登录。
但是,如果用户名/密码组合正确,则服务器必须发送S2CLoginACK消息以告知客户端消息已正确处理。它包含有关上次登录的信息,以便用户能够识别未经授权使用其帐户。在这种情况下,客户端状态更改为“已登录”。
State logged in
登录的东西很复杂。但幸运的是现在事情变得更加容易了。登录成功完成后,服务器发送S2CServerInfo消息。它告诉客户端正在运行什么类型的服务器,以及有关如何联系服务器管理员的详细信息(例如,他们的电子邮件地址)。该消息由“attribute = value”形式的字符串列表组成。此外,该消息还包含已定义的RPClasses列表。
之后,直接从服务器向客户端发送S2CCharacterList消息。它提供了一个可供选择的角色。此功能模拟了与单个帐户关联的多个角色的行为。每个角色名称在服务器级别必须是唯一的,并在创建角色时分配。
现在,客户端选择一个提供的角色。不支持多个角色的游戏可以选择与帐户名称匹配的角色。使用C2SChooseCharacter消息将选择发送到服务器。角色的名称必须是角色列表中列出的名称之一。
如果选择无效,服务器将检查所选角色并回复S2CChooseCharacterNACK。这意味着客户端应该发送另一个C2SChooseCharacter消息。
但是,如果选择成功,则发送ChooseCharacterACK消息并将客户端状态更改为“在游戏中”。
State in game
Regular Messages
S2CPerception消息是从服务器发送到客户端的消息,用于通知客户端有关其附近对象的更改。 该消息基于Delta Perception文档中解释的想法。
该消息由以下内容组成:
- A type that can be DELTA or TOTAL
- A string indicating the name of the zone the perception is related to.
- A time stamp value that will be just one unit bigger than the previous perception
- A List of RPObject that contains added object
- A List of RPObject that contains modified added object attributes
- A List of RPObject that contains modified deleted object attributes
- A List of RPObject that contains deleted object
- A RPObject that describes the things the rest of players don't see about OUR own object.
阅读Delta感知算法以了解它的用途。
客户端定期发送C2SKeepAlive消息。 如果一段时间内没有保持活动消息,则服务器将使客户端超时。
Static Content Transfer
感知是关于动态内容。但是大多数游戏也有一些静态数据。像地图,瓦片,声音。 RPManager跟踪客户端可能需要某些静态数据的情况。一个常见的情况是客户从一个区域移动到另一个区域。
如果RPManager检测到这种情况,它将使用S2CTransferREQ消息向客户端提供新内容。该消息由一个TransferContent对象数组组成,其中包含每个资源的名称,时间戳(或校验和)以及资源是否可缓存。
客户回复C2STransferACK确认。该消息由一个TransferContent对象数组组成,该对象包含每个资源的所有名称和一个表示ack的标志。注意:即使所有标志都指示不应进行传输,也始终发送C2STransferACK消息。
如果客户端已确认需要传输内容,则服务器会发送S2CTransfer。该消息再次包含一个TransferContent对象数组。但是,这一次也包含了实际数据。
Actions
Actions是使用C2SAction消息从客户端发送到服务器的命令。 命令的示例是“向右移动”,“查看该对象”,“攻击该对象”。 由服务器决定是执行操作还是拒绝操作。
Logging Out
如果玩家处于某种类型的战斗中,通常希望通过退出来阻止他保存他的生命。 因此,客户端向服务器发送注销请求,游戏服务器可以决定是接受还是拒绝它。 当然,用户可以关闭客户端窗口或强制断开其Internet连接。 但在这些情况下,无论如何,他都会在无人看管的情况下直接进入游戏。
客户端通过发送C2SLogout指示它想要完成会话。
服务器可以使用S2CLogoutNACK进行回复以拒绝注销请求。 或者它使用S2CLogoutACK确认请求。 在这种情况下,客户端状态将更改为已注销。
Transmitting Messages over TCP
Marauroa的网络协议背后的想法是在服务器和客户端之间使用单个TCP流。 不同类型的游戏内动作创建不同类型的消息,然后在相反的一侧将其解释为有意义的数据。 TCP负责对数据包进行排序并重新传输丢失的数据包。
每条消息都有一个通用头:
* 消息大小(4个字节)
* 协议版本(1字节)
* 消息类型(1个字节)
* 客户端ID(4个字节)
* 时间戳(4个字节)
此通用头后跟消息特定数据。 查看相关消息的readObject()和writeObject()方法的源代码。
Network Manager
网络管理器是我们的路由器,用于向网络发送消息和从网络接收消息。网络管理器公开允许以下内容的接口:
- Reading a message from the network
- Sending a message to the network
- Finalizing the manager
读取操作是阻塞类型操作,因此我们有两个选项,轮询(即连续检查数据是否存在)或阻塞(即仅在数据实际可用时处理,否则休眠)。
我们之所以选择阻止是因为我们不想浪费CPU时间来轮询网络中的消息,我们只是想睡觉直到消息可用。因此我们创建一个Thread来从网络中读取,我们称之为NetworkManagerRead。
将消息写入网络可以简单地编码为网络管理器的方法,因为写操作本质上是非阻塞的操作。
NetworkManager打开一个Socket,它将从该Socket接收来自网络的所有消息。它还会从同一个套接字将所有出站消息写入网络。注意:写入和读取都使用相同的Socket。
为了封装所有这些,我们将Read和Write方法创建为Network Manager的内部类。
NetworkManager
{
socket
messages
pendingToSendMessages
NetworkManagerRead isa Thread
{
read socket
build message
store in messages
}
NetworkManagerWrite isa Thread
{
get from pendingToSendMessages
serialize message
send socket
}
}
如您所见,消息在收到时存储在列表中。 因此,必须同步对列表的访问。
现在让我们回到接口,暴露给其他对象。 write方法是立即的,只需使用要发送的消息调用它,确保您已正确填充SourceAddress和ClientID。 然后该消息将发送给客户端。
read方法是阻塞的,当你调用read方法时,它会从队列中返回一条消息,或者如果队列为空,则线程会阻塞(休眠)直到一个到达。
这是网络管理器的基本思想。 请注意,网络管理器只发送一次数据包流,但不确认是否收到任何消息。 TCP处理这个问题。