You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java Socket双客户端石头剪刀布游戏:单客户端无法接收服务器消息

问题:Java Socket双客户端石头剪刀布服务端,单一客户端无法接收消息

我正在尝试用Java Socket实现一个支持两个客户端的石头剪刀布游戏服务端,但遇到了一个棘手的问题:其中一个客户端完全接收不到服务器发送的消息。以下是我的服务端代码片段:

package exam;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Scanner;
public class Server_Game {
public static void main(String[] args) throws Exception {
    // 这里是我的主逻辑代码,目前可能没有为每个客户端开独立线程
}
}

问题分析与解决方案

从你的代码片段来看,最可能的问题有两个核心点:

  1. 主线程被单个客户端的IO操作阻塞:如果你的服务端在accept客户端后,直接在主线程里处理该客户端的读写,那么当第一个客户端的IO操作(比如等待输入)阻塞主线程时,第二个客户端的消息发送逻辑根本无法执行。
  2. 输出流未及时flush:DataOutputStream默认会缓冲数据,如果写完消息后没有调用flush(),消息会滞留在缓冲区,客户端永远收不到。

下面是修复后的完整服务端实现,针对这两个问题做了优化:

package exam;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.CopyOnWriteArrayList;

public class Server_Game {
    // 线程安全的集合存储客户端处理器,避免并发修改问题
    private static CopyOnWriteArrayList<ClientHandler> clientHandlers = new CopyOnWriteArrayList<>();
    // 共享状态:记录两个玩家的选择,用同步块保证线程安全
    private static String[] playerChoices = new String[2];
    private static int choiceCount = 0;

    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(8888);
        System.out.println("石头剪刀布服务端已启动,等待玩家连接...");

        // 循环接收客户端连接
        while (true) {
            Socket clientSocket = serverSocket.accept();
            int playerId = clientHandlers.size();
            System.out.println("玩家" + (playerId + 1) + "已连接:" + clientSocket.getInetAddress());
            
            // 为每个客户端创建独立的处理线程
            ClientHandler handler = new ClientHandler(clientSocket, playerId);
            clientHandlers.add(handler);
            new Thread(handler).start();
        }
    }

    // 内部类:负责单个客户端的IO处理和游戏逻辑交互
    static class ClientHandler implements Runnable {
        private Socket socket;
        private DataInputStream inputStream;
        private DataOutputStream outputStream;
        private int playerId;

        public ClientHandler(Socket socket, int playerId) {
            this.socket = socket;
            this.playerId = playerId;
            try {
                inputStream = new DataInputStream(socket.getInputStream());
                outputStream = new DataOutputStream(socket.getOutputStream());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        @Override
        public void run() {
            try {
                // 向客户端发送初始提示消息,必须flush
                outputStream.writeUTF("欢迎加入游戏!你是玩家" + (playerId + 1) + "\n请输入你的选择:石头/剪刀/布");
                outputStream.flush();

                // 读取客户端的选择
                String playerChoice = inputStream.readUTF().trim();
                
                // 同步更新共享的选择状态,避免并发冲突
                synchronized (Server_Game.class) {
                    playerChoices[playerId] = playerChoice;
                    choiceCount++;
                }

                // 等待另一个玩家完成选择
                while (choiceCount < 2) {
                    Thread.sleep(100); // 短暂休眠,减少CPU占用
                }

                // 计算游戏结果
                String result = calculateResult(playerChoice, playerChoices[1 - playerId]);
                
                // 向当前玩家发送结果
                sendMessage("你的选择:" + playerChoice + "\n对方选择:" + playerChoices[1 - playerId] + "\n结果:" + result);
                // 向另一个玩家发送结果
                ClientHandler opponentHandler = clientHandlers.get(1 - playerId);
                opponentHandler.sendMessage("你的选择:" + playerChoices[1 - playerId] + "\n对方选择:" + playerChoice + "\n结果:" + result);

                // 重置游戏状态,准备下一轮
                synchronized (Server_Game.class) {
                    choiceCount = 0;
                    playerChoices = new String[2];
                }

            } catch (Exception e) {
                System.out.println("玩家" + (playerId + 1) + "连接异常:" + e.getMessage());
                e.printStackTrace();
            } finally {
                // 关闭资源并移除客户端处理器
                try {
                    if (inputStream != null) inputStream.close();
                    if (outputStream != null) outputStream.close();
                    if (socket != null) socket.close();
                    clientHandlers.remove(this);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        // 封装消息发送逻辑,确保flush
        private void sendMessage(String message) throws Exception {
            outputStream.writeUTF(message);
            outputStream.flush();
        }

        // 计算石头剪刀布的结果
        private String calculateResult(String myChoice, String opponentChoice) {
            if (myChoice.equalsIgnoreCase(opponentChoice)) {
                return "平局";
            }
            switch (myChoice.toLowerCase()) {
                case "石头":
                    return opponentChoice.equalsIgnoreCase("剪刀") ? "你赢了!" : "你输了...";
                case "剪刀":
                    return opponentChoice.equalsIgnoreCase("布") ? "你赢了!" : "你输了...";
                case "布":
                    return opponentChoice.equalsIgnoreCase("石头") ? "你赢了!" : "你输了...";
                default:
                    return "无效选择,游戏结果无效";
            }
        }
    }
}

关键修改点说明

  • 独立线程处理客户端:每个客户端连接后都会启动一个ClientHandler线程,避免主线程被单个客户端的IO操作阻塞,确保两个客户端的消息都能被及时处理。
  • 线程安全的客户端管理:使用CopyOnWriteArrayList存储客户端处理器,避免多线程环境下的并发修改异常。
  • 强制flush输出流:封装了sendMessage方法,每次发送消息后都会调用flush(),确保缓冲区的消息立即发送到客户端。
  • 同步共享状态:用synchronized块保护playerChoices和choiceCount这两个共享变量,避免并发读写导致的逻辑错误。

另外,记得检查你的客户端代码,确保客户端在读取服务器消息时没有出现阻塞(比如客户端是否在readUTF()后正确处理了输入,没有死循环等问题)。

内容的提问来源于stack exchange,提问作者sucket

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 07:45:19