【Android+WebSocket】全局单例封装
文章目录
- 【Android+WebSocket】全局单例封装
- 摘要
- 为什么需要全局单例WebSocket?
- 核心设计思路
- 什么是单例?
- WebSocketManager
- 实战代码
- 从0到1实现全局WebSocket管理器
- 使用方式
- 总结
- 注意事项
摘要
在Android开发中,WebSocket是實現实时通信的常用方案。然而,如果每个页面都独立建立WebSocket连接,会导致连接数失控、状态同步困难、资源浪费等一系列问题。本文深入分析了不使用单例模式的弊端,并从零开始封装一个全局唯一的WebSocket管理器。该管理器基于OkHttpClient实现,采用饿汉式单例保证全局唯一性,通过原子类保证线程安全,实现了自定义心跳保活机制、指数退避自动重连策略以及观察者模式的消息分发。文章提供了完整的代码实现和使用示例,帮助开发者快速构建稳定、高效、易维护的WebSocket长连接服务。
Abstract: When using WebSocket across multiple pages in Android applications, developers often encounter issues such as uncontrolled connection counts, difficult state synchronization, and resource waste. This article implements a global singleton WebSocket manager from scratch, adopting the eager initialization singleton pattern with OkHttpClient as the connection foundation. It fully implements core features including heartbeat keep-alive, exponential backoff reconnection, and message listening. The solution helps developers build stable and efficient long-connection services for their Android apps.
为什么需要全局单例WebSocket?
在开始编写代码之前,我们先来分析一下不使用单例模式可能带来的问题:
- 连接数失控:如果每个页面都独立建立WebSocket连接,每当打开新页面时都需要重新建立连接并添加监听,这会给服务器带来巨大压力。
- 状态同步困难:不同页面之间的WebSocket状态难以保持一致。例如,页面1已经检测到连接断开,而页面2仍在尝试向后端发送消息;又如多个页面同时监听同一字段(如消息角标),容易导致角标异常递增和消息重复处理。
- 资源严重浪费:每个独立的WebSocket连接都需要经过HTTP握手升级过程,这会消耗大量的网络资源和内存,降低应用整体性能。
核心设计思路
什么是单例?
单例模式确保一个类仅有一个实例,只需要一次初始化,并提供一个全局访问点。
WebSocketManager
我们将创建一个WebSocketManager类或对象,它具备以下能力:
- 全局只初始化一次
- 不管在哪里调用getInstance(),拿到的都是同一个实例
- 自动处理连接、心跳、重连
- 解耦,页面只需要关心消息
实战代码
这里以饿汉式为例(当然可以选择懒汉式、双重检查锁、静态内部类、枚举),先构造私有构造函数,初始化OkHttpClient
@GetterprivatestaticfinalSysWsManagerinstance=newSysWsManager();privateSysWsManager(){client=newOkHttpClient.Builder().connectTimeout(10,SECONDS)// 连接超时.writeTimeout(10,SECONDS)// 写入超时.readTimeout(10,SECONDS)// 读取超时.pingInterval(0,SECONDS)// 禁用OkHttp自带心跳,使用自定义心跳.retryOnConnectionFailure(true)// 连接失败自动重试(效果有限).build();}初始化函数,只需要在继承Application的类中调用一次
publicvoidinit(Contextcontext){this.appContext=context.getApplicationContext();}从0到1实现全局WebSocket管理器
WebSocket连接方法
@GetterprivateAtomicBooleanisConnected=newAtomicBoolean(false);publicCompletableFuture<Boolean>connect(StringunionUid){// 如果有心跳或处理正在重连,这里要先停止或取消// 如果已经连接,直接返回成功if(isConnected.get()&&webSocket!=null){returnCompletableFuture.completedFuture(true);}// 创建异步结果对象connectionFuture=newCompletableFuture<>();// 构建 WebSocket urlStringurl=NetworkApiConfig.WS_URL+unionUid;Requestrequest=newRequest.Builder().url(url).build();// 发起 WebSocket 连接webSocket=client.newWebSocket(request,newWebSocketListener(){@OverridepublicvoidonOpen(@NonNullWebSocketwebSocket,@NonNullResponseresponse){}@OverridepublicvoidonFailure(@NonNullWebSocketwebSocket,@NonNullThrowablet,Responseresponse){}@OverridepublicvoidonClosed(@NonNullWebSocketwebSocket,intcode,@NonNullStringreason){}@OverridepublicvoidonMessage(@NonNullWebSocketwebSocket,@NonNullStringtext){}});returnconnectionFuture;}处理心跳指数退避,原子布尔值保证状态线程安全
privateScheduledFuture<?>heartbeatFuture;privateAtomicIntegermissedHeartbeats=newAtomicInteger(0);privatestaticfinalintMAX_MISSED_HEARTBEATS=2;开始心跳
privatevoidstartHeartbeat(){// 先停止心跳stopHeartbeat();heartbeatFuture=scheduler.scheduleWithFixedDelay(()->{if(!isConnected.get()||webSocket==null){// 未连接->停止心跳stopHeartbeat();}intmissed=missedHeartbeats.incrementAndGet();if(missed>MAX_MISSED_HEARTBEATS){// n次未收到心跳,主动断开连接return;}booleansuccess=webSocket.send("ping");if(success){}else{// 主动断开连接}},HEARTBEAT_INTERVAL_SECONDS,HEARTBEAT_INTERVAL_SECONDS,SECONDS);}停止心跳
privatevoidstopHeartbeat(){if(heartbeatFuture!=null&&!heartbeatFuture.isDone()){heartbeatFuture.cancel(false);heartbeatFuture=null;}missedHeartbeats.set(0);}重点指数退避,调度重连,优雅地解决连续访问后端导致浪费资源的问题同时上限60秒,又不会造成用户长时间等待的窘迫
privatevoidscheduleReconnect(){if(reconnectAttempts>=MAX_RECONNECT_ATTEMPTS){// 已达到最大重连次数,停止重连// 停止重连逻辑return;}// 指数退避:2, 4, 8, 16, 32, 60(max)intdelay=BASE_RECONNECT_DELAY_SECONDS*(int)Math.pow(2,reconnectAttempts);delay=Math.min(delay,60);reconnectAttempts++;// 通知监听器ws连接状态notifyReconnecting(reconnectAttempts);reconnectFuture=scheduler.schedule(()->{if(!isManualDisconnect&¤tUnionUid!=null&&!isConnected.get()){connect(currentUnionUid);}},delay,SECONDS);}发送消息
publicvoidsendMessage(Stringmessage){if(!isConnected.get()||webSocket==null){// 检查连接状态return;}booleansuccess=webSocket.send(message);if(success){// 自行提示/日志/提示框等待}else{}}使用方式
全局(extends Application中的重写onCreate()方法中)
根据业务在获取到uid后进行连接操作
/* 全局WebSocket初始化 */SysWsManager.getInstance().init(this);SysWsManager.getInstance().connect(tokenVo.getLinkUnionUid()).thenAccept(isConnect->{if(isConnect){// 连接成功return;}// 连接失败});发送消息
// 构建jsonJSONObjectjsonObject=newJSONObject();try{jsonObject.put("type",type);// jsonObject.put("", );// jsonObject.put("", );}catch(JSONExceptione){thrownewRuntimeException(e);}SysWsManager.getInstance().sendMessage(jsonObject.toString());监听消息
SysWsManager.getInstance().addMessageListener(message->{if(message==null||"pong".equals(message)||"欢迎连接 WebSocket!".equals(message)){return;}try{Gsongson=newGson();JsonObjectjson=gson.fromJson(message,JsonObject.class);Stringtype=json.get("type").getAsString();switch(type){case"receive_new_friend_request"->handleReceiveNewFriendRequest(json);case"after_agree"->handleAfterAgree(json);default->;}}catch(JsonParseExceptione){Log.e(TAG,"JSON 解析失败,原始消息: "+message,e);}catch(Exceptione){Log.e(TAG,"处理消息时出错: "+e.getMessage(),e);}});总结
通过以上实现,我们的全局单例WebSocket管理器具有以下优势
| 特性 | 解决问题 | 实现方式 |
|---|---|---|
| 全局唯一连接 | 连接数失控、资源浪费 | 饿汉式单例模式 |
| 自动重连机制 | 网络波动导致连接中断 | 指数退避算法,最大间隔60秒 |
| 心跳保活 | 长连接被中间路由断开 | 定时发送ping,检测pong响应 |
| 状态同步 | 多页面连接状态不一致 | AtomicBoolean原子变量 |
| 线程安全 | 多线程并发访问问题 | ScheduledExecutorService + 原子类 |
| 解耦设计 | 页面与WebSocket强耦合 | 观察者模式监听器 |
注意事项
Android 9.0及以上需要添加网络明文配置
<!-- AndroidManifest.xml --><applicationandroid:usesCleartextTraffic="true">Service保活:如需进程保活,可将WebSocket放在前台Service中运行
内存泄漏:页面销毁时记得移除监听器
消息幂等性:后端应保证推送消息可重复消费,防止重复处理
通过本文的全局单例封装,你的Android应用可以拥有一个稳定、高效、易于维护的WebSocket长连接服务。有任何问题欢迎在评论区交流讨论~