使用Android WebSocket实现即时通讯功能
最近做这个功能,分享一下。即时通讯(instant messaging)最重要的毫无疑问就是即时,不能有明显的延迟,要实现im的功能其实并不难,目前有很多第三方,比如极光的jmessage,都比较容易实现。但是如果项目有特殊要求(如不能使用外网),那就得自己做了,所以我们需要使用websocket。
websocket
websocket协议就不细讲了,感兴趣的可以具体查阅资料,简而言之,它就是一个可以建立长连接的全双工(full-duplex)通信协议,允许服务器端主动发送信息给客户端。
java-websocket框架
对于使用websocket协议,android端已经有些成熟的框架了,在经过对比之后,我选择了java-websocket这个开源框架,github地址:https://github.com/tootallnate/java-websocket,目前已经有五千以上star,并且还在更新维护中,所以本文将介绍如何利用此开源库实现一个稳定的即时通讯功能。
效果图
国际惯例,先上效果图
文章重点
1、与websocket建立长连接
2、与websocket进行即时通讯
3、service和activity之间通讯和ui更新
4、弹出消息通知(包括锁屏通知)
5、心跳检测和重连(保证websocket连接稳定性)
6、服务(service)保活
一、引入java-websocket
1、build.gradle中加入
implementation "org.java-websocket:java-websocket:1.4.0"
2、加入网络请求权限
<uses-permission android:name="android.permission.internet" />
3、新建客户端类
新建一个客户端类并继承websocketclient,需要实现它的四个抽象方法和构造函数,如下:
public class jwebsocketclient extends websocketclient { public jwebsocketclient(uri serveruri) { super(serveruri, new draft_6455()); } @override public void onopen(serverhandshake handshakedata) { log.e("jwebsocketclient", "onopen()"); } @override public void onmessage(string message) { log.e("jwebsocketclient", "onmessage()"); } @override public void onclose(int code, string reason, boolean remote) { log.e("jwebsocketclient", "onclose()"); } @override public void onerror(exception ex) { log.e("jwebsocketclient", "onerror()"); } }
其中onopen()方法在websocket连接开启时调用,onmessage()方法在接收到消息时调用,onclose()方法在连接断开时调用,onerror()方法在连接出错时调用。构造方法中的new draft_6455()代表使用的协议版本,这里可以不写或者写成这样即可。
4、建立websocket连接
建立连接只需要初始化此客户端再调用连接方法,需要注意的是websocketclient对象是不能重复使用的,所以不能重复初始化,其他地方只能调用当前这个client。
uri uri = uri.create("ws://*******"); jwebsocketclient client = new jwebsocketclient(uri) { @override public void onmessage(string message) { //message就是接收到的消息 log.e("jwebsclientservice", message); } };
为了方便对接收到的消息进行处理,可以在这重写onmessage()方法。初始化客户端时需要传入websocket地址(测试地址:ws://echo.websocket.org),websocket协议地址大致是这样的
ws:// ip地址 : 端口号
连接时可以使用connect()方法或connectblocking()方法,建议使用connectblocking()方法,connectblocking多出一个等待操作,会先连接再发送。
try { client.connectblocking(); } catch (interruptedexception e) { e.printstacktrace(); }
运行之后可以看到客户端的onopen()方法得到了执行,表示已经和websocket建立了连接
5、发送消息
发送消息只需要调用send()方法,如下
if (client != null && client.isopen()) { client.send("你好"); }
6、关闭socket连接
关闭连接调用close()方法,最后为了避免重复实例化websocketclient对象,关闭时一定要将对象置空。
/** * 断开连接 */ private void closeconnect() { try { if (null != client) { client.close(); } } catch (exception e) { e.printstacktrace(); } finally { client = null; } }
二、后台运行
一般来说即时通讯功能都希望像qq微信这些app一样能在后台保持运行,当然app保活这个问题本身就是个伪命题,我们只能尽可能保活,所以首先就是建一个service,将websocket的逻辑放入服务中运行并尽可能保活,让websocket保持连接。
1、新建service
新建一个service,在启动service时实例化websocketclient对象并建立连接,将上面的代码搬到服务里即可。
2、service和activity之间通讯
由于消息是在service中接收,从activity中发送,需要获取到service中的websocketclient对象,所以需要进行服务和活动之间的通讯,这就需要用到service中的onbind()方法了。
首先新建一个binder类,让它继承自binder,并在内部提供相应方法,然后在onbind()方法中返回这个类的实例。
public class jwebsocketclientservice extends service { private uri uri; public jwebsocketclient client; private jwebsocketclientbinder mbinder = new jwebsocketclientbinder(); //用于activity和service通讯 class jwebsocketclientbinder extends binder { public jwebsocketclientservice getservice() { return jwebsocketclientservice.this; } } @override public ibinder onbind(intent intent) { return mbinder; } }
接下来就需要对应的activity绑定service,并获取service的东西,代码如下
public class mainactivity extends appcompatactivity { private jwebsocketclient client; private jwebsocketclientservice.jwebsocketclientbinder binder; private jwebsocketclientservice jwebsclientservice; private serviceconnection serviceconnection = new serviceconnection() { @override public void onserviceconnected(componentname componentname, ibinder ibinder) { //服务与活动成功绑定 log.e("mainactivity", "服务与活动成功绑定"); binder = (jwebsocketclientservice.jwebsocketclientbinder) ibinder; jwebsclientservice = binder.getservice(); client = jwebsclientservice.client; } @override public void onservicedisconnected(componentname componentname) { //服务与活动断开 log.e("mainactivity", "服务与活动成功断开"); } }; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_main); bindservice(); } /** * 绑定服务 */ private void bindservice() { intent bindintent = new intent(mainactivity.this, jwebsocketclientservice.class); bindservice(bindintent, serviceconnection, bind_auto_create); } }
这里首先创建了一个serviceconnection匿名类,在里面重写onserviceconnected()和onservicedisconnected()方法,这两个方法会在活动与服务成功绑定以及连接断开时调用。在onserviceconnected()首先得到jwebsocketclientbinder的实例,有了这个实例便可调用服务的任何public方法,这里调用getservice()方法得到service实例,得到了service实例也就得到了websocketclient对象,也就可以在活动中发送消息了。
三、从service中更新activity的ui
当service中接收到消息时需要更新activity中的界面,方法有很多,这里我们利用广播来实现,在对应activity中定义广播接收者,service中收到消息发出广播即可。
public class mainactivity extends appcompatactivity { ... private class chatmessagereceiver extends broadcastreceiver{ @override public void onreceive(context context, intent intent) { string message=intent.getstringextra("message"); } } /** * 动态注册广播 */ private void doregisterreceiver() { chatmessagereceiver = new chatmessagereceiver(); intentfilter filter = new intentfilter("com.xch.servicecallback.content"); registerreceiver(chatmessagereceiver, filter); } ... }
上面的代码很简单,首先创建一个内部类并继承自broadcastreceiver,也就是代码中的广播接收器chatmessagereceiver,然后动态注册这个广播接收器。当service中接收到消息时发出广播,就能在chatmessagereceiver里接收广播了。
发送广播:
client = new jwebsocketclient(uri) { @override public void onmessage(string message) { intent intent = new intent(); intent.setaction("com.xch.servicecallback.content"); intent.putextra("message", message); sendbroadcast(intent); } };
获取广播传过来的消息后即可更新ui,具体布局就不细说,比较简单,看下我的源码就知道了,demo地址我会放到文章末尾。
四、消息通知
消息通知直接使用notification,只是当锁屏时需要先点亮屏幕,代码如下
/** * 检查锁屏状态,如果锁屏先点亮屏幕 * * @param content */ private void checklockandshownotification(string content) { //管理锁屏的一个服务 keyguardmanager km = (keyguardmanager) getsystemservice(context.keyguard_service); if (km.inkeyguardrestrictedinputmode()) {//锁屏 //获取电源管理器对象 powermanager pm = (powermanager) this.getsystemservice(context.power_service); if (!pm.isscreenon()) { @suppresslint("invalidwakelocktag") powermanager.wakelock wl = pm.newwakelock(powermanager.acquire_causes_wakeup | powermanager.screen_bright_wake_lock, "bright"); wl.acquire(); //点亮屏幕 wl.release(); //任务结束后释放 } sendnotification(content); } else { sendnotification(content); } } /** * 发送通知 * * @param content */ private void sendnotification(string content) { intent intent = new intent(); intent.setclass(this, mainactivity.class); pendingintent pendingintent = pendingintent.getactivity(this, 0, intent, pendingintent.flag_update_current); notificationmanager notifymanager = (notificationmanager) getsystemservice(context.notification_service); notification notification = new notificationcompat.builder(this) .setautocancel(true) // 设置该通知优先级 .setpriority(notification.priority_max) .setsmallicon(r.mipmap.ic_launcher) .setcontenttitle("昵称") .setcontenttext(content) .setvisibility(visibility_public) .setwhen(system.currenttimemillis()) // 向通知添加声音、闪灯和振动效果 .setdefaults(notification.default_vibrate | notification.default_all | notification.default_sound) .setcontentintent(pendingintent) .build(); notifymanager.notify(1, notification);//id要保证唯一 }
如果未收到通知可能是设置里通知没开,进入设置打开即可,如果锁屏时无法弹出通知,可能是未开启锁屏通知权限,也需进入设置开启。为了保险起见我们可以判断通知是否开启,未开启引导用户开启,代码如下:
最后加
/** * 检测是否开启通知 * * @param context */ private void checknotification(final context context) { if (!isnotificationenabled(context)) { new alertdialog.builder(context).settitle("温馨提示") .setmessage("你还未开启系统通知,将影响消息的接收,要去开启吗?") .setpositivebutton("确定", new dialoginterface.onclicklistener() { @override public void onclick(dialoginterface dialog, int which) { setnotification(context); } }).setnegativebutton("取消", new dialoginterface.onclicklistener() { @override public void onclick(dialoginterface dialog, int which) { } }).show(); } } /** * 如果没有开启通知,跳转至设置界面 * * @param context */ private void setnotification(context context) { intent localintent = new intent(); //直接跳转到应用通知设置的代码: if (android.os.build.version.sdk_int >= build.version_codes.lollipop) { localintent.setaction("android.settings.app_notification_settings"); localintent.putextra("app_package", context.getpackagename()); localintent.putextra("app_uid", context.getapplicationinfo().uid); } else if (android.os.build.version.sdk_int == build.version_codes.kitkat) { localintent.setaction(settings.action_application_details_settings); localintent.addcategory(intent.category_default); localintent.setdata(uri.parse("package:" + context.getpackagename())); } else { //4.4以下没有从app跳转到应用通知设置页面的action,可考虑跳转到应用详情页面 localintent.addflags(intent.flag_activity_new_task); if (build.version.sdk_int >= 9) { localintent.setaction("android.settings.application_details_settings"); localintent.setdata(uri.fromparts("package", context.getpackagename(), null)); } else if (build.version.sdk_int <= 8) { localintent.setaction(intent.action_view); localintent.setclassname("com.android.settings", "com.android.setting.installedappdetails"); localintent.putextra("com.android.settings.applicationpkgname", context.getpackagename()); } } context.startactivity(localintent); } /** * 获取通知权限,检测是否开启了系统通知 * * @param context */ @targetapi(build.version_codes.kitkat) private boolean isnotificationenabled(context context) { string check_op_no_throw = "checkopnothrow"; string op_post_notification = "op_post_notification"; appopsmanager mappops = (appopsmanager) context.getsystemservice(context.app_ops_service); applicationinfo appinfo = context.getapplicationinfo(); string pkg = context.getapplicationcontext().getpackagename(); int uid = appinfo.uid; class appopsclass = null; try { appopsclass = class.forname(appopsmanager.class.getname()); method checkopnothrowmethod = appopsclass.getmethod(check_op_no_throw, integer.type, integer.type, string.class); field oppostnotificationvalue = appopsclass.getdeclaredfield(op_post_notification); int value = (integer) oppostnotificationvalue.get(integer.class); return ((integer) checkopnothrowmethod.invoke(mappops, value, uid, pkg) == appopsmanager.mode_allowed); } catch (exception e) { e.printstacktrace(); } return false; }
入相关的权限
<!-- 解锁屏幕需要的权限 --> <uses-permission android:name="android.permission.disable_keyguard" /> <!-- 申请电源锁需要的权限 --> <uses-permission android:name="android.permission.wake_lock" /> <!--震动权限--> <uses-permission android:name="android.permission.vibrate" />
五、心跳检测和重连
由于很多不确定因素会导致websocket连接断开,例如网络断开,所以需要保证websocket的连接稳定性,这就需要加入心跳检测和重连。
心跳检测其实就是个定时器,每个一段时间检测一次,如果连接断开则重连,java-websocket框架在目前最新版本中有两个重连的方法,分别是reconnect()和reconnectblocking(),这里同样使用后者。
private static final long heart_beat_rate = 10 * 1000;//每隔10秒进行一次对长连接的心跳检测 private handler mhandler = new handler(); private runnable heartbeatrunnable = new runnable() { @override public void run() { if (client != null) { if (client.isclosed()) { reconnectws(); } } else { //如果client已为空,重新初始化websocket initsocketclient(); } //定时对长连接进行心跳检测 mhandler.postdelayed(this, heart_beat_rate); } }; /** * 开启重连 */ private void reconnectws() { mhandler.removecallbacks(heartbeatrunnable); new thread() { @override public void run() { try { //重连 client.reconnectblocking(); } catch (interruptedexception e) { e.printstacktrace(); } } }.start(); }
然后在服务启动时开启心跳检测
mhandler.postdelayed(heartbeatrunnable, heart_beat_rate);//开启心跳检测
我们打印一下日志,如图所示
六、服务(service)保活
如果某些业务场景需要app保活,例如利用这个websocket来做推送,那就需要我们的app后台服务不被kill掉,当然如果和手机厂商没有合作,要保证服务一直不被杀死,这可能是所有android开发者比较头疼的一个事,这里我们只能尽可能的来保证service的存活。
1、提高服务优先级(前台服务)
前台服务的优先级比较高,它会在状态栏显示类似于通知的效果,可以尽量避免在内存不足时被系统回收,前台服务比较简单就不细说了。有时候我们希望可以使用前台服务但是又不希望在状态栏有显示,那就可以利用灰色保活的办法,如下
private final static int gray_service_id = 1001; //灰色保活手段 public static class grayinnerservice extends service { @override public int onstartcommand(intent intent, int flags, int startid) { startforeground(gray_service_id, new notification()); stopforeground(true); stopself(); return super.onstartcommand(intent, flags, startid); } @override public ibinder onbind(intent intent) { return null; } } //设置service为前台服务,提高优先级 if (build.version.sdk_int < 18) { //android4.3以下 ,隐藏notification上的图标 startforeground(gray_service_id, new notification()); } else if(build.version.sdk_int>18 && build.version.sdk_int<25){ //android4.3 - android7.0,隐藏notification上的图标 intent innerintent = new intent(this, grayinnerservice.class); startservice(innerintent); startforeground(gray_service_id, new notification()); }else{ //暂无解决方法 startforeground(gray_service_id, new notification()); }
androidmanifest.xml中注册这个服务
<service android:name=".im.jwebsocketclientservice$grayinnerservice" android:enabled="true" android:exported="false" android:process=":gray"/>
这里其实就是开启前台服务并隐藏了notification,也就是再启动一个service并共用一个通知栏,然后stop这个service使得通知栏消失。但是7.0以上版本会在状态栏显示“正在运行”的通知,目前暂时没有什么好的解决办法。
2、修改service的onstartcommand 方法返回值
@override public int onstartcommand(intent intent, int flags, int startid) { ... return start_sticky; }
onstartcommand()返回一个整型值,用来描述系统在杀掉服务后是否要继续启动服务,start_sticky表示如果service进程被kill掉,系统会尝试重新创建service。
3、锁屏唤醒
powermanager.wakelock wakelock;//锁屏唤醒 private void acquirewakelock() { if (null == wakelock) { powermanager pm = (powermanager)this.getsystemservice(context.power_service); wakelock = pm.newwakelock(powermanager.partial_wake_lock|powermanager.on_after_release, "postlocationservice"); if (null != wakelock) { wakelock.acquire(); } } }
获取电源锁,保持该服务在屏幕熄灭时仍然获取cpu时,让其保持运行。
4、其他保活方式
服务保活还有许多其他方式,比如进程互拉、一像素保活、申请自启权限、引导用户设置白名单等,其实android 7.0版本以后,目前没有什么真正意义上的保活,但是做些处理,总比不做处理强。这篇文章重点是即时通讯,对于服务保活有需要的可以自行查阅更多资料,这里就不细说了。
最后附上这篇文章源码地址,github:https://github.com/yangxch/websocketclient,如果有帮助帮忙点个star吧。
总结
以上所述是小编给大家介绍的android websocket实现即时通讯功能,希望对大家有所帮助
下一篇: 为什么使用消息队列?消息对列有什么好处?