长连接状态管理

618 阅读6分钟

前两篇文章我们介绍了客户端长连接的设计框架协议实现两部分内容,还不了解的同学可以点击下面链接查看相关文档:

本篇文章我们重点介绍长连接的连接状态管理,通过这篇文章我们将掌握:

  1. 长连接状态有哪些
  2. 不同状态之间的流转关系。
  3. 不同状态之间切换源码实现。

状态流转关系

我们首先需要掌握长连接状态流转关系,可以更好辅助我们写出高效的代码并且方便维护管理。

长连接会有下面几个状态:

  • Disconnected: 断开状态
  • Connecting:建连中状态
  • Connected:已建连状态
  • Disconnecting:断开中状态
  • Closed:关闭状态

它们之间的流转关系如下:

长连接状态管理.png

CLOSEPENDING 和 STOPPING 是两个boolean值,后面会具体介绍。

Disconnected

长连接默认是Disconnected 状态。只有当前状态为Disconnecting 时才允许设为Disconnected状态,其他状态均不允许。

Connecting

Connecting 是建连中状态,也只有 Disconnected 状态时才允许变更到 Connecting 状态。

Connected

和服务器建连成功后会将状态设置为 Connected状态,但是此时不需要做状态验证。

Disconnecting

只有 Connecting 和 Connected 状态时才允许设置为 Disconnecting。

Closed

只有长连接完全断开(Disconnected) 状态时,才允许设置为Close ,Close时会将所有的长连接对象全部销毁并设为null。

源码实现

手把手教你搭建客户端长连接框架 中我们有提到我们是通过 ClientComms 来负责管理长连接状态的,接下来我们从connect ,disconnect ,close 等操作过程中观察状态的切换过程。

connect过程

这里的connect 操作包含了2部分内容:

  1. 创建Socket 连接。

  2. 发起连接请求,可以理解为登录请求,验证客户端的身份是否合法。

connect 过程中, 状态会从有这样的流转 Disconnected -> Connecting -> Connected , 具体流程如下:

connect过程状态流转.png

从流程中,我们可以看到connect动作 只有在Disconnected 状态且 ClosePending 态为false 时才允许执行,否则都会抛出异常通知上层业务为什么建连失败。

下面我们再来看看具体的代码实现:


class ClientComms {

    private static final byte CONNECTED = 0;

    private static final byte CONNECTING = 1;

    private static final byte DISCONNECTING = 2;

    private static final byte DISCONNECTED = 3;

    private static final byte CLOSED = 4;

    //Connect 连接锁,这里可以简单理解为状态锁。
    private final Object mConLock = new Object();

    //当前长连接状态
    private byte mConState = DISCONNECTED;

    //断开长连接标记,防止多次重复断开
    private boolean mStoppingComms = false;

    private boolean mClosePending = false;

    //开始建立连接
    public void connect() {
        synchronized (mConLock) {
        if (isDisconnected() && !mClosePending) {
        conStateChanged(CONNECTING);
        //省略具体发起异步 connect 代码
        ......
        ......
        } else {
            if (isClosed() || mClosePending) {
                throw new Exception(REASON_CODE_CLIENT_CLOSED);
            } else if (isConnecting()) {
                throw new Exception(REASON_CODE_CONNECT_IN_PROGRESS);
            } else if (isDisconnecting()) {
                throw new Exception(REASON_CODE_CLIENT_DISCONNECTING);
            } else {   
                throw new Exception(REASON_CODE_CLIENT_CONNECTED);
            }
        }
    }

    //建立连接请求回调。请求建连失败会抛出异常。
    public void connectComplete(MqttConnack cack, Exception e) throws Exception{
        if (e != null) {
            throw e;
        }
        //同步更新状态,设为连接状态。
        synchronized (mConLock) {
            conStateChanged(CONNECTED);
        }
    }

    //状态赋值
    private void onStateChanged(byte state) {
        synchronized (mConLock) {
            mConState = state;
        }
    }

    public boolean isDisconnected() {   
        synchronized (mConLock) {   
            return mConState == DISCONNECTED;  
        }
    }

    public boolean isClosed() {   
        synchronized (mConLock) {   
            return mConState == CLOSED;  
        }
    }

    public boolean isConnecting() {   
        synchronized (mConLock) {   
            return mConState == DISCONNECTING;  
        }
    }

    public boolean isConnecting() {   
        synchronized (mConLock) {   
            return mConState == DISCONNECTING;  
        }
    }

}

代码中主要包含了connect 和connect 成功的状态更新。

  • 在connect 的启动时将状态设置为Connecting , 主要可以防止多次重复发起请求
  • 建连完成后,判断是否建连成功。若成功,则将状态更新到Connected 状态

注意,若建连请求失败了,则不仅仅是更新状态,还会关闭Socket等操作。我们在下一步Disconnect 操作中介绍。

Disconnect过程

Disconnect过程也分为2步:

  1. 向服务器发送Disconnect 请求,告知客户端要断开了,不要再发送消息了。
  2. 客户端主动关闭Scoket,通知业务长连接断开了。

Disconnect 过程中, 状态会从有这样的流转 Connected -> DisConnecting -> DisConnected , 具体流程如下:

Disconnect过程状态流转.png

从流程中,我们可以看到disconnect动作 只有在connected 状态or connecting 时才允许执行,否则都会抛出异常通知上层业务断连失败。

下面我们再来看看具体的代码实现:在connect中已经定义的变量和方法这里就不再重复贴出来了。

class ClientComms {
    private Callback mCallback;

    //断开长连接标记,防止多次重复断开
    private boolean mStoppingComms = false;

    private boolean mClosePending = false;

    public void disconnect() {
        synchronized (mConLock) {
            if (isClosed()) {
                throw new Exception(REASON_CODE_CLIENT_CLOSED);
            } else if (isDisconnected()) {
                throw new Exception(REASON_CODE_CLIENT_DISCONNECTED);
            } else if (isDisconnecting()) {
                throw new Exception(REASON_CODE_CLIENT_DISCONNECTING);
            } else if (Thread.currentThread() == mCallback.getThread()) {
                throw new Exception(REASON_CODE_CLIENT_DISCONNECT_PROHIBITED);
            }
            //状态更新为断开中
            conStateChanged(DISCONNECTING);
            //发起具体的Disconnect 请求
            ......
            ......
            ......
        }
    }

    public void shutdownConnection(Token token, Exception e) {
        synchronized (mConLock) {
            //已经在停止或者关闭中了,则不再重新关闭
            if (mStoppingComms || mClosePending) { 
                return;
            }
            mStoppingComms = true;
            conStateChanged(DISCONNECTING);
        }

        //此处省略关闭长连接Socket,关闭发送线程,关闭接收线程,关闭callback线程,处理仍未返回的请求。重点讲状态流转赋值操作。
        .....
        .....
        .....
        synchronized (mConLock) {
            conStateChanged(DISCONNECTED);
            mStoppingComms = false;
        }
    }

    //状态赋值
    private void onStateChanged(byte state) {
        synchronized (mConLock) {
            mConState = state;
        }
    }

}
  1. 通过 disconnect() 发起Disconnect 请求,并且将状态置为 Disconnecting 状态。
  2. 通过 shutdownConnection() 方法主动释放所有长连接资源,并将状态置为 Disconnected。

Close 过程

Close过程不需要向服务器发起网络请求,Close会释放所有长连接资源。从设计上是要完全退出长连接系统时才会调用close 。

注意:只有在Disconnected 状态下,即完全停止长连接后才允许close 长连接。

Close 过程如下:

Close过程状态流转.png

源码如下:


class ClientComms {
    private Callback mCallback;
    
    //断开长连接标记,防止多次重复断开
    private boolean mStoppingComms = false;

    private boolean mClosePending = false;

    public void close() {
        synchronized (mConLock) {
            if (!isClosed()) {
                if (isConnecting()) {
                    throw new Exception(REASON_CODE_CONNECT_IN_PROGRESS);
                } else if (isConnected()) {
                    throw new Exception(REASON_CODE_CLIENT_CONNECTED);
                } else if (isDisconnecting()) {
                    mClosePending = true;
                    return;
                }
                conStateChanged(CLOSED);
                //释放所有的资源对象,并将其赋值为null。
                ......
                ......
                ......
            }
        }
    }

    //状态赋值
    private void onStateChanged(byte state) {
        synchronized (mConLock) {
            mConState = state;
        }
    }
}

注意:若close 时发现此时正在disconnecting, 此时我们会用一个 mClosePending 状态来标记该场景。等到disconnecting结束并把状态变更为 disconnected 之后,会自动执行close 动作。

总结

综上所述,长连接各个阶段的状态变更并不复杂,前提是我们需要梳理好各种状态之间的流转关系,确保一定需要满足状态条件才能进入下一个状态阶段。

细心的同学可能会发现,我们所有的状态都是内聚在ClientComms 模块中统一管理的,仅仅只是对外开放相关行为,比如connect , disconnect,close等等行为。 这种好的设计方案值得随时去学习并借鉴到实际项目中去。

我们下篇文章介绍长连接保活-智能心跳的实现