欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

使用Android WebSocket实现即时通讯功能

程序员文章站 2022-06-20 14:52:50
最近做这个功能,分享一下。即时通讯(instant messaging)最重要的毫无疑问就是即时,不能有明显的延迟,要实现im的功能其实并不难,目前有很多第三方,比如极光的jmessa...

最近做这个功能,分享一下。即时通讯(instant messaging)最重要的毫无疑问就是即时,不能有明显的延迟,要实现im的功能其实并不难,目前有很多第三方,比如极光的jmessage,都比较容易实现。但是如果项目有特殊要求(如不能使用外网),那就得自己做了,所以我们需要使用websocket。

websocket

websocket协议就不细讲了,感兴趣的可以具体查阅资料,简而言之,它就是一个可以建立长连接的全双工(full-duplex)通信协议,允许服务器端主动发送信息给客户端。

java-websocket框架

对于使用websocket协议,android端已经有些成熟的框架了,在经过对比之后,我选择了java-websocket这个开源框架,github地址:https://github.com/tootallnate/java-websocket,目前已经有五千以上star,并且还在更新维护中,所以本文将介绍如何利用此开源库实现一个稳定的即时通讯功能。

效果图

国际惯例,先上效果图

使用Android WebSocket实现即时通讯功能

使用Android WebSocket实现即时通讯功能

使用Android WebSocket实现即时通讯功能

使用Android WebSocket实现即时通讯功能

文章重点

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建立了连接

使用Android 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);//开启心跳检测

我们打印一下日志,如图所示

使用Android WebSocket实现即时通讯功能

六、服务(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实现即时通讯功能,希望对大家有所帮助