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
相关产品推荐
相关产品推荐

