今年在公司第一个需求就是基于websocket写一个客户端消息中心,现在已经上线很久了在司机这种网络环境平均一天重连8次,自认为还是不错的.当时写的时候那个心酸啊,主要因为第一次写都不知道该从哪下手,没有方向.所以这里我将尽可能详细的跟大家分享出来.
本篇内容会比较多,先来段舞蹈热身下.
我准备按如下顺序来讲解
-
整体流程的一个概括了解大体思路.
-
把大体流程细化,逐步去实现.
前言
这里特别说明下因为WebSocket服务端是公司线上项目所以这里url和具体协议我全部抹去了,但我会尽力给大家讲明白并且demo我都是测试过,还望各位看官见谅
我们先粗犷的讲下流程,掌握个大概的方向,然后在深入讲解细节的实现.这里先解答一个疑惑,为啥我们这要用WebSocket而不是Socket呢,因为WebSocket是一个应用层协议很多东西都规定好了我们直接按他的规定来用就好,而Socket是传输层和应用层的一个抽象层很多东西我们还得自己规定相对来说会比较麻烦,所以这里我们用的WebSocket.
既然WebSocket是一个应用层协议,我们肯定不可能自己去实现,所以第一步是需要找一个实现了该协议的框架,这里我用的nv-websocket-client,api我就不介绍了,库中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请求
上面只是一个概括,对于心跳,重连,发送请求这里有不少细节需要注意的下一节我们将详细讲解
具体实现
理论说完了,接下来我们将一步步实现客户端代码.首先我们添加依赖
compile 'com.neovisionaries:nv-websocket-client:2.2'
然后创建一个单利的WsManager管理websocket供全局调用,
public 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;
}
}
建立连接
然后添加建立连接代码,这里关于WebSocket协议的操作用的都是nv-websocket-client,我也加上了详细的注释,实在不理解可以去读一遍readme文件.
public class WsManager {
private static WsManager mInstance;
private final String TAG = this.getClass().getSimpleName();
/**
* WebSocket config
*/
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(websoc