十分钟带你写一个Android Websocket 第二篇

我正在参加「掘金·启航计划」

可以先阅读该系列上一篇文章

1、十分钟带你写一个Android Websocket 第一篇

1、前言

上一篇挖的坑重连策略,这也是WS的重中之重,让我们一起来看下吧。(咕咕咕)一起学习、一起进步。如果写的不好,或者有错误之处,恳请在评论、私信、邮箱指出,万分感谢?

2、介绍

  • 心跳测试法的优化:通过延迟心跳测试法,我们能够确保测试结果的准确性。我们认为,在建立长连接后,连续多次成功的短心跳测试能够极大地保证下一次心跳的环境是正常的。
  • 失败判断的改进:我们采用了成功一次认定,失败连续累积认定的策略。这意味着成功是绝对的,只有在连续多次失败后,我们才会判定为失败的情况。
  • 临界值的巧妙处理:为了避免出现临界值的问题,我们使用略小于计算出的心跳值作为稳定心跳的标准。这样能够更好地确保心跳的稳定性,避免临界值带来的不确定性。
  • 动态调整的灵活性:即使在一次完整的智能心跳计算过程中,我们没有找到最优的数值,也不必担心,因为我们仍然有机会进行校正和调整。这种动态调整的机制使得我们能够不断优化心跳的表现,适应各种复杂的环境和变化。

4、开始

1)定义连接状态

维护一个连接状态变量,用于表示当前WebSocket的连接状态,包括”CONNECTED”(已连接)、”DISCONNECTED”(已断开连接)和”RECONNECTING”(正在重连)等。

private var connectionStatus = ConnectionStatus.DISCONNECTED // 初始状态为断开连接
enum class ConnectionStatus {
    CONNECTED,
    DISCONNECTED,
    RECONNECTING
}

2)准备一个循环

这里我使用的是协程

private var reconnectJob: Job? = null
private fun reconnect() {

    if (reconnectJob?.isActive == true) {
        Log.e(TAG, "reconnect isActive")
        // 避免重复执行重连逻辑
        return
    }
    connectionStatus = ConnectionStatus.RECONNECTING
    reconnectJob = launch(Dispatchers.IO) {
        var retryCount = 0
        while (true) {
            if (connectionStatus != ConnectionStatus.CONNECTED) {
                // 进行重连
                connect()
                Log.e(TAG, "reconnect 进行重连")
                retryCount++
            } else {
                Log.e(TAG, "reconnect 连接成功")
                // 连接成功,退出重连循环
                break
            }

            delay(1000)
        }
    }
}

其中connect的内容如下

private fun connect() {
    Log.e(TAG, "connectWs")
    mWebSocket = wsHttpClient.newWebSocket(requestHttp, object : WebSocketListener() {
        override fun onOpen(webSocket: WebSocket, response: Response) {
            super.onOpen(webSocket, response)
            connectionStatus = ConnectionStatus.CONNECTED
            Log.e(TAG, "WS connection successful")
            // WebSocket 连接建立
        }
      //**
        override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
            super.onMessage(webSocket, bytes)
            // 收到服务端发送来的 ByteString 类型消息
        }
      //**
        override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
            super.onClosed(webSocket, code, reason)
            // WebSocket 连接关闭
            if (code != 1000) {
                reconnect()
            }

        }
        override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
            super.onFailure(webSocket, t, response)
            // 出错了
            reconnect()
        }
    })
}

为什么要code!=1000呢?

1000 indicates a normal closure, meaning that the purpose for
which the connection was established has been fulfilled.
//1000表示正常关闭,这意味着建立连接的目的已经实现。

3)重连间隔控制

其实到这里,一个重连策略就已经完成了,这个重连策略使用1000ms的间隔去频繁的尝试重连,频繁重连对服务端和网络都有压力,所以我们需要去优化,这里我们使用

指数退避算法

来进行重连间隔控制,修改后

private fun reconnect() {
    //**省略**
    reconnectJob = launch(Dispatchers.IO) {
        var retryCount = 0
        while (true) {
            //**省略**
            delay(exponentialBackoffRetry(retryCount))
        }
    }
}
private fun exponentialBackoffRetry(retryCount: Int): Long {
    val maxRetryDelay = 10000L // 最大重试延迟时间(毫秒)
    val baseDelay = 500L // 基础延迟时间(毫秒)
    val multiplier = 1.1 // 延迟时间乘数
    val delay = baseDelay * multiplier.pow(retryCount.toDouble()).toLong()
    return minOf(delay, maxRetryDelay)
}

随着重试次数的增加,重连间隔时间逐渐增加。

4)重连次数控制

如果一定规则下重连不上,不断去尝试重连是没有意义的,为了避免无限重连,可以设置最大重连次数。

private val mMaxRetryCount = 5 // 最大重试次数
private fun reconnect() {

    //**省略**
    reconnectJob = launch(Dispatchers.IO) {
        var retryCount = 0
        while (retryCount <= mMaxRetryCount) {
            //**省略**
            if (retryCount == mMaxRetryCount) {
                Log.e(TAG, "last reconnect max retry")
                break
            }
            delay(exponentialBackoffRetry(retryCount))
        }
    }
}

我们在达到最大的重连次数后,不再进行while循环

5)监听网络状态变化

在我们停止重连时,说明此时客户端网络已经出现了问题,我们可以客户端监听网络状态的变化。当网络重新可用时,启动重连操作,保证在网络可用时及时进行重连。

我们准备一个工具类存放和网络相关的方法

PS:appContext只是一个application context ,如果你测试需要,也可以使用Activity。但是可能会导致内存泄漏

/**
 * 网络状态监测器
 * 注意:需要在适当的时候取消注册以避免内存泄漏
 */
object NetworkStatusMonitor {
    private val connectivityService by lazy {
        appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    }
    private var isRegistered = false;
    var isAvailable = false
        private set
    var isUnAvailable = false
        private set
        get() {
            return !isAvailable
        }
    private val mCallback by lazy {
        object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) {
                // 联网后开始重连
                isAvailable = true
                Log.e("", "onAvailable")
            }
​
            override fun onLost(network: Network) {
                // 断网停止重连
                isAvailable = false
                Log.e("", "onLost")
​
            }
        }
    }
​
    fun register(callback: ConnectivityManager.NetworkCallback) {
        connectivityService.registerNetworkCallback(
            NetworkRequest.Builder()
                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                .build(), callback
        )
        if (!isRegistered) {
            isRegistered = true
            register(callback = mCallback)
        }
    }
​
​
    fun unregister(callback: ConnectivityManager.NetworkCallback) {
        connectivityService.unregisterNetworkCallback(callback)
    }
​
    /**
     * 判断网络是否连接
     */
    fun isNetworkConnected(): Boolean {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            val mNetworkInfo = connectivityService.activeNetworkInfo
            if (mNetworkInfo != null) {
                return mNetworkInfo.isAvailable
            }
        } else {
            val network = connectivityService.activeNetwork ?: return false
            val status = connectivityService.getNetworkCapabilities(network)
                ?: return false
            if (status.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
                return true
            }
        }
        return false
    }
​
    /**
     * 判断是否是WiFi连接
     */
    fun isWifiConnected(context: Context?): Boolean {
        if (context != null) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
                val mWiFiNetworkInfo = connectivityService
                    .getNetworkInfo(ConnectivityManager.TYPE_WIFI)
                if (mWiFiNetworkInfo != null) {
                    return mWiFiNetworkInfo.isAvailable
                }
            } else {
                val network = connectivityService.activeNetwork ?: return false
                val status = connectivityService.getNetworkCapabilities(network)
                    ?: return false
                if (status.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
                    return true
                }
            }
        }
        return false
    }
​
    /**
     * 判断是否是数据网络连接
     */
    fun isMobileConnected(context: Context?): Boolean {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
            val mMobileNetworkInfo = connectivityService
                .getNetworkInfo(ConnectivityManager.TYPE_MOBILE)
            if (mMobileNetworkInfo != null) {
                return mMobileNetworkInfo.isAvailable
            }
        } else {
            val network = connectivityService.activeNetwork ?: return false
            val status = connectivityService.getNetworkCapabilities(network)
                ?: return false
            status.transportInfo
            if (status.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
                return true
            }
        }
        return false
    }
​
}

我们在WsManager中准备一个网络状态变化的监听

private val mNetWorkCallback by lazy {
    object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) {
            // 联网后开始重连
            reconnect()
            Log.e(TAG, "onAvailable reconnect")
        }
​
        override fun onLost(network: Network) {
            // 断网停止重连
            if (!NetworkStatusMonitor.isNetworkConnected()) {
                cancelReconnect()
            }
            Log.e(TAG, "onLost cancelReconnect")
        }
    }
}

​

cancelReconnect想必你也知道是什么啦~就是取消协程

private fun cancelReconnect() {
    connectionStatus = ConnectionStatus.DISCONNECTED
    reconnectJob?.cancel()
}

在我们openWs中,注册监听

fun openWs() {
    if (connectionStatus == ConnectionStatus.RECONNECTING || connectionStatus ==      ConnectionStatus.CONNECTED) {
        return
    }
    connect()
    NetworkStatusMonitor.register(mNetWorkCallback)
}

至此我们一个重连策略就好了很多啦

image-20230616173934901

image-20230616174020318

当然你还可以考虑到客户端切换前后台来修改重连策略,不过就自己扩展吧~~

一个简单的WsManager就做好了,当然了还有许多东西都没有扩展好(因为天气太热了),大部分代码也都塞在一个类里面的。不过我相信各位还是好懂的,咕咕咕咕

5、下个篇章

因为篇幅原因,我们先到这,哈、下一篇还没想好写什么,如果你有想看的也可以在评论区,或者私信给我,马上写~咕咕咕。

如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。?

6、感谢

  1. 校稿:ChatGpt
  2. 文笔优化:ChatGpt

代码可能有所出入,不过大差不差啦

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MY3GKJmN' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片