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

        本篇文章主要介绍自己使用WebSocket实现Android端即时通讯聊天功能的过程,最终我们使用WebSocket实现了两个客户端之间的即时通讯聊天功能和直播中的聊天室功能,当然整个WebSocket还是比较复杂的,特别是长链接的稳定性方面自己还需加强(感叹微信的长链接真是稳定啊),所以也希望大家共同探讨。

        关于Socket和WebSocket的区别以及详细介绍在此就不赘述了,这方面的介绍网上还是比较多的。

        一、使用Java-WebSocket开源框架        

        这个框架也是我在 Github 上对比了一圈之后选中的一个,使用比较方便,Star数可观并且一直还在更新维护。首先,本地使用Java-WebSocket框架实现WebSocket客户端,地址:Java-WebSocket地址,根据项目主页介绍在Android Studio中添加依赖:  

    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还是比较复杂的,我自己实现的稳定性也还需要加强,上面如有不到之处还请指出。   

猜你喜欢

转载自blog.csdn.net/beita08/article/details/80162070