User Tools

Site Tools


zh_cn:tutorial:networking

This is an old revision of the document!


注意:本页已经取代旧版页面。建议使用本页描述的新的网络API。旧的页面参见此处(英文)

网络通信

Minecraft 中的网络通信用于客户端与服务器相互通信。网络是个比较广泛的主题,所以本页分成了几个类别。

例子:网络通信为何重要?

一段简单的代码可以清楚地展示网络通信有多重要。您*不需要*使用这段代码,这段代码只是用来解释网络通信的重要性的。

假如你有根魔杖,可以向附近的所有玩家展示你正在查看的方块。

  1. class HighlightingWandItem extends Item {
  2. public HighlightingWand(Item.Settings settings) {
  3. super(settings)
  4. }
  5.  
  6. public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
  7. // 视线追踪并找到你面对的方块
  8. BlockPos target = ...
  9.  
  10. // 不好的代码:别这么写:
  11. ClientBlockHighlighting.highlightBlock(MinecraftClient.getInstance(), target);
  12. return super.use(world, user, hand);
  13. }
  14. }

测试后,你会看到你面对的方块高亮了,并且没有崩溃。现在你想向你的朋友展示这个模组,启动一个专用服务器并邀请朋友安装模组。你使用了这个物品,结果服务器崩了……你可能会注意到崩溃日志中有这样的错误:

[Server thread/FATAL]: Error executing task on Server
java.lang.RuntimeException: Cannot load class net.minecraft.client.MinecraftClient in environment type SERVER

为什么服务器崩溃?

因为代码调用的逻辑只有 Minecraft 的客户端分发中存在。Mojang 这样分发游戏的是为了减少 Minecraft 服务器的 jar 文件的大小。服务器没有理由包含整个渲染引擎,渲染引擎只会在你自己的机器渲染世界时才会用到。在开发环境中,一些类注解了 @Environment(EnvType.CLIENT),表示这些类仅存在于客户端。

怎样修复崩溃?

要修复崩溃,你需要了解 Minecraft 如何在客户端和专用服务器之间通信(communication)。

从上面这张图可以看到,游戏客户端和专用服务器是相互分离的系统,并使用数据包(packets,注意不是 datapack)桥接在一起。这个数据包桥(packet bridge)不仅存在于游戏客户端和专用服务器之间,还存在于您的客户端和通过 LAN 连接的另一个客户端之间。注意即使是单人游戏也有数据包桥,这是因为游戏客户端会启动一个特殊的集成服务器实例来运行游戏。下表显示了三种连接类型之间的主要区别:

连接类型 访问游戏客户端
连接至专用服务器 不可以 → 服务器崩溃
通过局域网(LAN)连接 可以 → 非主机游戏客户端
单人(或者LAN主机) 可以 → 完全访问

这样以三种不同的方式与服务器进行通信看上去很复杂,但您不并需要以三种不同的方式与游戏客户端进行通信。由于所有三种连接类型都使用数据包与游戏客户端进行通信,因此只需与游戏客户端通信,就像始终在专用服务器上运行一样。通过无线局域网(LAN)连接到服务器或者单人游戏时也可以将服务器看做是远程的专用服务器,所以你的游戏客户端不能直接访问服务器实例。

网络简介

首先解决上面展示的示例代码的问题。我们使用数据包与客户端进行通信,所以希望确保仅在服务器上启动操作时才发送数据包。

将数据包发送至游戏客户端

  1. public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
  2. // 确认我们是否是在逻辑服务器上进行操作
  3. if (world.isClient()) return super.use(world, user, hand);
  4.  
  5. // 视线追踪并找到玩家朝向的方块
  6. BlockPos target = ...
  7.  
  8. // 不好的代码:不要这样写!
  9. ClientBlockHighlighting.highlightBlock(MinecraftClient.getInstance(), target);
  10. return TypedActionResult.success(user.getStackInHand(hand));
  11. }

接下来,我们需要将数据包发送到游戏客户端。首先需要定义一个用于识别数据包的 Identifier。对于本例,我们的标识符为 wiki_example:highlight_block。为将数据包发送到游戏客户端,需要指定要哪个玩家的游戏客户端接收数据包。由于该操作发生在逻辑服务器上,所以可以将 player 向上强转为 ServerPlayerEntity

要将数据包发送到玩家,我们使用 ServerPlayerNetworking 中的一些方法。我们使用该类中的以下方法:

public static void send(ServerPlayerEntity player, Identifier channelName, PacketByteBuf buf) {
    ...

数据包将会被发送到此方法中的玩家。通道名称是你之前决定用来识别数据包的 IdentifierPacketByteBuf 用于存储数据包的数据。We will return later to writing data to the packet's payload via the buf.

由于我们没有向数据包写入任何数据,所以现在,我们将发送带有空有效负载的数据包。可以使用 PacketByteBufs.empty() 创建带有空有效负载的 buf。

  1. ....
  2. ServerPlayNetworking.send((ServerPlayerEntity) user, TutorialNetworkingConstants.HIGHLIGHT_PACKET_ID, PacketByteBufs.empty());
  3. return TypedActionResult.success(user.getHandStack(hand));
  4. }

虽然你已经向游戏客户端发送了一个数据包,但游戏客户端无法对数据包做任何事情,因为客户端不知道如何接收数据包。关于游戏客户端接收数据包的信息请参见下方:

Receiving a packet on the game client

To receive a packet from a server on the game client, your mod needs to specify how it will handle the incoming packet. In your client entrypoint, you will register the receiver for your packet using ClientPlayNetworking.registerGlobalReceiver(Identifier channelName, ChannelHandler channelHandler)

The Identifier should match the same Identifier you use to send the packet to the client. The ChannelHandler is the functional interface you will use to implement how the packet is handled. Note the ChannelHandler should be the one that is a nested interface of ClientPlayNetworking

The example below implements the play channel handler as a lambda:

  1. ClientPlayNetworking.registerGlobalReceiver(TutorialNetworkingConstants.HIGHLIGHT_PACKET_ID, (client, handler, buf, responseSender) -> {
  2. ...
  3. });

However, you cannot draw the highlight box immediately. This is because the receiver is called on the netty event loop. The event loop runs on another thread, and you must draw the highlight box on the render thread.

In order to draw the highlight box, you need to schedule the task on the game client. This may be done with the client field that is provided in the channel handler. Typically you will run the task on the client by using the execute method:

  1. ClientPlayNetworking.registerGlobalReceiver(TutorialNetworkingConstants.HIGHLIGHT_PACKET_ID, (client, handler, buf, responseSender) -> {
  2. client.execute(() -> {
  3. // Everything in this lambda is run on the render thread
  4. ClientBlockHighlighting.highlightBlock(client, target);
  5. });
  6. });

You may have noticed you are not told where the block to highlight is. You can write this data to the packet byte buf. Instead of sending PacketByteBufs.empty() to the game client in your item's use method, instead, you will create a new packet byte buf and send that instead.

  1. PacketByteBuf buf = PacketByteBufs.create();

Next, you need to write the data to the packet byte buf. It should be noted that you must read data in the same order you write it.

  1. PacketByteBuf buf = PacketByteBufs.create();
  2.  
  3. buf.writeBlockPos(target);

Afterwards, you will send the buf field through the send method.

To read this block position on the game client, you can use PacketByteBuf.readBlockPos().

You should read all data from the packet on the network thread before scheduling a task to occur on the client thread. You will get errors related to the ref count if you try to read data on the client thread. If you must read data on the client thread, you need to retain() the data and then read it on the client thread. If you do retain() the data, make sure you release() the data when you no longer need it.

In the end, the client's handler would look like this:

  1. ClientPlayNetworking.registerGlobalReceiver(TutorialNetworkingConstants.HIGHLIGHT_PACKET_ID, (client, handler, buf, responseSender) -> {
  2. // Read packet data on the event loop
  3. BlockPos target = buf.readBlockPos();
  4.  
  5. client.execute(() -> {
  6. // Everything in this lambda is run on the render thread
  7. ClientBlockHighlighting.highlightBlock(client, target);
  8. });
  9. });

Sending packets to the server and receiving packets on the server

Sending packets to a server and receiving a packet on the server is very similar to how you would on the client. However, there are a few key differences.

Firstly sending a packet to the server is done through ClientPlayNetworking.send. Receiving a packet on the server is similar to receiving a packet on the client, using the ServerPlayNetworking.registerGlobalReceiver(Identifier channelName, ChannelHandler channelHandler) method. The ChannelHandler for the server networking also passes the ServerPlayerEntity (player) who sent the packet through the player parameter.

The concept of tracking and why you only see the highlighted block

Now that the highlighting wand properly uses networking so the dedicated server does not crash, you invite your friend back on the server to show off the highlighting wand. You use the wand and the block is highlighted on your client and the server does not crash. However, your friend does not see the highlighted block. This is intentional with the code that you already have here. To solve this issue let us take a look at the item's use code:

  1. public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
  2. // Verify we are processing the use action on the logical server
  3. if (world.isClient()) return super.use(world, user, hand);
  4.  
  5. // Raycast and find the block the user is facing at
  6. BlockPos target = ...
  7. PacketByteBuf buf = PacketByteBufs.create();
  8. buf.writeBlockPos(target);
  9.  
  10. ServerPlayNetworking.send((ServerPlayerEntity) user, TutorialNetworkingConstants.HIGHLIGHT_PACKET_ID, buf);
  11. return TypedActionResult.success(user.getHandStack(hand));
  12. }

You may notice the item will only send the packet to the player who used the item. To fix this, we can use the utility methods in PlayerLookup to get all the players who can see the highlighted block.

Since we know where the highlight will occur, we can use PlayerLookup.tracking(ServerWorld world, BlockPos pos) to get a collection of all players who can see that position in the world. Then you would simply iterate through all players in the returned collection and send the packet to each player:

  1. public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
  2. // Verify we are processing the use action on the logical server
  3. if (world.isClient()) return super.use(world, user, hand);
  4.  
  5. // Raycast and find the block the user is facing at
  6. BlockPos target = ...
  7. PacketByteBuf buf = PacketByteBufs.create();
  8. buf.writeBlockPos(target);
  9.  
  10. // Iterate over all players tracking a position in the world and send the packet to each player
  11. for (ServerPlayerEntity player : PlayerLookup.tracking((ServerWorld) world, target)) {
  12. ServerPlayNetworking.send(player, TutorialNetworkingConstants.HIGHLIGHT_PACKET_ID, buf);
  13. }
  14.  
  15. return TypedActionResult.success(user.getHandStack(hand));
  16. }

After this change, when you use the wand, your friend should also see the highlighted block on their own client.

Advanced Networking topics

The Networking system Fabric API supplies is very flexible and supports additional features other than just sending and receiving simple packets. As some of these more advanced topics are long, here are links to their specific pages:

Networking Topic Description
Connection Network connection events Events related to the the lifecycle of a connection to a client or server
Channel registration events Events related to a server of client declaring the ability to receive a packet on a channel of a specific name
Login phase networking Sending requests to a client during login; and allowing delay of login for a short amount of time
Dynamic registration of channel handlers Allowing for a connection to receive a packet with a special handler
zh_cn/tutorial/networking.1643352434.txt.gz · Last modified: 2022/01/28 06:47 by solidblock