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

Android WebSocket客户端接收首条消息后服务端报EOFException断连问题

问题根因

服务端抛出java.io.EOFException后连接立刻断开,是三类问题共同导致:

  • 服务端配置缺失:仅引入javax.websocket-api的provided依赖时,Spring Boot内嵌Tomcat未完成WebSocket容器初始化,且默认会话空闲超时极短(部分版本默认30秒无读写就主动断开连接,断开时未发送标准WebSocket关闭帧,直接触发EOF异常)
  • 客户端无保活机制:Android系统、运营商NAT路由会主动回收空闲Socket连接,Java-WebSocket 1.4.0版本默认未开启自动心跳,无数据传输时连接会被中间节点掐断
  • 客户端逻辑缺陷:在Service中创建JWebSocketClient匿名子类时,onOpen回调直接调用未初始化完成的外层client对象发消息,部分场景会触发连接状态异常;Service未设为前台服务,退后台后容易被系统回收

分步修复方案

1. 修复服务端配置

替换原有javax.websocket依赖,引入Spring Boot官方WebSocket Starter:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

添加WebSocket配置类注册服务端点:

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

修改WSServer类,配置会话超时规则,补全心跳响应逻辑:

@Component
@ServerEndpoint(value = "/dialer")
public class WSServer {
    // 生产环境建议设为300000(5分钟),配合客户端心跳使用,设为0表示永不超时
    private static final long MAX_IDLE_TIMEOUT = 300000;

    @OnOpen
    public void onOpen(Session session) throws IOException {
        session.setMaxIdleTimeout(MAX_IDLE_TIMEOUT);
        System.out.println("Client Connected");
    }

    @OnMessage
    public void onMessage(Session session, String msg) throws IOException {
        System.out.println("Message received: " + msg);
        // 响应客户端心跳
        if ("ping".equals(msg)) {
            session.getBasicRemote().sendText("pong");
        }
    }

    @OnClose
    public void onClose(Session session) throws IOException {
        System.out.println("Session Closed");
    }

    @OnError
    public void onError(Session session, Throwable throwable) {
        System.out.println("Session Exception: "+throwable.getMessage());
    }
}

2. 修复客户端实现,添加保活与重连

升级Java-WebSocket依赖到稳定版,在app模块的build.gradle中添加依赖:

dependencies {
    implementation 'org.java-websocket:Java-WebSocket:1.5.4'
}

修改JWebSocketClient类,添加自动心跳、自动重连逻辑,修复对象引用问题:

public class JWebSocketClient extends WebSocketClient {
    // 心跳间隔30秒,适配国内运营商NAT超时规则
    private static final int HEARTBEAT_INTERVAL = 30;

    public JWebSocketClient(URI serverUri) {
        super(serverUri, new Draft_6455());
        // 开启库自带的连接存活检测,自动发送ping帧
        setConnectionLostTimeout(HEARTBEAT_INTERVAL);
    }

    @Override
    public void onOpen(ServerHandshake handshakedata) {
        Log.i("JWebSocketClient", "Connection established");
        // 直接调用当前对象的send方法,不要引用外部未初始化的client实例
        send("connect:::TESTKEY");
    }

    @Override
    public void onMessage(String message) {
        Log.i("JWebSocketClient", "Received message: " + message);
    }

    @Override
    public void onClose(int code, String reason, boolean remote) {
        Log.i("JWebSocketClient", "Connection closed, code: " + code + " reason: " + reason);
        // 非主动关闭场景,延迟3秒自动重连
        if (code != 1000) {
            new Handler(Looper.getMainLooper()).postDelayed(() -> {
                if (!isOpen() && !isConnecting()) {
                    try {
                        reconnectBlocking();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, 3000);
        }
    }

    @Override
    public void onError(Exception ex) {
        Log.e("JWebSocketClient", "Connection error", ex);
    }
}

修改JWebSocketClientService,改为前台服务避免被系统回收,去掉重复的匿名客户端实现:

public class JWebSocketClientService extends Service {
    private static final int FOREGROUND_ID = 1001;
    public JWebSocketClient client;
    private final JWebSocketClientBinder mBinder = new JWebSocketClientBinder();

    class JWebSocketClientBinder extends Binder {
        public JWebSocketClientService getService() {
            return JWebSocketClientService.this;
        }
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // Android 8.0以上必须启动前台服务,否则后台会被系统强制杀死
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel("ws_conn", "长连接服务", NotificationManager.IMPORTANCE_LOW);
            NotificationManager manager = getSystemService(NotificationManager.class);
            manager.createNotificationChannel(channel);
            Notification notification = new Notification.Builder(this, "ws_conn")
                    .setContentTitle("业务服务运行中")
                    .setSmallIcon(R.mipmap.ic_launcher)
                    .build();
            startForeground(FOREGROUND_ID, notification);
        }
        // 阻塞连接方法必须放在子线程调用,避免ANR
        new Thread(() -> {
            try {
                connectToWS();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        // 服务被系统杀死后自动重启
        return START_STICKY;
    }

    public void connectToWS() throws InterruptedException {
        Log.i("websocket", "Start connecting to WS server");
        try {
            // 替换为实际服务端地址,模拟器访问宿主机用10.0.2.2,真机用局域网IP
            URI uri = new URI("ws://{实际服务IP:端口}/dialer");
            client = new JWebSocketClient(uri);
            client.connectBlocking();
        } catch (Exception e) {
            Log.e("WS_CONN", "Connect failed", e);
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (client != null && client.isOpen()) {
            client.close(1000, "Client normal exit");
        }
        stopForeground(true);
    }
}

3. 必要配置补充

  • Android 9及以上系统默认禁止明文传输,需要在AndroidManifest.xml的<application>标签下添加android:usesCleartextTraffic="true"配置,否则ws连接会被系统拦截
  • 不要在主线程调用connectBlocking()、reconnectBlocking()这类阻塞方法,否则会触发应用无响应
  • 真机测试时保证手机和服务端在同一局域网,不要用localhost/127.0.0.1作为服务地址

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 11:09:28