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

联网游戏服务器中ClientHandler的PrintWriter抛出NullPointerException排查

解决ClientHandler中PrintWriter空指针异常问题

问题本质

你遇到的NullPointerException核心原因是线程执行的异步性:
在Server的run方法中,创建ClientHandler并启动线程后,立刻调用handlers.get(0).out.println(),但此时ClientHandler的run方法可能还未执行到out = new PrintWriter(...)初始化代码,out变量仍为null,因此触发空指针异常。此外,ClientHandler内部嵌套额外线程处理输入属于冗余设计,会增加线程管理复杂度。

解决方案

1. 提前初始化IO流(最直接修复)

将PrintWriter和BufferedReader的初始化逻辑移到ClientHandler的构造方法中,确保创建实例时IO流已完成初始化,Server后续调用out时不会出现null。

2. 移除冗余嵌套线程

ClientHandler本身已是Runnable,直接在其run方法中处理客户端输入即可,无需额外启动新线程。

3. 修复其他潜在问题

  • 登录逻辑:原代码遍历handler判断用户名重复时,新用户的name尚未设置,会触发空指针,改为检查players列表;
  • switch分支:START case缺少break,会继续执行default分支的广播逻辑;
  • 线程安全:改用CopyOnWriteArrayList存储handlers,避免多线程操作ArrayList的并发问题。

修改后的代码示例

ClientHandler代码

import java.io.*;
import java.net.Socket;
import java.util.ArrayList;

public class ClientHandler implements Runnable{
    private Socket socket;
    private ArrayList<ClientHandler> handlers;
    private Server server;
    public PrintWriter out;
    private BufferedReader in;
    private String name;

    // 构造方法中初始化IO流,确保实例创建后即可使用
    public ClientHandler(Socket socket, ArrayList<ClientHandler> handlers, Server server){
        this.socket = socket;
        this.handlers = handlers;
        this.server = server;
        try {
            out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true);
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public void broadcastMessage(String msg){
        System.out.println("Broadcasting");
        for (ClientHandler s : this.handlers){
            s.out.println("Player: " + msg);
        }
    }

    public static String removePrefix(String s, String prefix)
    {
        if (s != null && s.startsWith(prefix)) {
            return s.split(prefix, 2)[1];
        }
        return s;
    }

    public String getName(){
        return name;
    }

    @Override
    public void run() {
        // 直接在当前线程处理输入,移除冗余嵌套线程
        try {
            String msg;
            while((msg = in.readLine()) != null){
                switch (msg.split(" ")[0]){
                    case "LOGIN":
                        name = removePrefix(msg,"LOGIN ");
                        server.login(name);
                        break;
                    case "HELLO":
                        server.hello();
                        break;
                    case "PLACE":
                        server.place(removePrefix(msg,"PLACE "));
                        break;
                    case "QUIT":
                        // todo 实现退出逻辑
                        break;
                    case "STOP":
                        // todo 实现停止逻辑
                        break;
                    case "START":
                        server.setStart();
                        break; // 补充break,避免执行default分支
                    default:
                        broadcastMessage(msg);
                        break;
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            // 关闭资源并从列表移除当前handler
            try {
                in.close();
                out.close();
                socket.close();
                handlers.remove(this);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Server代码(关键修改部分)

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.concurrent.CopyOnWriteArrayList;

public class Server implements Runnable{
    // 使用线程安全的CopyOnWriteArrayList存储客户端handler
    private ArrayList<ClientHandler> handlers = new CopyOnWriteArrayList<>();
    private ArrayList<Player> players = new ArrayList<>();
    private Game game;
    private boolean start;

    // ... 其他原有方法不变 ...

    public void login(String name){
        // 修改登录逻辑:检查players列表中的用户名是否重复
        boolean isTaken = false;
        for (Player p : players){
            if (p.getName().equals(name)){
                isTaken = true;
                break;
            }
        }
        if (isTaken){
            alreadyTaken(name);
        } else {
            // 找到当前客户端对应的handler并添加玩家
            for(ClientHandler handler : handlers){
                if(handler.getName().equals(name)){
                    players.add(new HumanPlayer(name, handler));
                    break;
                }
            }
        }
    }

    @Override
    public void run() {
        ServerSocket serverSocket;
        try {
            serverSocket = new ServerSocket(1800);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        System.out.println("----Server----");
        while (!serverSocket.isClosed()) {
            try {
                Socket socket = serverSocket.accept();
                ClientHandler handler = new ClientHandler(socket,handlers,this);
                handlers.add(handler);
                Thread h = new Thread(handler);
                h.start();
                System.out.println("A new client has connected");
                // 此时out已完成初始化,不会抛出空指针
                handlers.get(0).out.println("START? "+ handlers.size());
                if (start){
                    System.out.println("start request works");
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 01:00:59