WebSocket安卓客户端实现详解(⼀)--连接建⽴与重连
今年在公司第⼀个需求就是基于websocket写⼀个客户端消息中⼼,现在已经上线很久了在司机这种⽹络环境平均⼀天重连8次,⾃认为还是不错的.当时写的时候那个⼼酸啊,主要因为第⼀次写都不知道该从哪下⼿,没有⽅向.所以这⾥我将尽可能详细的跟⼤家分享出来.
本篇内容会⽐较多,先来段舞蹈热⾝下.
我准备按如下顺序来讲解
1. 整体流程的⼀个概括了解⼤体思路.
2. 把⼤体流程细化,逐步去实现.
前⾔
这⾥特别说明下因为WebSocket服务端是公司线上项⽬所以这⾥url和具体协议我全部抹去了,但我会尽⼒给⼤家讲明⽩并且demo我都是测试过,还望各位看官见谅
我们先粗犷的讲下流程,掌握个⼤概的⽅向,然后在深⼊讲解细节的实现.这⾥先解答⼀个疑惑,为啥我们这要⽤WebSocket⽽不是呢,因为WebSocket是⼀个应⽤层协议很多东西都规定好了我们直接按他的规定来⽤就好,⽽Socket是传输层和应⽤层的⼀个抽象层很多东西我们还得⾃⼰规定相对来说会⽐较⿇烦,所以这⾥我们⽤的WebSocket.
既然WebSocket是⼀个应⽤层协议,我们肯定不可能⾃⼰去实现,所以第⼀步是需要⼀个实现了该协议的框架,这⾥我⽤的,我就不介绍了,库中readme已经详细的介绍了,后⾯我就直接使⽤了.
关于通讯协议为了⽅便,这⾥我们使⽤的是json.
接下来我们先简单描述下我们将要做的事情
⽤户登录流程
第⼀步⽤户输⼊账号密码登录成功后,我们将会通过websocket协议建⽴连接,当连接失败回调的时候
我们尝试重连,直到连接成功,当然这个尝试重连的时间间隔我是根据重连失败次数按⼀定规则写的具体后⾯再说.
第⼆步当连接建⽴成功后,我们需要在后台通过长连接发送请求验证该⽤户的⾝份也就是上图的授权,既然前⾯⽤户登录都成功了⼀般情况下授权是不会失败的,所以这⾥对于授权失败并未处理,授权成功后我们开启⼼跳,并且发送同步数据请求到服务端获取还未收到的消息.
客户端发送请求流程
第⼀步将请求参数封装成请求对象,然后添加超时任务并且将该请求的回调添加到回调集合.
这⾥有点需要说明下,封装请求参数的时候这⾥额外添加了两个参数seqId和reqCount,这⾥我们是通过长连接请求当服务端响应的时候为了能够到对应的回调,所以每个请求我们都需要传给服务端⼀个唯⼀标识来标识该请求,这⾥我⽤的seqId,请求成功后服务端再把seqId回传,我们再通过这个seqId作为key从回调集合中到对应的回调.⽽reqCount的话主要针对请求超时的情况,如果请求超时,第⼆次请求的时候就把reqCount++在放⼊request中,我们约定同⼀个请求次数⼤于三次时候⾛http补偿通道,那么当request中的reqCount>3的时候我们就通过http发送该请求,然后根据响应回调对应结果.
第⼆步开始请求,成功或者失败的话通过seqId到对应回调执⾏并从回调集合中移除该回调,然后取消
超时任务.如果超时的话根据seqId拿到对应的回调并从回调集合中移除该回调,然后判断请求次数如果⼩于等于3次再次通过websocket尝试请求,如果⼤于3次通过http请求,根据请求成功失败情况执⾏对应回调.服务端主动推送消息流程
先说明下这⾥服务端推送的消息仅仅是个事件,不携带具体消息.
第⼀步根据notify中事件类型到对应的处理类,⼀般情况下这⾥需要同步对应数据.
第⼆步然后⽤eventbus通知对应的ui界⾯更新
第三步如果需要ack,发送ack请求
上⾯只是⼀个概括,对于⼼跳,重连,发送请求这⾥有不少细节需要注意的下⼀节我们将详细讲解
具体实现
理论说完了,接下来我们将⼀步步实现客户端代码.⾸先我们添加依赖
然后创建⼀个单利的WsManager管理websocket供全局调⽤,
建⽴连接
然后添加建⽴连接代码,这⾥关于WebSocket协议的操作⽤的都是,我也加上了详细的注释,实在不理解可以去读⼀遍readme⽂件.    compile 'visionaries:nv-websocket-client:2.2'
1public  class  WsManager {    private  static  WsManager mInstance ;    private  WsManager () {    }    public  static  WsManager getInstance (){        if (mInstance == null ){            synchronized  (WsManager .class ){                if (mInstance == null ){                    mInstance = new  WsManager ();                }            }        }        return  mInstance ;    }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public  class  WsManager {    private  static  WsManager mInstance ;    private  final  String TAG = this .getClass ().getSimpleName ();    /**    * WebSocket config      */
1
2
3
4
5
6
7
*/    private  static  final  int  FRAME_QUEUE_SIZE = 5;    private  static  final  int  CONNECT_TIMEOUT = 5000;    private  static  final  String DEF_TEST_URL = "测试服地址";//测试服默认地址    private  static  final  String DEF_RELEASE_URL = "正式服地址";//正式服默认地址    private  static  final  String DEF_URL = BuildConfig .DEBUG ? DEF_TEST_URL : DEF_RELEASE_URL ;    private  String url ;    private  WsStatus mStatus ;    private  WebSocket ws ;    private  WsListener mListener ;    private  WsManager () {    }    public  static  WsManager getInstance
(){        if (mInstance == null ){            synchronized  (WsManager .class ){                if (mInstance == null ){                    mInstance = new  WsManager ();                }            }        }        return  mInstance ;    }    public  void  init (){        try  {          /**          * configUrl 其实是缓存在本地的连接地址          * 这个缓存本地连接地址是app 启动的时候通过http 请求去服务端获取的,          * 每次app 启动的时候会拿当前时间与缓存时间⽐较,超过6⼩时就再次去服务端获取新的连接地址更新本地缓存          */            String configUrl = "";            url = TextUtils .isEmpty (configUrl ) ? DEF_URL : configUrl ;            ws = new  WebSocketFactory ().createSocket (url , CONNECT_TIMEOUT )                .setFrameQueueSize (FRAME_QUEUE_SIZE )//设置帧队列最⼤值为5                .setMissingCloseFrameAllowed (false )//设置不允许服务端关闭连接却未发送关闭帧                .addListener (mListener = new  WsListener ())//添加回调监听                .connectAsynchronously ();//异步连接            setStatus (WsStatus .CONNECTING );            Logger .t (TAG ).d ("第⼀次连接");        } catch  (IOException e ) {            e .printStackTrace ();        }    }    /**    * 继承默认的监听空实现WebSocketAdapter,重写我们需要的⽅法    * onTextMessage 收到⽂字信息    * onConnected 连接成功    * onConnectError 连接失败    * onDisconnected 连接关闭    */    class  WsListener extends  WebSocketAdapter {        @Override        public  void  onTextMessage (WebSocket websocket , String text ) throws  Exception {            super .onTextMessage (websocket , text );            Logger .t (TAG ).d (text );        }        @Override        public  void  onConnected (WebSocket websocket , Map <String , List <String >> headers )            throws  Exception {            super .onConnected (websocket , headers );
Logger .t (TAG ).d ("连接成功");
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
从注释我们可以知道,这⾥我们是app启动的时候通过http请求获取WebSocket连接地址,如果获取失败就⾛本地默认的url建⽴连接.并且内部⾃⼰维护了⼀个websocket状态后⾯发送请求和重连的时候会⽤上.
其实获取连接地址这个地⽅是可以优化的,就是app启动的时候先⽐较上次获取的时间如果⼤于6⼩时就通过http请求获取websocket的连接地址,这个地址应该是个列表,然后存⼊本地,连接的时候我们可以先ping下地址,选择耗时最短的地址接⼊.如果连不上我们在连耗时第⼆短的地址以此类推.但这⾥我们就以简单的⽅式做了.⾄于建⽴连接代码在哪调⽤的话,我选择的是主界⾯onCreate()的时候,因为⼀般能进⼊主界⾯了,就代表⽤户已经登录成功.断开连接的话在主界⾯onDestroy()的时候调⽤
重连
建⽴连接有成功就有失败,对于失败情况我们需要重连,那么下⾯我们分别说明重连的时机,重连的策略和当前是否应该重连的判断.            Logger .t (TAG ).d ("连接成功");            setStatus (WsStatus .CONNECT_SUCCESS );        }        @Override        public  void  onConnectError (WebSocket websocket , WebSocketException exception )            throws  Exception {            super .onConnectError (websocket , exception );            Logger .t (TAG ).d ("连接错误");            setStatus (WsStatus .CONNECT_FAIL );        }        @Override        public  void  onDisconnected (WebSocket we
bsocket , WebSocketFrame serverCloseFrame , WebSocketFrame clientCloseFrame , boolean  closedByServ            throws  Exception {            super .onDisconnected (websocket , serverCloseFrame , clientCloseFrame , closedByServer );            Logger .t (TAG ).d ("断开连接");            setStatus (WsStatus .CONNECT_FAIL );        }    }    private  void  setStatus (WsStatus status ){        this .mStatus = status ;    }    private  WsStatus getStatus (){        return  mStatus ;    }    public  void  disconnect (){        if (ws != null )        ws .disconnect ();    }}72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
public  enum  WsStatus {    CONNECT_SUCCESS ,//连接成功    CONNECT_FAIL ,//连接失败    CONNECTING ;//正在连接}
1
2
3
4
5WsManager .getInstance ().init ();
1WsManager .getInstance ().disconnect ();
1
对于重连的时机有如下⼏种情况我们需要尝试重连
1. 应⽤⽹络的切换.具体点就是可⽤⽹络状态的切换,⽐如4g切wifi连接会断开我们需要重连.
2. 应⽤回到前台的时候,判断如果连接断开我们需要重连,这个是尽量保持当应⽤再前台的时候连接的稳定.
3. 收到连接失败或者连接断开事件的时候,这个没什么好解释.
4. ⼼跳连续3次失败时候.当然这个连续失败3次是⾃⼰定义的,⼤伙可以根据⾃⼰app的情况定制.
等会我们先展⽰前三种情况,⼼跳失败这个在后⾯我们把客户端发送请求讲完再说.websocket和socket
上⾯把需要重连的情景说了,现在讲讲具体的重连策略.
这⾥我定义了⼀个最⼩重连时间间隔min和⼀个最⼤重连时间间隔max,当重连次数⼩于等于3次的时候都以最⼩重连时间间隔min去尝试重连,当重连次数⼤于3次的时候我们将重连地址替换成默认地址DEF_URL,将重连时间间隔按min*(重连次数-2)递增最⼤不不超过max.还有最后⼀个当前是否应该重连的判断
1. ⽤户是否登录,可以通过本地是否有缓存的⽤户信息来判断.因为重连成功后我们需要将⽤户信息通过WebSocket发送到服务器进⾏⾝
份验证所以这⾥必须登录成功.
2. 当前连接是否可⽤,这个通过库中的api判断ws.isOpen().
3. 当前不是正在连接状态,这⾥我们根据⾃⼰维护的状态来判断getStatus() != WsStatus.CONNECTING .
4. 当前⽹络可⽤.
下⾯我们show code.跟之前相同的代码这⾥就省略了public  class  WsManager {    .....省略部分跟之前代码⼀样.....    /**    * 继承默认的监听空实现WebSocketAdapter,重写我们需要的⽅法    * onTextMessage 收到⽂字信息    * onConnected 连接成功    * onConnectError 连接失败    * onDisconn
ected 连接关闭    */    class  WsListener extends  WebSocketAdapter {        @Override        public  void  onTextMessage (WebSocket websocket , String text ) throws  Exception {            super .onTextMessage (websocket , text );            Logger .t (TAG ).d (text );        }        @Override        public  void  onConnected (WebSocket websocket , Map <String , List <String >> headers )            throws  Exception {            super .onConnected (websocket , headers );            Logger .t (TAG ).d ("连接成功");            setStatus (WsStatus .CONNECT_SUCCESS );            cancelReconnect ();//连接成功的时候取消重连,初始化连接次数        }        @Override        public  void  onConnectError (WebSocket websocket , WebSocketException exception )            throws  Exception {            super .onConnectError (websocket , exception );
Logger .t (TAG ).d ("连接错误");1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。