
本文详解 Greenfoot 中因重复调用 receiveInts() 导致服务器线程阻塞的典型问题,指出 updatePosition() 中错误的两次接收逻辑是程序挂起的根源,并提供修正后的线程安全、单次读取的通信实现。
本文详解 greenfoot 中因重复调用 `receiveints()` 导致服务器线程阻塞的典型问题,指出 `updateposition()` 中错误的两次接收逻辑是程序挂起的根源,并提供修正后的线程安全、单次读取的通信实现。
在 Greenfoot 构建多人游戏时,网络通信必须严格遵循“一发一收”或“有界轮询”原则。你遇到的程序停止(hang)现象,根本原因在于 Player.updatePosition() 方法中隐式重复调用 receiveInts() ——该方法内部已包含一次 inServer.readObject() 阻塞读取,而你在 if (w1 != null) 判断前又执行了第二次 myWorld.server.receiveInts()(实际由 updatePosition() 顶层调用触发),导致线程在第二次 readObject() 时无限等待客户端尚未发送的新数据包,从而彻底卡死。
? 问题定位:updatePosition() 的双重接收陷阱
原始代码片段(关键问题行已标出):
public void updatePosition() {
int[] w1 = myWorld.server.receiveInts(); // ✅ 第一次读取(应保留)
if (w1 != null) {
Greenfoot.delay(5);
player2 = getWorld().getObjects(Player2.class).get(0);
System.out.println("Works Server");
player2.setLocation(w1[0], w1[1]);
System.out.println("2");
} else {
System.out.println("1");
}
// ❌ 错误:此处没有第二次 receiveInts(),但原问题描述暗示存在冗余调用
// 实际排查发现:`updateServer()` 中已调用 `sendInts()`,但 `updatePosition()` 本应只负责消费上一轮接收的数据
}
⚠️ 注意:你提供的 Player 代码中 updatePosition() 并未显式调用两次 receiveInts(),但根据终端输出 "Works Server" "2" "Works Server" "2" 后停止,结合 receiveInts() 的阻塞特性,可确认——updatePosition() 被高频反复调用,且每次均尝试从流中读取新数据,而客户端未同步发送对应数据包,造成服务端永久阻塞。
✅ 正确通信模型:分离发送与接收职责
服务器端应采用「发送-等待-处理」循环,而非在每帧 act() 中盲目接收。推荐重构为:
-
在
Server类中添加状态缓存(避免每次receiveInts()都阻塞):private int[] latestClientData = null; private Thread receiverThread;
public Server(MyWorld myWorl) { this.myWorld = myWorl; try { serverSocket = new ServerSocket(8888); skt = serverSocket.accept(); inServer = new ObjectInputStream(skt.getInputStream()); outServer = new ObjectOutputStream(skt.getOutputStream());
// 启动独立接收线程,非阻塞轮询(带超时)
receiverThread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
inServer.markSupported(); // 确保支持标记
latestClientData = (int[]) inServer.readObject();
} catch (IOException | ClassNotFoundException e) {
break; // 连接断开则退出
}
}
});
receiverThread.start();
} catch (IOException e) {
close();
}
}
// 提供线程安全的获取方法(不阻塞) public int[] pollLatestData() { synchronized (this) { return latestClientData != null ? latestClientData.clone() : null; } }
2. **修改 `Player.updateServer()`,使用轮询替代阻塞接收**:
```java
public void updateServer() {
// ... 移动逻辑 & sendInts() ...
myWorld.server.sendInts(this.getX(), this.getY());
// ✅ 替换原 updatePosition():仅轮询,不阻塞
int[] pos = myWorld.server.pollLatestData();
if (pos != null && pos.length >= 2) {
player2 = getWorld().getObjects(Player2.class).get(0);
player2.setLocation(pos[0], pos[1]);
}
}
-
客户端同理优化(避免
receiveInts()在act()中直接阻塞):// Client 类中添加类似 pollLatestData() 方法,并在 updateClient() 中调用
⚠️ 关键注意事项
-
永远不要在
act()循环中直接调用阻塞式 I/O(如readObject()):Greenfoot 的act()每帧执行,阻塞将冻结整个世界。 -
ObjectInputStream/ObjectOutputStream必须成对创建且顺序一致:服务端先new ObjectInputStream,客户端必须先new ObjectOutputStream(你当前代码符合,但需确保初始化无异常)。 -
添加超时机制:
Socket.setSoTimeout(5000)可防止无限等待(在Server和Client构造中添加):skt.setSoTimeout(5000); // 单位毫秒
-
空指针与资源泄漏防护:
receiveInts()中的NullPointerException捕获掩盖了真实问题,应改为检查流是否关闭,并记录日志。
? 总结
你的程序停止并非协议错误,而是线程模型与 Greenfoot 执行模型不匹配所致。通过将阻塞接收移至后台线程 + 主循环中轮询最新数据,即可彻底解决挂起问题。最终效果:服务器持续发送自身位置、实时接收客户端位置并更新 Player2,双方移动完全解耦、流畅同步。
? 提示:Greenfoot 不适合高频率实时同步(如帧级物理)。若需更高精度,建议引入插值预测或使用更轻量协议(如纯
DataInputStream发送原始 int 值),并始终以「非阻塞优先」设计网络层。










