本篇文章主要介绍自己使用WebSocket实现Android端即时通讯聊天功能的过程,最终我们使用WebSocket实现了两个客户端之间的即时通讯聊天功能和直播中的聊天室功能,当然整个WebSocket还是比较复杂的,特别是长链接的稳定性方面自己还需加强(感叹微信的长链接真是稳定啊),所以也希望大家共同探讨。
关于Socket和WebSocket的区别以及详细介绍在此就不赘述了,这方面的介绍网上还是比较多的。
一、使用Java-WebSocket框架
这个框架也是我在 Github 上对比了一圈之后选中的一个,使用比较方便,Star数可观并且一直还在更新维护。首先,本地使用Java-WebSocket框架实现WebSocket客户端,地址:Java-WebSocket地址,添加依赖:
compile 'org.java-websocket:Java-WebSocket:1.3.8'
Java-WebSocket是一个纯java写的WebSocket客户端和服务端实现,在客户端我们需要自己写一个类继承Java-WebSocket中的客户端 WebSocketClient ,实现四个抽象方法和一个构造方法,如下:
public class MyWebSocketClient extends WebSocketClient {
public MyWebSocketClient(URI serverUri) {
super(serverUri);
}
//长链接开启
@Override
public void onOpen(ServerHandshake handshakedata) {
}
//消息通道收到消息
@Override
public void onMessage(String message) {
}
//长链接关闭
@Override
public void onClose(int code, String reason, boolean remote) {
}
//链接发生错误
@Override
public void onError(Exception ex) {
}
}
构造方法中需要传一个serverUri,需要说明的是WebSocket的链接是ws协议的,所以应该是这样的:
ws:// [ip地址] : [端口号]
点击super看源码可以看见如下构造,由此可见Java-WebSocket使用的WebSocket协议版本是RFC 6455(作者在项目主页也说明了),当然也提供了其他构造更改协议版本。
//源码中的构造方法
public WebSocketClient( URI serverUri ) {
this( serverUri, new Draft_6455());
}
由于WebSocketClient对象是不能重复使用的,所以我将MyWebSocketClient写为单例模式:
private Context mContext;
//选择懒汉模式
private static MyWebSocketClient mInstance;
//1. 私有构造方法
private MyWebSocketClient(Context context) {
//开启WebSocket客户端
super(URI.create("webSocket链接"));
this.mContext = context;
}
//2.公开方法,返回单例对象
public static MyWebSocketClient getInstance(Context context) {
//懒汉: 考虑线程安全问题, 两种方式: 1. 给方法加同步锁 synchronized, 效率低; 2. 给创建对象的代码块加同步锁
if (mInstance == null) {
synchronized (MyWebSocketClient.class) {
if (mInstance == null) {
mInstance = new MyWebSocketClient(context);
}
}
}
return mInstance;
}
这样我们就可以从外部对MyWebSocketClient进行初始化并开启链接。
二、开启WebSocket链接
开启WebSocket链接时要特别注意!!!WebSocket有五种状态,分别是NOT_YET_CONNECTED(还没有连接), CONNECTING(正在连接), OPEN(打开状态), CLOSING(正在关闭), CLOSED(已关闭)。由于WebSocketClient对象是不能重复使用的,所以当WebSocket处于CONNECTING、OPEN、CLOSING、 CLOSED这四种状态时,说明已经被初始化过了,所以此时再次初始化链接时会报异常: WebSocketClient objects are not reuseable ; (这里我刚开始没有弄清楚,使用的是isConnecting()、isOpen()、isClosing()、isClosed()这四个方法返回的boolean值来判断状态,判断不出来NOT_YET_CONNECTED状态,然后各种混乱)
//源码中五种状态的枚举
enum READYSTATE {
NOT_YET_CONNECTED, CONNECTING, OPEN, CLOSING, CLOSED
}
//源码中初始化链接的方法,如果状态不对会报异常
public void connect() {
if( writeThread != null )
throw new IllegalStateException( "WebSocketClient objects are not reuseable" );
writeThread = new Thread(this);
writeThread.setName( "WebSocketConnectReadThread-" + writeThread.getId() );
writeThread.start();
}
上面也可以看到执行connect()时底层会创建一个线程并对其命名,所以并不需要我们自己创建线程。
好了,现在只需要在合适的地方对MyWebSocketClient判断状态并使用connect()方法进行初始化开启链接:
//初始化开启WebSocket链接
WebSocket.READYSTATE readyState = MyWebSocketClient.getInstance(this).getReadyState();
Log.i("WebSocket", "getReadyState() = " + readyState);
//当WebSocket的状态是NOT_YET_CONNECTED时使用connect()方法进行初始化开启链接:
if (readyState.equals(WebSocket.READYSTATE.NOT_YET_CONNECTED)) {
Log.i("WebSocket", "---初始化WebSocket客户端---");
MyWebSocketClient.getInstance(this).connect();
}
开启链接时会回调WebSocketClient中的onOpen(ServerHandshake handshakedata)、onMessage(String message)两个方法。在一对一聊天时,还需要服务器针对每一台设备生成一个唯一的客户端设备ID,可以通过onMessage(String message)将其返回到客户端,然后客户端需要将客户端设备ID和用户UserID进行绑定。
后续所有通过消息通道推送到客户端的消息会通过onMessage(String message)方法发送到客户端。所以收到消息后的操作需要在onMessage(String message)方法中完成,比如收到聊天消息,首先将消息保存到本地消息数据库,然后使用EventBus将消息发送到聊天页面用以展示。
三、WebSocket重新连接
WebSocket的重新连接是建立一个稳定的WebSocket长链接非常重要的一部分,因为WebSocket的长链接通道随时可能因为手机的网络变化、WiFi切换等因素而断开,从而影响聊天等功能的稳定性。WebSocket的重新连接我采用过两种方法:一种是不使用心跳包,在WebSocket长链接断开时,发起重连,若未重连成功,则再次发起重连;另一种是每隔一定时间(比如30秒),客户端向服务器发送心跳包,判断长链接通道是否连通,如果心跳包发送不成功,则发起重连。第一种的优势是不用发送心跳包,不必耗费客户端的资源,但是稳定性不如第二种发送心跳包的方式;第二种发送心跳包的方式相比第一种稳定性更好,但是一直发送心跳包(包括APP在后台运行时),会影响耗费客户端资源,会影响一些性能。
1、在WebSocket长链接断开时,发起重连
首先,我们应该在什么时候发起重新连接?在各种因素导致WebSocket的长链接断开时,会回调WebSocketClient中的onClose()方法,所以我们可以在此发起重新连接;还有将客户端设备ID和用户UserID进行绑定时,如果失败也需要发起重新连接然后再次进行绑定。在需要多次重连时,我设计了一个简单的时间间隔机制:第一次断开时延时500毫秒发起重连,如果重连失败则第二次延时1000毫秒发起重连,如果再次失败则第三次延时2000毫秒发起重连,以此类推每次时间间隔翻倍直至重连10次以后如果还未成功则宣告重连失败(最大的时间间隔可达17分钟,用户从弱网环境回到正常网络环境时也可以重连)。当某一次重连成功后则将重连时间间隔重置为500毫秒,将重连次数重置为0,以待下次执行同样的时间间隔机制进行重新连接。
//长链接关闭
//在各种因素导致WebSocket的长链接断开时会回调onClose()方法,所以可以在此发起重新连接;(客户端设备ID和用户UserID绑定失败时也需要发起重新连接然后再次进行绑定)
@Override
public void onClose(int code, String reason, boolean remote) {
Log.i("WebSocket", "...MyyWebSocketClient...onClose...");
mHandler.removeMessages(MSG_EMPTY);
mHandler.sendEmptyMessageDelayed(MSG_EMPTY, GlobalConstants.RECONNECT_DELAYED_TIME);
//将时间间隔翻倍
GlobalConstants.RECONNECT_DELAYED_TIME = GlobalConstants.RECONNECT_DELAYED_TIME * 2;
}
//长链接开启
//重连成功后则将重连时间间隔重置为500毫秒,将重连次数重置为0,以待下次执行同样的时间间隔机制进行重新连接
@Override
public void onOpen(ServerHandshake handshakedata) {
Log.i("WebSocket", "...MyyWebSocketClient...onOpen...");
GlobalConstants.webSocketConnectNumber = 0;
GlobalConstants.RECONNECT_DELAYED_TIME = 500;
mHandler.removeMessages(MSG_EMPTY);
}
在我们自己的项目做即时通讯聊天和直播聊天室功能时,Java-WebSocket框架的版本还是1.3.5;当我写这篇文章时Java-WebSocket框架的版本已经更新至1.3.8,在1.3.8版本中新增了两个重新连接的方法reconnect()和reconnectBlocking()。在1.3.5版本时没有直接提供重新连接的方法,我采取的方法是:先将原来的链接彻底关闭,再重新创建一个MyWebSocketClient对象(因为WebSocketClient对象是不能重复使用的),然后执行connect()方法重新连接。当然,在1.3.8及以后版本中建议使用reconnect()或reconnectBlocking()方法进行重新连接。
//在Handler消息队列中执行重新连接,也便于重连时间间隔控制
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
Log.i("WebSocket", "webSocketConnectNumber = " + GlobalConstants.webSocketConnectNumber);
//未超过设置次数时执行重连操作
if (GlobalConstants.webSocketConnectNumber <= 10) {
if (mInstance != null) {
WebSocket.READYSTATE readyState = mInstance.getReadyState();
if (readyState.equals(WebSocket.READYSTATE.NOT_YET_CONNECTED)) {
mInstance.connect();
} else if (readyState.equals(WebSocket.READYSTATE.CLOSED) || readyState.equals(WebSocket.READYSTATE.CLOSING)) {
//先将原来的链接关闭,再重新创建一个MyWebSocketClient对象,然后执行connect()方法重新连接。
//(此为1.3.5版本重连的方法,建议使用1.3.8版本提供的重连方法)
mInstance.closeBlocking();
mInstance = new MyWebSocketClient(mContext);
mInstance.connect();
//将连接次数自增
GlobalConstants.webSocketConnectNumber++;
}
}
} else {
//超过设置次数则清空重连消息队列,并将mInstance置为null(待外部初始化连接)
mHandler.removeMessages(MSG_EMPTY);
mInstance = null;
}
}
};
//在Handler消息队列中执行重新连接,也便于重连时间间隔控制
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
Log.i("WebSocket", "webSocketConnectNumber = " + GlobalConstants.webSocketConnectNumber);
//未超过设置次数时执行重连操作
if (GlobalConstants.webSocketConnectNumber <= 10) {
if (mInstance != null) {
WebSocket.READYSTATE readyState = mInstance.getReadyState();
if (readyState.equals(WebSocket.READYSTATE.NOT_YET_CONNECTED)) {
mInstance.connect();
} else if (readyState.equals(WebSocket.READYSTATE.CLOSED) || readyState.equals(WebSocket.READYSTATE.CLOSING)) {
//使用1.3.8版本提供的reconnect()方法重连
mInstance.reconnect();
//将连接次数自增
GlobalConstants.webSocketConnectNumber++;
}
}
} else {
//超过设置次数则清空重连消息队列,并将mInstance置为null(待外部初始化连接)
mHandler.removeMessages(MSG_EMPTY);
mInstance = null;
}
}
};
2、客户端向服务器发送心跳包
Java-WebSocket框架内部封装的有两个发送消息的方法:send(String text)和sendPing(),当我们需要发送消息时采用send(String text)方法会将携带的消息发送至服务端,而sendPing()方法不会携带任何消息,但可以判断长链接通道和服务器是否连通,所以我们可以使用sendPing()方法来向服务器发送心跳包,若未成功则发起重连。
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == MSG_HEART) {
try {
mInstance.sendPing();
} catch (Exception e) {
e.printStackTrace();
mInstance.reconnect();
} finally {
mHandler.removeMessages(MSG_HEART);
mHandler.sendEmptyMessageDelayed(MSG_HEART, 30 * 1000);
}
}
}
};
在长链接开启时就开始发送心跳包,在长链接关闭的时候onClose()方法中也应该立即发送一个心跳包。
//长链接开启
@Override
public void onOpen(ServerHandshake handshakedata) {
mHandler.removeMessages(MSG_HEART);
mHandler.sendEmptyMessageDelayed(MSG_HEART, 30 * 1000);
}
到这里我实现的功能就基本完毕了,当然整个WebSocket还是比较复杂的,我自己实现的稳定性也还需要加强,上面如有不到之处还请指出。
- 1. Windows Server 2008 R2永久激活及Chew-WGA v0.9下载(12644)
- 2.Visual Studio 2017中安装visualSVN及使用详解(4838)
- 3.完美解决iis下JWplayer提示Error loading media: File could not be played错误(3707)
- 4.asp.net mvc+jquery easyui开发基础(一)模块首页及增加、修改、删除模块实现(2931)
- 5.Android avax.net.ssl.SSLPeerUnverifiedException: No peer certificate 解决方法(httpClient支持HTTPS的访问方式)(2792)
- 6..Net Mvc中使用Jquery EasyUI控件讲解(一)表格控件datagrid使用介绍(2637)
- 7.asp.net mvc+jquery easyui开发实战教程之网站后台管理系统开发(三)登录模块开发(2577)
- 8.asp.net mvc+jquery easyui开发实战教程之网站后台管理系统开发(七)权限管理模块之系统菜单动态生成(2541)
- 9. asp.net mvc+jquery easyui开发实战教程之网站后台管理系统开发(六)权限管理模块之初始数据准备(2191)
- 10.asp.net mvc+jquery easyui开发实战教程之网站后台管理系统开发(八)权限管理模块之权限管理实现(2160)