热门标签 | HotTags
当前位置:  开发笔记 > 运维 > 正文

在Android中使用WebSocket实现消息通信的方法详解

前言 消息推送功能可以说移动APP不可缺少的功能之一,一般简单的推送我们可以使用第三方推送的SDK,比如极光推送、信鸽推送等,但是对于

前言

消息推送功能可以说移动APP不可缺少的功能之一,一般简单的推送我们可以使用第三方推送的SDK,比如极光推送、信鸽推送等,但是对于消息聊天这种及时性有要求的或者三方推送不满足业务需求的,我们就需要使用WebSocket实现消息推送功能。

基本流程

WebSocket是什么,这里就不做介绍了,我们这里使用的开源框架是https://github.com/TakahikoKawasaki/nv-websocket-client

基于开源协议我们封装实现WebSocket的连接、注册、心跳、消息分发、超时任务功能,基本流程如下:

连接功能

首先我们新建一个项目,在build.grade中添加配置

compile 'com.neovisionaries:nv-websocket-client:2.2'

新建websocket管理类WsManger

public class WsManager {
 
 private volatile static WsManager wsManger;
 
 private WsManager() {
 }
 
 public static WsManager getWsManger() {
 if (wsManger == null) {
 synchronized (WsManager.class) {
 if (wsManger == null) {
  wsManger = new WsManager();
 }
 }
 }
 return wsManger;
 }
 
 
}

接下来添加连接方法,我们将webSocket的状态分为三种,新建WsStatue枚举类对应起来

public enum WsStatus {
 
 /**
 * 连接成功
 */
 CONNECT_SUCCESS,
 /**
 * 连接失败
 */
 CONNECT_FAIL,
 /**
 * 正在连接
 */
 CONNECTING;
}

连接方法如下所示:

/**
 * 连接方法 这里要判断是否登录 此处省略
 */
public void connect() {
 //WEB_SOCKET_API 是连接的url地址,
 // CONNECT_TIMEOUT是连接的超时时间 这里是 5秒
 try {
 ws = new WebSocketFactory().createSocket(WEB_SOCKET_API, CONNECT_TIMEOUT)
 //设置帧队列最大值为5
 .setFrameQueueSize(5)
 //设置不允许服务端关闭连接却未发送关闭帧
 .setMissingCloseFrameAllowed(false)
 //添加回调监听
 .addListener(new WsListener())
 //异步连接
 .connectAsynchronously();
 } catch (IOException e) {
 e.printStackTrace();
 }
 setStatus(WsStatus.CONNECTING);
}

调用连接方法后 我们来看连接的回调 也就是WsListener

/**
 * websocket回调事件
 */
private class WsListener extends WebSocketAdapter {
 
 
 @Override
 public void onConnected(WebSocket websocket, Map> headers) throws Exception {
 Log.d(TAG, "onConnected: 连接成功");
 }
 
 @Override
 public void onConnectError(WebSocket websocket, WebSocketException exception) throws Exception {
 Log.d(TAG, "onConnectError: 连接失败");
 }
 
 @Override
 public void onDisconnected(WebSocket websocket, WebSocketFrame serverCloseFrame,
  WebSocketFrame clientCloseFrame,
  boolean closedByServer) throws Exception {
 Log.d(TAG, "onDisconnected: 断开连接");
 
 }
 
 @Override
 public void onTextMessage(WebSocket websocket, String text) throws Exception {
 Log.d(TAG, "onTextMessage: 收到消息:" + text);
 }
}

下面我们调用连接方法

WsManager.getWsManger().connect();

运行项目我们可以看到如下打印:

 

此处我们要做的处理是,如果收到连接失败或者断开连接的回调 需要重新连接,我们重新调用一次连接方法即可,并且如果超过三次重连失败,我们在业务中可以通过调用接口来获取数据,避免数据丢失,此处细节省略。

协议封装

此处协议如下所示:

{
 "action":"",
 "requestChild":{
 "clientType":"",
 "id":""
 }
}

心跳、发送请求都属于客户端主动发送请求,对于请求结果我们分为成功和失败以及超时,发送超时我们是收不到服务器任何回复的,所以我们需要在发送之后将发送放在超时任务队列中,如果请求成功将任务从超时队列中移除,超时从超时队列中获取任务重新请求。

超时任务队列中回调有成功、失败、超时。

我们按照上述协议,新增对应实体类,采用Builder设计模式

public class Request {
 
 /**
 * 行为
 */
 private String action;
 
 /**
 * 请求体
 */
 private RequestChild req;
 
 
 /**
 * 请求次数
 */
 private transient int reqCount;
 
 /**
 * 超时的时间
 */
 private transient int timeOut;
 
 
 public Request() {
 }
 
 
 public Request(String action, int reqCount, int timeOut, RequestChild req) {
 this.action = action;
 this.req = req;
 this.reqCount = reqCount;
 this.timeOut = timeOut;
 }
 
 
 public static class Builder {
 //action 请求类型
 private String action;
 //请求子类数据 按照具体业务划分
 private RequestChild req;
 //请求次数 便于重试
 private int reqCount;
 //超时时间
 private int timeOut;
 
 public Builder action(String action) {
 this.action = action;
 return this;
 }
 
 
 public Builder req(RequestChild req) {
 this.req = req;
 return this;
 }
 
 
 public Builder reqCount(int reqCount) {
 this.reqCount = reqCount;
 return this;
 }
 
 public Builder timeOut(int timeOut) {
 this.timeOut = timeOut;
 return this;
 }
 
 public Request build() {
 return new Request(action, reqCount, timeOut, req);
 }
 
 }
}
 

public class RequestChild {
 
 /**
 * 设备类型
 */
 private String clientType;
 
 
 /**
 * 用于用户注册的id
 */
 private String id;
 
 public RequestChild(String clientType, String id) {
 this.clientType = clientType;
 this.id = id;
 }
 
 public RequestChild() {
 }
 
 
 public static class Builder {
 private String clientType;
 private String id;
 
 public RequestChild.Builder setClientType(String clientType) {
 this.clientType = clientType;
 return this;
 }
 
 
 public RequestChild.Builder setId(String id) {
 this.id = id;
 return this;
 }
 
 
 public RequestChild build() {
 return new RequestChild(clientType, id);
 }
 
 }
 
 
}

我们添加一个发送请求的方法如下:

/**
 * 发送请求
 *
 * @param request 请求体
 * @param reqCount 请求次数
 * @param requestListern 请求回调
 */
private void senRequest(Request request, final int reqCount, final RequestListern requestListern) {
 if (!isNetConnect()) {
 requestListern.requestFailed("网络未连接");
 return;
 }
 
}

请求回调如下所示

public interface RequestListern {
 
 /**
 * 请求成功
 */
 void requestSuccess();
 
 /**
 * 请求失败
 *
 * @param message 请求失败消息提示
 */
 void requestFailed(String message);
}

接着我们要把请求放在超时队列中,新建超时任务类,对应的分别是请求参数、请求回调、任务调度

public class TimeOutTask {
 
 
 /**
 * 请求主体
 */
 private Request request;
 
 /**
 * 通用返回
 */
 private RequestCallBack requestCallBack;
 
 /**
 * r任务
 */
 private ScheduledFuture scheduledFuture;
 
 
 public TimeOutTask(Request request,
  RequestCallBack requestCallBack,
  ScheduledFuture scheduledFuture) {
 this.request = request;
 this.requestCallBack = requestCallBack;
 this.scheduledFuture = scheduledFuture;
 }
 
 public ScheduledFuture getScheduledFuture() {
 return scheduledFuture;
 }
 
 public void setScheduledFuture(ScheduledFuture scheduledFuture) {
 this.scheduledFuture = scheduledFuture;
 }
 
 public Request getRequest() {
 return request;
 }
 
 public void setRequest(Request request) {
 this.request = request;
 }
 
 public RequestCallBack getRequestCallBack() {
 return requestCallBack;
 }
 
 public void setRequestCallBack(RequestCallBack requestCallBack) {
 this.requestCallBack = requestCallBack;
 }
 
}

RequestCallBack是超时任务的回调,只是比请求回调多了个超时,因为超时的处理机制是一样的,所以这里我们没必要将超时回调到请求中

public interface RequestCallBack {
 
 /**
 * 请求成功
 */
 void requestSuccess();
 
 /**
 * 请求失败
 *
 * @param request 请求体
 * @param message 请求失败的消息
 */
 void requestFailed(String message, Request request);
 
 /**
 * 请求超时
 *
 * @param request 请求体
 */
 void timeOut(Request request);
}
/**
 * 添加超时任务
 */
private ScheduledFuture enqueueTimeout(final Request request, final long timeout) {
 Log.d(TAG, " " + "enqueueTimeout: 添加超时任务类型为:" + request.getAction());
 return executor.schedule(new Runnable() {
 @Override
 public void run() {
 TimeOutTask timeoutTask = callbacks.remove(request.getAction());
 if (timeoutTask != null) {
 timeoutTask.getRequestCallBack().timeOut(timeoutTask.getRequest());
 }
 }
 }, timeout, TimeUnit.MILLISECONDS);
}

超时任务的方法是通过任务调度定时调用,请求成功后我们会把超时任务移除,当到了超时时间时,任务还存在就说明任务超时了。

每次的任务我们以action为键值存在hashMap中

private Map callbacks = new HashMap<>();

将任务放入超时任务代码如下所示:

final ScheduledFuture timeoutTask = enqueueTimeout(request, request.getTimeOut());
 
final RequestCallBack requestCallBack = new RequestCallBack() {
 @Override
 public void requestSuccess() {
 requestListern.requestSuccess();
 }
 
 @Override
 public void requestFailed(String message, Request request) {
 requestListern.requestFailed(message);
 }
 
 @Override
 public void timeOut(Request request) {
 timeOutHanlder(request);
 }
};
callbacks.put(request.getAction(),
 new CallbackWrapper(request, requestCallBack, timeoutTask));

一般而言,任务超时都是由于连接原因导致,所以我们这里可以尝试重试一次,如果还是超时,通过 timeOutHanlder(request);方法 进行重新连接,重连代码和连接代码一样,这里就省略了,做好这步操作,我们就可以发送消息了。

/**
 * 超时任务
 */
private void timeOutHanlder(Request requset) {
 setStatus(WsStatus.CONNECT_FAIL);
 //这里假装有重连
 Log.d(TAG, "timeOutHanlder: 请求超时 准备重连");
}

到这里我们的流程基本可以走通了。

心跳

首先我们要了解下心跳的作用是什么,心跳是在连接成功后,通过固定的间隔时间向服务器发送询问,当前是否还在线,有很多人说心跳失败我们就重连,成功就继续心跳,但是这里要注意的是,我们一般是收不到心跳失败回调的,心跳也是向服务器发送数据,所以我们要将所有的主动请求都放在超时任务队列中,

所以对websocket来说 请求结果有三种:成功、失败、超时,对于用户 只有成功、失败即可。

至于心跳、注册等请求发送的数据是什么,这就得看我们与服务端定的协议是什么样了,通常来说 分为action 和 requestBody,协议格式我们再第二步已经封装好了,这里我们以心跳任务为例验证上面的封装。

/**
 * 心跳
 */
void keepAlive() {
 
 Request request = new Request.Builder()
 .reqCount(0)
 .timeOut(REQUEST_TIMEOUT)
 .action(ACTION_KEEPALIVE).build();
 
 WsManager.getWsManger().senRequest(request, request.getReqCount() + 1, new RequestListern() {
 @Override
 public void requestSuccess() {
 Log.d(TAG, "requestSuccess: 心跳发送成功了");
 }
 
 @Override
 public void requestFailed(String message) {
 }
 });
}

我们每间隔10s中开启一次心跳任务

/**
 * 开始心跳
 */
public void startKeepAlive() {
 mHandler.postDelayed(mKeepAliveTask, HEART_BEAT_RATE);
}
/**
 * 心跳任务
 */
private Runnable mKeepAliveTask = new Runnable() {
 
 @Override
 public void run() {
 keepAlive();
 mHandler.removeCallbacks(mKeepAliveTask);
 mHandler.postDelayed(mKeepAliveTask, HEART_BEAT_RATE);
 }
};

为了便于操作演示,在主页面上加个按钮 ,点击按钮调用startKeepAlive方法,运行如下所示: 

我们可以看到心跳返回的statue是300 不成功,5秒之后走到了请求超时的方法中,所以如果状态返回成功的话,我们需要回调给调用者

/**
 * 处理 任务回调
 *
 * @param action 请求类型
 */
void disPatchCallbackWarp(String action, boolean isSuccess) {
 CallbackWrapper callBackWarp = callbacks.remove(action);
 if (callBackWarp == null) {
 Logger.d(TAG+" "+ "disPatchCallbackWarp: 任务队列为空");
 } else {
 callBackWarp.getScheduledFuture().cancel(true);
 if (isSuccess) {
 callBackWarp.getRequestCallBack().requestSuccess();
 } else {
 callBackWarp.getRequestCallBack().requestFailed("", new Request());
 }
 
 }
}

这样调用者才知道成功或失败。

发送其他消息与心跳一样,只是请求参数不同而已,修改Request参数即可。这样我们根据协议和业务就实现一个比较规范的webSocket消息推送流程了。

 到此这篇关于在Android中使用WebSocket实现消息通信的方法详解的文章就介绍到这了,更多相关Android使用WebSocket实现消息通信内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!


推荐阅读
  • 在处理 XML 数据时,如果需要解析 `` 标签的内容,可以采用 Pull 解析方法。Pull 解析是一种高效的 XML 解析方式,适用于流式数据处理。具体实现中,可以通过 Java 的 `XmlPullParser` 或其他类似的库来逐步读取和解析 XML 文档中的 `` 元素。这样不仅能够提高解析效率,还能减少内存占用。本文将详细介绍如何使用 Pull 解析方法来提取 `` 标签的内容,并提供一个示例代码,帮助开发者快速解决问题。 ... [详细]
  • 实现Win10与Linux服务器的SSH无密码登录
    本文介绍了如何在Windows 10环境下使用Git工具,通过配置SSH密钥对,实现与Linux服务器的无密码登录。主要步骤包括生成本地公钥、上传至服务器以及配置服务器端的信任关系。 ... [详细]
  • WebBenchmark:强大的Web API性能测试工具
    本文介绍了一款名为WebBenchmark的Web API性能测试工具,该工具不仅支持HTTP和HTTPS服务的测试,还提供了丰富的功能来帮助开发者进行高效的性能评估。 ... [详细]
  • Fiddler 安装与配置指南
    本文详细介绍了Fiddler的安装步骤及配置方法,旨在帮助用户顺利抓取用户Token。文章还涵盖了一些常见问题的解决方案,以确保安装过程顺利。 ... [详细]
  • H5技术实现经典游戏《贪吃蛇》
    本文将分享一个使用HTML5技术实现的经典小游戏——《贪吃蛇》。通过H5技术,我们将探讨如何构建这款游戏的两种主要玩法:积分闯关和无尽模式。 ... [详细]
  • Docker安全策略与管理
    本文探讨了Docker的安全挑战、核心安全特性及其管理策略,旨在帮助读者深入理解Docker安全机制,并提供实用的安全管理建议。 ... [详细]
  • 如何高效学习鸿蒙操作系统:开发者指南
    本文探讨了开发者如何更有效地学习鸿蒙操作系统,提供了来自行业专家的建议,包括系统化学习方法、职业规划建议以及具体的开发技巧。 ... [详细]
  • 深入理解iOS中的链式编程:以Masonry为例
    本文通过介绍Masonry这一轻量级布局框架,探讨链式编程在iOS开发中的应用。Masonry不仅简化了Auto Layout的使用,还提高了代码的可读性和维护性。 ... [详细]
  • 笔记说明重学前端是程劭非(winter)【前手机淘宝前端负责人】在极客时间开的一个专栏,每天10分钟,重构你的前端知识体系& ... [详细]
  • 2023年,Android开发前景如何?25岁还能转行吗?
    近期,关于Android开发行业的讨论在多个平台上热度不减,许多人担忧其未来发展。本文将探讨当前Android开发市场的现状、薪资水平及职业选择建议。 ... [详细]
  • Java 中的十进制样式 getZeroDigit()方法,示例 ... [详细]
  • 在开发一个网页音乐播放器时遇到问题,需要从不同源读取MP3文件的ID3标签信息,包括流派、歌手和歌曲名称等。尝试使用PHP未果后转而考虑使用JavaScript进行跨域读取,但不清楚具体配置方法,寻求技术指导。 ... [详细]
  • CRZ.im:一款极简的网址缩短服务及其安装指南
    本文介绍了一款名为CRZ.im的极简网址缩短服务,该服务采用PHP和SQLite开发,体积小巧,约10KB。本文还提供了详细的安装步骤,包括环境配置、域名解析及Nginx伪静态设置。 ... [详细]
  • Requests库的基本使用方法
    本文介绍了Python中Requests库的基础用法,包括如何安装、GET和POST请求的实现、如何处理Cookies和Headers,以及如何解析JSON响应。相比urllib库,Requests库提供了更为简洁高效的接口来处理HTTP请求。 ... [详细]
  • 本文汇集了我在网络上搜集以及在实际面试中遇到的前端开发面试题目,并附有详细解答。无论是初学者还是有一定经验的开发者,都应深入理解这些问题背后的原理,通过系统学习和透彻研究,逐步形成自己的知识体系和技术框架。 ... [详细]
author-avatar
圆季
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有