本文中我们将讲解一下App的长连接实现。一般而言长连接已经是App的标配了,推送功能的实现基础就是长连接,当然了我们也可以通过轮训操作实现推送功能,但是轮训一般及时性比较差,而且网络消耗与电量销毁比较多,因此一般推送功能都是通过长连接实现的。

那么如何实现长连接呢?现在一般有这么几种实现方式:

  • 使用第三方的长连接服务;
  • 通过NIO等方案实现长连接服务;
  • 通过MINA等第三方框架实现长连接;

几种长连接服务的具体实现,以及各自的优缺点。

1. 使用第三方的长连接服务

介绍:这是最简单的方式,我们可以通过接入极光推送,百度推送,友盟等第三方服务实现长连接,通过接入第三方的API我们可以很方便的接入第三方的长连接,推送服务,但是这种方式定制化程度不太好,如果对长连接服务不是要求特别高,对定制化要求不是很高的话基本可以考虑这种方式(目前主流的App都是使用第三方的长连接服务) 
优势:简单,方便 
劣势:定制化程度不高

2. 使用NIO等方案实现长连接服务

介绍:通过NIO的方式实现长连接,这种方式对技术要求程度比较高,基本都是通过java API实现长连接,实现心跳包,实现异常情况的容错等操作,可以说通过NIO实现长连接对技术要求很高,一般如果没有成行的技术方案比建议这么做,就算实现了长连接,后期连接的维护,对电量,流量的损耗等都需要持续的优化。 
优势:定制化比较高 
劣势:技术要求高,需要持续的维护

3. 使用MINA等第三方框架实现长连接

介绍:MINA是一个第三方的NIO框架,该框架实现了一整套的长连接机制,包括长连接的建立,心跳包的实现,异常机制的容错等。使用MINA实现长连接可以定制化的实现一些特有的功能,并且比NIO方案较为简单,因为其已经封装了一些长连接的特有机制,比如心跳包,容错等。 
优势:可定制,较NIO方法简单 
劣势:也需要一定的技术储备

长连接具体实现

在我们的Android客户端中长连接的实现机制采用–MINA方式。这里多说一句,一开始的长连接采用的是NIO方案,但是采用这种方案之后踩了很多坑,包括心跳,容错等机制都是自己写的,所以耗费了大量的时间,而且对手机电量的消耗很大,最后决定使用MINA NIO框架重新实现一遍长连接,后来经过实测,长连接的稳定性还有耗电量,流量的消耗等指标方面有了很大的提高。

下面我将简单的介绍一下通过NIO实现长连接的具体流程:

  • 引入MINA jar包,在App启动页面,登录页面启动长连接;
  • 创建后台服务,在服务中创建MINA长连接;
  • 实现心跳包,重写一些容错机制;
  • 实现长连接断了之后的重连机制,并且重连次数有限制不能一直重连;
  • 长连接断了之后实现轮训操作,这里的轮训服务只有在长连接断了之后才启动,在长连接恢复之后关闭;

以下就是在长连接中实现的具体代码:

  • 在Application的onCreate方法中检测App是否登录,若登录的话启动长连接
1 /**
 2  * 在Application的onCreate方法中执行启动长连接的操作
 3  **/
 4 @Override
 5     public void onCreate() {
 6         ...
 7         // 登录后开启长连接
 8         if (UserConfig.isPassLogined()) {
 9             L.i("用户已登录,开启长连接...");
10             startLongConn();
11         }
12         ...
13     }
  • 通过闹钟服务实现具体的启动长连接service的操作,即每隔60秒钟判断长连接是否启动,若未启动则实现启动操作 
1     /**
 2      * 开始执行启动长连接服务
 3      */
 4     public void startLongConn() {
 5         quitLongConn();
 6         L.i("长连接服务已开启");
 7         AlarmManager manager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
 8         Intent intent = new Intent(this, LongConnService.class);
 9         intent.setAction(LongConnService.ACTION);
10         PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
11         long triggerAtTime = SystemClock.elapsedRealtime();
12         manager.setRepeating(AlarmManager.RTC_WAKEUP, triggerAtTime, 60 * 1000, pendingIntent);
13     }
  • 下面的代码就是长连接服务的具体实现
1 /**
 2  * 后台长连接服务
 3  **/
 4 public class LongConnService extends Service {
 5     public static String ACTION = "com.youyou.uuelectric.renter.Service.LongConnService";
 6     private static MinaLongConnectManager minaLongConnectManager;
 7     public String tag = "LongConnService";
 8     private Context context;
 9 
10     @Override
11     public int onStartCommand(Intent intent, int flags, int startId) {
12         context = getApplicationContext();
13         // 执行启动长连接的操作
14         startLongConnect();
15         ObserverManager.addObserver("LongConnService", stopListener);
16         return START_STICKY;
17     }
18 
19     public ObserverListener stopListener = new ObserverListener() {
20         @Override
21         public void observer(String from, Object obj) {
22             closeConnect();
23         }
24     };
25 
26     @Override
27     public void onDestroy() {
28         super.onDestroy();
29         closeConnect();
30     }
31 
32     /**
33      * 开始执行启动长连接的操作
34      */
35     private void startLongConnect() {
36         if (Config.isNetworkConnected(context)) {
37             if (minaLongConnectManager != null && minaLongConnectManager.checkConnectStatus()) {
38                 L.i("长连接状态正常...");
39                 return;
40             }
41             if (minaLongConnectManager == null) {
42                 startThreadCreateConnect();
43             } else {
44                 if (minaLongConnectManager.connectIsNull() && minaLongConnectManager.isNeedRestart()) {
45                     L.i("session已关闭,需要重新创建一个session");
46                     minaLongConnectManager.startConnect();
47                 } else {
48                     L.i("长连接已关闭,需要重开一个线程来重新创建长连接");
49                     startThreadCreateConnect();
50                 }
51             }
52         }
53 
54     }
55 
56     private final AtomicInteger mCount = new AtomicInteger(1);
57 
58     private void startThreadCreateConnect() {
59         if (UserConfig.getUserInfo().getB3Key() != null && UserConfig.getUserInfo().getSessionKey() != null) {
60             System.gc();
61 
62             new Thread(new Runnable() {
63                 @Override
64                 public void run() {
65                     // 执行具体启动长连接操作
66                     minaLongConnectManager = MinaLongConnectManager.getInstance(context);
67                     minaLongConnectManager.crateLongConnect();
68                 }
69             }, "longConnectThread" + mCount.getAndIncrement()).start();
70         }
71     }
72 
73 
74     private void closeConnect() {
75 
76         if (minaLongConnectManager != null) {
77             minaLongConnectManager.closeConnect();
78         }
79         minaLongConnectManager = null;
80 
81         // 停止长连接服务LongConnService
82         stopSelf();
83     }
84 
85     @Override
86     public IBinder onBind(Intent intent) {
87         throw new UnsupportedOperationException("Not yet implemented");
88     }
89 }
  • 而下面的代码就是长连接的具体实现操作,具体的代码有相关注释说明
1 /**
  2  * 具体实现长连接的管理对象
  3  **/
  4 public class MinaLongConnectManager {
  5 
  6     private static final String TAG = MinaLongConnectManager.class.getSimpleName();
  7     /**
  8      * 服务器端口号
  9      */
 10     public static final int DEFAULT_PORT = 18156;
 11     /**
 12      * 连接超时时间,30 seconds
 13      */
 14     public static final long SOCKET_CONNECT_TIMEOUT = 30 * 1000L;
 15 
 16     /**
 17      * 长连接心跳包发送频率,60s
 18      */
 19     public static final int KEEP_ALIVE_TIME_INTERVAL = 60;
 20     private static Context context;
 21     private static MinaLongConnectManager minaLongConnectManager;
 22 
 23     private static NioSocketConnector connector;
 24     private static ConnectFuture connectFuture;
 25     public static IoSession session;
 26     private static ExecutorService executorService = Executors.newSingleThreadExecutor();
 27 
 28     /**
 29      * 长连接是否正在连接中...
 30      */
 31     private static boolean isConnecting = false;
 32 
 33     private MinaLongConnectManager() {
 34         EventBus.getDefault().register(this);
 35     }
 36 
 37     public static synchronized MinaLongConnectManager getInstance(Context ctx) {
 38 
 39         if (minaLongConnectManager == null) {
 40             context = ctx;
 41             minaLongConnectManager = new MinaLongConnectManager();
 42         }
 43         return minaLongConnectManager;
 44     }
 45 
 46     /**
 47      * 检查长连接的各种对象状态是否正常,正常情况下无需再创建
 48      *
 49      * @return
 50      */
 51     public boolean checkConnectStatus() {
 52         if (connector != null && connector.isActive() && connectFuture != null && connectFuture.isConnected() && session != null && session.isConnected()) {
 53             return true;
 54         } else {
 55             return false;
 56         }
 57     }
 58 
 59     public boolean connectIsNull() {
 60         return connector != null;
 61     }
 62 
 63     /**
 64      * 创建长连接,配置过滤器链和心跳工厂
 65      */
 66     public synchronized void crateLongConnect() {
 67         // 如果是长连接正在创建中
 68         if (isConnecting) {
 69             L.i("长连接正在创建中...");
 70             return;
 71         }
 72         if (!Config.isNetworkConnected(context)) {
 73             L.i("检测到网络未打开,无法正常启动长连接,直接return...");
 74             return;
 75         }
 76         // 检查长连接的各种对象状态是否正常,正常情况下无需再创建
 77         if (checkConnectStatus()) {
 78             return;
 79         }
 80         isConnecting = true;
 81         try {
 82             connector = new NioSocketConnector();
 83             connector.setConnectTimeoutMillis(SOCKET_CONNECT_TIMEOUT);
 84 
 85             if (L.isDebug) {
 86                 if (!connector.getFilterChain().contains("logger")) {
 87                     // 设置日志输出工厂
 88                     connector.getFilterChain().addLast("logger", new LoggingFilter());
 89                 }
 90             }
 91             if (!connector.getFilterChain().contains("codec")) {
 92                 // 设置请求和响应对象的编解码操作
 93                 connector.getFilterChain().addLast("codec", new ProtocolCodecFilter(new LongConnectProtocolFactory()));
 94             }
 95             // 创建心跳工厂
 96             ClientKeepAliveMessageFactory heartBeatFactory = new ClientKeepAliveMessageFactory();
 97             // 当读操作空闲时发送心跳
 98             KeepAliveFilter heartBeat = new KeepAliveFilter(heartBeatFactory, IdleStatus.READER_IDLE);
 99             // 设置是否将事件继续往下传递
100             heartBeat.setForwardEvent(true);
101             // 设置心跳包请求后超时无反馈情况下的处理机制,默认为关闭连接,在此处设置为输出日志提醒
102             heartBeat.setRequestTimeoutHandler(KeepAliveRequestTimeoutHandler.LOG);
103             //设置心跳频率
104             heartBeat.setRequestInterval(KEEP_ALIVE_TIME_INTERVAL);
105             if (!connector.getFilterChain().contains("keepAlive")) {
106                 connector.getFilterChain().addLast("keepAlive", heartBeat);
107             }
108             if (!connector.getFilterChain().contains("reconnect")) {
109                 // 设置长连接重连过滤器,当检测到Session(会话)断开后,重连长连接
110                 connector.getFilterChain().addLast("reconnect", new LongConnectReconnectionFilter());
111             }
112             // 设置接收和发送缓冲区大小
113             connector.getSessionConfig().setReceiveBufferSize(1024);
114             connector.getSessionConfig().setSendBufferSize(1024);
115             // 设置读取空闲时间:单位为s
116             connector.getSessionConfig().setReaderIdleTime(60);
117 
118             // 设置长连接业务逻辑处理类Handler
119             LongConnectHandler longConnectHandler = new LongConnectHandler(this, context);
120             connector.setHandler(longConnectHandler);
121 
122         } catch (Exception e) {
123             e.printStackTrace();
124             closeConnect();
125         }
126 
127         startConnect();
128     }
129 
130     /**
131      * 开始或重连长连接
132      */
133     public synchronized void startConnect() {
134         if (connector != null) {
135             L.i("开始创建长连接...");
136             boolean isSuccess = beginConnect();
137             // 创建成功后,修改创建中状态
138             if (isSuccess) {
139                 isNeedRestart = false;
140                 if (context != null) {
141                     // 长连接启动成功后,主动拉取一次消息
142                     LoopRequest.getInstance(context).sendLoopRequest();
143                 }
144             } else {
145                 // 启动轮询服务
146                 startLoopService();
147             }
148             isConnecting = false;
149 //            printProcessorExecutor();
150         } else {
151             L.i("connector已为null,不能执行创建连接动作...");
152         }
153     }
154 
155     /**
156      * 检测MINA中线程池的活动状态
157      */
158     private void printProcessorExecutor() {
159         Class connectorClass = connector.getClass().getSuperclass();
160         try {
161             L.i("connectorClass:" + connectorClass.getCanonicalName());
162             Field field = connectorClass.getDeclaredField("processor");
163             field.setAccessible(true);
164             Object connectorObject = field.get(connector);
165             if (connectorObject != null) {
166                 SimpleIoProcessorPool processorPool = (SimpleIoProcessorPool) connectorObject;
167                 Class processPoolClass = processorPool.getClass();
168                 Field executorField = processPoolClass.getDeclaredField("executor");
169                 executorField.setAccessible(true);
170                 Object executorObject = executorField.get(processorPool);
171                 if (executorObject != null) {
172                     ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executorObject;
173                     L.i("线程池中当前线程数:" + threadPoolExecutor.getPoolSize() + "\t 核心线程数:" + threadPoolExecutor.getCorePoolSize() + "\t 最大线程数:" + threadPoolExecutor.getMaximumPoolSize());
174                 }
175 
176             } else {
177                 L.i("connectorObject = null");
178             }
179         } catch (Exception e) {
180             e.printStackTrace();
181         }
182     }
183 
184 
185     /**
186      * 开始创建Session
187      *
188      * @return
189      */
190     public boolean beginConnect() {
191 
192         if (session != null) {
193             session.close(false);
194             session = null;
195         }
196         if (connectFuture != null && connectFuture.isConnected()) {
197             connectFuture.cancel();
198             connectFuture = null;
199         }
200         FutureTask<Boolean> futureTask = new FutureTask<>(new Callable<Boolean>() {
201             @Override
202             public Boolean call() {
203                 try {
204                     InetSocketAddress address = new InetSocketAddress(NetworkTask.getBASEURL(), DEFAULT_PORT);
205                     connectFuture = connector.connect(address);
206                     connectFuture.awaitUninterruptibly(3000L);
207                     session = connectFuture.getSession();
208                     if (session == null) {
209                         L.i(TAG + "连接创建失败...当前环境:" + NetworkTask.getBASEURL());
210                         return false;
211                     } else {
212                         L.i(TAG + "长连接已启动,连接已成功...当前环境:" + NetworkTask.getBASEURL());
213                         return true;
214                     }
215                 } catch (Exception e) {
216                     return false;
217                 }
218             }
219         });
220 
221         executorService.submit(futureTask);
222         try {
223             return futureTask.get();
224         } catch (Exception e) {
225             return false;
226         }
227 
228     }
229 
230     /**
231      * 关闭连接,根据传入的参数设置session是否需要重新连接
232      */
233     public synchronized void closeConnect() {
234         if (session != null) {
235             session.close(false);
236             session = null;
237         }
238         if (connectFuture != null && connectFuture.isConnected()) {
239             connectFuture.cancel();
240             connectFuture = null;
241         }
242         if (connector != null && !connector.isDisposed()) {
243             // 清空里面注册的所以过滤器
244             connector.getFilterChain().clear();
245             connector.dispose();
246             connector = null;
247         }
248         isConnecting = false;
249         L.i("长连接已关闭...");
250     }
251 
252     private volatile boolean isNeedRestart = false;
253 
254     public boolean isNeedRestart() {
255         return isNeedRestart;
256     }
257 
258     public void onEventMainThread(BaseEvent event) {
259         if (event == null || TextUtils.isEmpty(event.getType()))
260             return;
261         if (EventBusConstant.EVENT_TYPE_NETWORK_STATUS.equals(event.getType())) {
262             String status = (String) event.getExtraData();
263             // 当网络状态变化的时候请求startQuery接口
264             if (status != null && status.equals("open")) {
265                 if (isNeedRestart && UserConfig.getUserInfo().getB3Key() != null && UserConfig.getUserInfo().getSessionKey() != null) {
266                     L.i("检测到网络已打开且长连接处于关闭状态,需要启动长连接...");
267                     Intent intent = new Intent(context, LongConnService.class);
268                     intent.setAction(LongConnService.ACTION);
269                     context.startService(intent);
270                 }
271             }
272         }
273     }
274 
275     /**
276      * 出现异常、session关闭后,接收事件进行长连接重连操作
277      */
278     public void onEventMainThread(LongConnectMessageEvent event) {
279 
280         if (event.getType() == LongConnectMessageEvent.TYPE_RESTART) {
281 
282             long currentTime = System.currentTimeMillis();
283 
284             // 票据有效的情况下进行重连长连接操作
285             if (UserConfig.getUserInfo().getB3Key() != null && UserConfig.getUserInfo().getSessionKey() != null
286                     && ((currentTime / 1000) < UserConfig.getUserInfo().getUnvalidSecs())) {
287                 // 等待2s后重新创建长连接
288                 SystemClock.sleep(1000);
289                 if (Config.isNetworkConnected(context)) {
290                     L.i("出现异常情况,需要自动重连长连接...");
291                     startConnect();
292                 } else {
293                     isNeedRestart = true;
294                     L.i("长连接出现异常,需要重新创建session会话...");
295                 }
296             }
297         } else if (event.getType() == LongConnectMessageEvent.TYPE_CLOSE) {
298             L.i("收到session多次close的消息,此时需要关闭长连接,等待下次闹钟服务来启动...");
299             closeConnect();
300         }
301     }
302 
303 
304     /**
305      * 启动轮询服务
306      */
307     public void startLoopService() {
308         // 启动轮询服务
309         // 暂时不考虑加入网络情况的判断...
310         if (!LoopService.isServiceRuning) {
311             // 用户是登录态,启动轮询服务
312             if (UserConfig.isPassLogined()) {
313                 // 判断当前长连接的状态,若长连接已连接,则不再开启轮询服务
314                 if (MinaLongConnectManager.session != null && MinaLongConnectManager.session.isConnected()) {
315                     LoopService.quitLoopService(context);
316                     return;
317                 }
318                 LoopService.startLoopService(context);
319             } else {
320                 LoopService.quitLoopService(context);
321             }
322         }
323     }
324 
325 }

以上是通过NIO实现App长连接的部分核心代码,NIO中其实已经实现了长连接的核心流程,我们需要做的就是按照其流程实现长连接,需要注意的是要处理好异常情况,重连机制等。

当长连接创建成功之后需要重新拉取一次服务器端的长连接消息,并且这里的长连接做了容错处理,当长连接断了之后需要有重连机制,一直启动轮训服务,当长连接修复之后轮训服务退出。以上只是通过MINA框架实现的长连接操作的核心流程,还有一些长连接实现的操作细节这里就不做过多的说明。

总结: 
基本上对于App来说长连接已经是标配了,产品开发人员可以根据具体的产品需求选择不同的实现方式,一般而言使用第三方的推送服务已经可以满足大部分的需求了,当然了若是相对技术有所追求的话也可以选择自己实现一套长连接服务,不过其中可能存在一些坑需要填,希望这里的长连接实现能够对大家对长连接实现上有所帮助。

    • 可以通过使用第三方长连接服务或者是自己实现连接的方式;

    • 自定义实现长连接可以通过使用NIO或者是第三方NIO框架,比如MINA实现;

    • 长连接实现中通过心跳包的机制实现App与服务器的长时间连接;

    • 可以通过闹钟的机制定时检测长连接服务是否可靠,长连接是否出现异常等;

    • 为了消息的及时性,在长连接出现异常情况时可通过创建轮训服务的机制实现对消息的获取,待长连接恢复之后关闭轮训服务;